多线程——线程的休眠、中断和等待
目录
1.线程启动
2.线程休眠
3.线程中断
3.1 自定义变量作为标记
3.2 使用 Thread 类提供的 interrupt() 方法
4.线程等待
上一期讲到线程的创建以及Thread的常见属性,并且提出一些概念如线程休眠、线程中断和线程等待等,这期将会对它们做一个深入理解。
1.线程启动
其在这在创建线程时就已经介绍,一个线程的创建要调用 start() 方法,然后触发线程的 run() 方法,由JVM调度执行。关于 start() 和 run() 的区别在上期末尾已经总结。
但是,值得注意的是,一个 Thread 对象,只能 start 一次,如果有两个或多个 start 同一个 Thread 对象,运行时将会抛出异常 IllegalThreadStateException。
2.线程休眠
假设:车间里的工人,人是会疲劳的,但是车间的生产机器可以不停地工作,所以为了机器可以一直工作,工人就需要轮班,这车间里的工位让出来,留给换班的工人。此时工人休息就属于休眠行为。
线程休眠(Thread.sleep(时间ms)
)是 Java 多线程编程中的一个重要机制,它允许当前执行的线程暂停一段时间,让出 CPU 资源给其他线程使用,Thread.sleep(时间),这里的时间是以毫秒(ms)为单位
。
比如,在前面的代码中,我们不能很好地看到打印结果,也就是 thread 和 main 是怎么执行的,为方便理解,我们可以给代码写一个死循环,并给一定的休眠时间,观察它们的执行顺序。
示例代码:
public class Demo7 {public static void main(String[] args) {Thread thread = new Thread( () -> {while (true) {System.out.println("hello thread");try {Thread.sleep(1000);//以ms为单位,这里是1000ms,相当于休眠1秒} catch (InterruptedException e) {throw new RuntimeException(e);}}});thread.start();while (true) {System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
运行结果:
从运行结果来看,两个打印是交替打印的。为什么呢?这是因为线程的交替执行是由操作系统的线程调度机制决定的,会根据线程的状态、优先级等交替执行每一个线程。也正是因为线程的调度是不可控的,所以这个休眠时间也不一定真的是每隔一秒才会打印一次。
在线程休眠中,Thread.sleep()
这个方法可以传入两个参数,即
Thread.sleep(时间1 ms,时间2 ns),
时间1和时间2的作用是一样的,表示总休眠时间是时间1 + 时间2,前者单位是毫秒,后者单位是纳秒。但是因为线程是不可控的,它实际休眠的时间也并不一定等于设置的时间参数。
或者说:sleep()
的纳秒参数(sleep(ms, ns)
)通常用于高精度计时,但实际休眠时间受操作系统调度影响,无法保证精确性。
3.线程中断
假设:在一生产车间里,工人正在工作,突然车间里的经理发现车间某个角落着火了,但是工人并没有发现,此时此时经理就需要让所有的工人立刻撤离,但是经理无法直接拽走工人,而是让工人赶快撤离,工人听见后,需要立刻停止当前的工作,并赶快安全按撤离。此时经理让工人赶快撤离,就属于中断行为。
线程中断主要有两种方式,一是自定义一个变量来标记进行沟通,二是使用 Thread 类提供的 interrupt() 方法。
3.1 自定义变量作为标记
这个方法就类似于经理需要自己去喊工人赶快撤离。
(这里用到一个关键字 volatile,volatile
确保多线程间对变量的可见性,避免编译器优化导致读取旧值,这里知道有这么一个关键字即可,在后面两期会介绍到。)
示例代码:
public class Demo8 {private volatile static boolean isFinished = false;//定义标志public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {while (!isFinished) {System.out.println("hello thread");try {Thread.sleep(1000);//每隔1秒打印一次} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("thread结束!!!");});thread.start();Thread.sleep(5000);//这个线程跑5秒isFinished = true;//更改标志位}
}
不过这种方式,需要注意的是,自定义的标志不能把它定义成局部变量,即这里不能把它定义在 main 方法里,如果定义在 main 方法里,那么这个标志(isFinished)将不能修改,不能修改将导致它进入死循环。为什么呢?
主要原因就是 lambda 表达式,这里创建的线程实际上 在lambda 里面,当希望使用局部变量时,是触发“变量捕获”这样语法,而 lambda 是回调函数,执行的时机可能会在操作系统真正创建出线程之后,而当线程创建好后,可能 main 方法已经执行完毕,对应的局部变量也就销毁了。为了解决这个问题,其实Java会把捕获到的变量临时拷贝一份,拷贝给 lambda ,当局部变量是否销毁也不会影响到 lambda 。但是,拷贝也就意味着重新创建了一个变量,也就是当前的 isFinished 其实已经不是最开始定义的,所以在这里不能修改。所以把这个自定义的标志定义为局部变量时,不仅不能修改,更不能中断程序。(即使这里不是 lambda 表达式创建的线程,而是使用匿名内部类,依然也是同样结果(比如以下的示例代码)。
public class Demo9 {public static void main(String[] args) throws InterruptedException {boolean isFinished = false;//定义标志为局部变量Thread thread = new Thread() {@Overridepublic void run() {while (!isFinished) {System.out.println("hello thread");try{Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("thread结束!!!");}};thread.start();Thread.sleep(5000);//isFinished = true;这里不能修改,如果修改就报错}
}
为什么定义为全局变量就可以呢?这是因为定义为全局变量后,不再是“变量捕获”的语法,而是切换成了“内部类访问外部类的成员”语法,创建的线程相当于内部类,定义的标志变量就相当于外部类,所以当main方法执行结束后,并不会影响这个全局变量,也不需要拷贝,从始至终都是一个变量,所以这里本质就是“内部类访问外部类的成员”语法,自然也就可以修改,从而达到中线线程的目的。
3.2 使用 Thread 类提供的 interrupt() 方法
Java中 Thread 提供了interrupt() 方法,它交由线程自身检查,如果想让线程中断,人为只需要调用这个方法就可以。这个方法就类似于经理去拉警报器,警告工人赶紧撤离。
示例代码:
public class Demo10 {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("thread结束了");});thread.start();Thread.sleep(3000);System.out.println("main结束了");thread.interrupt();}
}
这里抛出的异常是因为线程在 Thread.sleep() 期间被中断(InterruptedException),导致线程异常终止。在这里,如果不希望抛出这个异常,想要代码优雅的跳出循环,就可以把用 break。
但是,对于Java来说,如果没有这个 break ,它又会进入应该死循环:
这是因为:sleep() 被中断了,相当唤醒了 sleep ,并修改 isInterrupted(),由于没有 break
,循环会继续执行,而 !Thread.currentThread().isInterrupted()
会返回 true
(因为中断状态已被清除)所以导致线程无法退出,进入死循环。
4.线程等待
假设:你和你的女神约了明天上午一起去逛街,约定的时间是明天早上八点见面。你会非常地激动,于是你明天会准到到达约定地点,但是你的女神,可能因为路上堵车或者早上睡过头了(也有可能放你鸽子但是你不知道),所以没有在规定的时间到达约定的地点。这个时候,你心想好不容易能和女神一起逛街,于是你就会在约定的地点等你的女神,并且你还给自己大气,女神不出现你就线不吃早饭了,你想和女神一起吃早饭。你在等你的女神出现就属于等待行为。
线程等待(join()),就是一个线程需要等待另一个线程完成它的工作后,这个线程才能进行自己的下一步工作。
join() 会有异常,所以需要捕获异常(在IDEA中对 sleep 按CTRL + 回车 自动补全),但在这里不是抛出异常,而在 main 方法里声明一个可能出现的异常,即 throws InteruptedException,需要和 sleep() 做一个区分。
代码实例:
public class Demo12 {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("thread 结束...");});thread.start();thread.join();System.out.println("main 结束....");}
}
比如上面的代码中,在 main 线程中,调用 thread.join() 方法,这个时候,main 这个线程需要等到 thread 这个线程执行完毕之后,main 这个线程才会执行。
需要着重理解是那个线程等待,哪个线程先执行,可以理解位一个插队行为,比如上面的代码中,main 线程的结束是在最后,因为在main 线程还没结束之前,thread 这个线程插队了。
假如,现在把你的等待条件设置如果女神不出现,你也不走,你心里想着不见不散,那么这个时候你就会一直等下去。显然这是不符合现实的,所以你可能会等上一段时间,一段时间后你的女神还没有出现,那么你就会回去。
所以在线程中,在方法 join() 就可以加一个等待的时间,也就是超时机制,即方法 join(时间ms)。这个方法和线程休眠的方法 sleep() 是类似的,也可以写成 join(时间1 ms,时间2 ns),表示总等待时间是时间1 + 时间2,前者单位是毫秒,后者单位是纳秒。但是因为线程是不可控的,它实际等待的时间也并不一定等于设置的时间参数,超时后将不再等待。
public class Demo12 {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {System.out.println("hello thread");try {Thread.sleep(1000);//每隔1秒打印一次} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("thread结束!!!");});thread.start();thread.join(3600*1000);//等待一小时thread.interrupt();System.out.println("main 结束...");}
}
这个时候我们也可以在 jconsole 中查看线程的信息:
可以发现 main 线程是需要等待执行的,而线程 Thread-0 的等待是说明还没有到结束时间,也就是你还在等你的女神中。main 线程需要等待 thread 线程指定时间后,才会执行。
本篇文章主要探讨线程的启动、休眠、中断和等待等核心操作:
-
线程启动:通过
start()
方法触发线程执行,且每个线程只能启动一次。 -
线程休眠:
Thread.sleep()
让线程暂停执行,但实际休眠时间可能受系统调度影响。 -
线程中断:通过标志位或
interrupt()
方法通知线程终止。 -
线程等待:
join()
实现线程间的等待,可设置超时避免无限等待。
线程经历的启动、休眠、中断和等待,在这个过程中,都经历了哪些状态呢?欲知后事如何,且听下回分解!