目录
1.Thread类常用的构造方法
2.Thread类的几个常见属性
2.1 什么是守护线程?isDaemon
2.2线程是否存活 isAliye()
3.终止线程的方法
3.1使用共享标志位通知中断线程
3.2使用Thread自带的标志位通知
4.等待线程 join
5.获取当前线程的引用
6.休眠当前线程
Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联
1.Thread类常用的构造方法
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象并命名 |
Thread(Runnable target,String name) | 使用Runnable对象创建线程对象并命名 |
多了个name参数是为了给线程一个名称方便调试线程
我们使用最后一个方法创建对象并且命名,然后再java工具中找到这个name的线程
public class ThreadDemo6 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println("hello world!!");
}
}
},"myThread");
t.start();
}
}
这里就可以看到我们刚刚创建并且命名的线程了,t是代码里面的变量名,myThread是线程名
可以看到这里这里没有main线程了,是因为主线程执行完了start之后就紧接着结束了main方法,对主线程来说,main方法没了,自己也就结束了
同样的,当run方法执行完了,myThread线程也就自动结束了
2.Thread类的几个常见属性
属性 | 获取方法 | 说明 |
---|---|---|
ID | getId() | 获取到线程的身份标识 |
名称 | getName() | 获取到构造方法中的名字 |
状态 | getState() | 获取到线程的状态(Java中的线程的状态比操作系统的原生的状态更丰富一些) |
优先级 | getPriority() | 获取或者设置优先级 |
是否后台线程 | isDaemon() | 是否是守护线程 |
是否存活 | isAliye() | 判定线程是否存活 |
是否被中断 | isInterrupted() | 是否被中断 |
2.1 什么是守护线程?isDaemon
java中的线程分为前台和后台线程,代码里手动创建的线程,Main线程都是前台的线程,其它的JVM自带的线程都是后台的线程,也就是守护线程.
也可以通过setDaemon来手动设置成后台线程
前台线程和后台线程的特点:
前台线程会阻止进程的结束,前台线程的工作没有做完,进程是不能结束的,后台线程不会阻止进程结束,后台工作没做完,进程是可以结束的
刚刚在上面创建的myThread线程默认是前台线程,我们使用 setDaemon来手动设置成后台线程
t线程还没执行完,但是进程直接结束了
当t被设置成守护线程,那么进程的结束与否就与t没有关系了,此时前台线程只剩下主线程了,主线程什么时候执行完,进程什么时候结束
2.2线程是否存活 isAliye()
回顾一下线程是如何被创建的
因此,在调用start()之前调用isAliye()结果应该是false,调用start()之后调用isAliye()结果应该是true.
isAliye()是在判断当前系统里的线程是否是真的创建了,如果内核中的线程把run()执行完了,PCB释放了,但是t这个对象不一定被释放,此时isAliye()被调用后还是false
也可以看出t这个对象的生命周期,是比内核中的PCB生命周期长的
下面看代码演示:
public class ThreadDemo6 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello world!!");
}
},"myThread");
t.start();
while(true){
try {
Thread.sleep(1000);
System.out.println(t.isAlive());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
run()方法执行完了打印helloworld之后线程就结束了,即PCB没有了, 但是t对象还在,此时再去调isAlive(),结果就是false.
当引用不指向对象,t才会被GC回收
如果让run()方法执行慢一点,就能看到true了
将run()方法改为
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("hello world!!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
运行后run()3s后才被销毁,期间isAlive()返回true,3s后线程结束,isAlive()返回false
线程之间是并发执行并且是抢占式调度的,因此先执行哪个线程是不确定的,这里根据结果我们大概可以看到,第一轮,主线程先sleep,先执行t线程,然后打印了helloworld!!,紧接着也进入了sleep,接下来就是看谁先唤醒,然后继续执行,,从结果看到执行的是hello world!!因此还是t线程先唤醒,继续执行,第一轮执行结束,主线程打印true.进入sleep,第二轮开始,t线程打印helloworld!!,然后主线程打印true,进入sleep,然后时间来到了3s,t线程结束,主线程一直打印false
这两轮操作除了第一个helloworld是确定的,后面的hello在前还是true在前都是无法确定的,完全看调度器是如何调度的结果,这就是抢占式执行
看一个清楚点的例子:
public class ThreadDemo7 {
public static void main(String[] args) {
Thread t = new Thread(()->{
System.out.println("hello world!!");
});
t.start();
System.out.println("hello main!!");
}
}
这两个线成所执行的顺序完全就是随机的,也就是先打印出哪个,完全是不确定的 ,这和我们以前写代码,是固定的顺序执行的是不同的,理解多线程代码需要考虑到无数的顺序
总结一下
当run()没跑的时候,isAlive()返回false
当run()正在跑的时候,isAlive()返回true
当run()跑结束的时候,isAlive()返回false
3.终止线程的方法
中断一个线程不是让线程立即就停止工作,只是告诉它应该停止了,是否真的停止,要看线程内的代码逻辑
常见的中断线程有以下两种方式:1. 通过共享的标记来进行沟通2. 调用 interrupt() 方法来通知
3.1使用共享标志位通知中断线程
在主线程创建一个共享标志位flag,通过它的改变来通知线程是否中断
public class ThreadDemo8 {
private static boolean flag = true;
public static void main(String[] args) {
Thread t = new Thread(()->{
while (flag){
System.out.println("hrllo world!!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
flag = false;
}
}
运行结果
主线程sleep3s,因此t线程运行3s之后,共享标志位发生改变,t线程就结束了
注意:这个代码之所以能修改了标志位,就起到中断t的作用,完全取决于线程t的代码是什么逻辑,如果代码内部不用flag控制循环,那么标志位的改变也不起任何作用,因此,这个标志位的改变咱们就是说只是通知它要中断了,是否中断还是看代码内部来决定的
自定义的共享标志位这种方式,也是不能及时响应的,像循环里有sleep的代码,无法在更改标志位后立即响应,这时线程t已经执行了很多代码了,才会收到通知,下来我们看一下使用Thread自带的标志位通知
3.2使用Thread自带的标志位通知
public class ThreadDemo9 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while (!Thread.currentThread().isInterrupted()){
System.out.println("hello world!!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt();
}
}
Thread.currentThread()是Thread类的静态方法,通过这个方法可以获取到当前线程的对象的引用,谁调用它就会获取谁的对象的引用,类似于this
isInterrupted()为true表示被终止,为false表示继续执行
!Thread.currentThread().isInterrupted()如果在t.run()中被调用,获取的就是t线程
加了!是逻辑取反,当isInterrupted()是true时,是要中断线程的,while(flag)中flag取false才能中断,因此要加!来取反
t.interrupt()表示终止线程,哪个对象调用就终止呢个线程,这里main线程调用t.interrupt()表示的是main线程通知t线程你该终止了!!
isInterrupted()相当于设置一个布尔变量,t.interrupt()方法是在设置这个变量.3.1中的方法是直接操作一个布尔变量,3.2的方法是把布尔变量操作封装到Thread方法中了
封装到Thread类之后,如果线程在sleep中休眠,此时调用interrupt()来唤醒t线程,这时就不继续休眠了.interrupt()会直接触发sleep中的异常
InterruptedException
导致sleep提前返回了,我们看一下上面代码的运行结果
可以发现:前三次是正常执行的,然后调用t.interrupt()触发异常后t线程中断了,中断之后t线程又继续运行了!!这是什么原因呢?
是因为t.interrupt()后首先把线程内部的标志位设置成true,若线程再进行sleep,会唤醒sleep,触发异常!!但是还没完,触发异常之后,又将标志位在设置回false,相当于清空了标志位,导致了sleep的异常被catch了之后,代码继续执行!!因此我们看见的结果是先运行然后中断,然后继续运行!还要注意调用t.interrupt()只是main线程通知t线程终止,t线程并不是一定会终止!!
这种情况是线程t忽略了main的终止请求
加上break后,线程t就立即响应main的终止请求
这种情况是稍后进行终止
在catch处可以添加任意代码,因此为什么sleep的标志位会被清除就很明了了,在sleep被唤醒之后,线程到底是要终止还是继续运行,选择权就交给程序员了!!可以通过代码控制到底继续执行还是终止
4.等待线程 join
线程是一个随机调度的过程,等待线程就是控制两个线程的结束顺序,因为线程调度是随机的,我们无法控制线程的开始顺序,但是能通过方法控制线程的结束顺序
使用的是join()方法
public class ThreadDemo10 {
public static void main(String[] args) {
Thread t = new Thread(()->{
for (int i = 0; i < 3; i++) {
System.out.println("hello world!!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
System.out.println("join前");
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("join后");
}
}
此处的join()方法作用就是让main线程等待t线程执行结束
当start启动t线程时,主线程和t线程就开始运行了,主线程走到t.join()时,就发生阻塞了!!
一直阻塞到t线程执行结束,主线程才会从阻塞中恢复过来,才能继续往下执行!!因此t线程一定是比主线程先结束的,就达到了有序结束线程的目的!
从结果也可以看出来,两个线程开始运行后,主线程先打印了join之前,然后t线程执行,此时主线程阻塞了,t线程执行的时候并没有继续执行,也就是打印join之后,当t执行完了,才打印的join之后
还有一个问题,如果主线程执行join之前,t线程就结束了,会发生什么?
调整一下代码,让主线程sleep5s,其它地方不变,t线程3s就结束了,我们看效果
结果:t线程执行结束后,主线程是没有阻塞的,直接返回了,执行结束了
因此假设开始执行join时,t已经结束了,join就不会再阻塞了,而是会立即返回!!
join方法有下面三种:
方法 | 说明 |
---|---|
public void join() | 一直等待,等到线程结束 |
public void join(long mills) | 等待线程结束,最多等 millis 毫秒 |
public void join(long mills,int nanos) | 等待线程结束,最多等 millis 毫秒,但可以更高精度 |
第二种比较常用,如果等到一定的时间没有结果,就不等了,一直死等会让程序无法继续执行了!
5.获取当前线程的引用
这个方法之前就已经了解过了
public static Thread currentThread();
功能:返回当前线程对象的引用
这是一个静态方法,可以直接通过类名调用,不需要实例化对象来调用
public class ThreadDemo11 {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
结果是获取当前线程的对象的引用
该方法是通过类名调用的,也就是类方法,为什么不构造一个新的关键字表示"类属性类方法"?
当构造一个新的关键字,必然要有一个名字,这个名字肯定不能作为变量名,方法名,一旦引入新的关键字,就需要考虑改动所有和它冲突的方法名,变量名,那么带来的结果是很严重的,因此就直接把static关键字拿过来使用了,不管他本来什么意思,直接赋予了新的使命!
6.休眠当前线程
本质上是让这个线程不参与调度,不去cpu上执行了
线程调度是不可控的,因此休眠时间会大于等于参数设置的时间,即使你参数设置的时间到了,也不一定会立即调度然后执行这个线程
之前谈到过就绪队列,链表中的PCB都是等待调度的就绪状态,如果A线程在就绪队列中,并且调用sleep,那么A就会立即进入一个阻塞队列,这个队列的PCB都是阻塞的,不参与调度.
当这个PCBsleep结束之后回到就绪队列,考虑到实际的调度开销,对应的线程回到就绪队列后是无法立即被调度的!!!
看一下休眠的方法
方法 | 说明 |
---|---|
public static void sleep(long millis) throws InterruptedException | 休眠当前线程 millis毫秒 |
public static void sleep(long millis, int nanos) throwsInterruptedException | 高精度的休眠 |
public static void main(String[] args) {
System.out.println(System.currentTimeMillis());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis());
}
结果是main线程休眠了3s然后继续执行!