五、睡眠和唤醒一个线程
有时,你会想要在一段特定的时间后再去中断线程的运行。举个例子,程序中的一个线程每一分钟检查一次传感器的状态,剩余的时间,线程应该处于空闲的状态。在这段空闲时间里,线程不会使用计算机的任何资源。一分钟后,线程已经准备好了,才让JVM选择调用它继续执行。你可以使用 Thread 类的 sleep() 方法来达到此目的。该方法接受一个 int 类型参数表明线程挂起不运行的毫秒数。当睡眠时间结束,线程转移到可运行状态等待JVM的调度。
TimeUnit 枚举类的某个成员同样具有 sleep() 方法。该方法利用了 Thread 类的 sleep() 方法,使得当前线程睡眠,但是它接受的参数的单位并不是固定为毫秒数,更加方便。
在本秘诀中,我们开发一个利用 sleep() 方法每秒输出 Date 值的程序。
public class FileMain { public static void main(String[] args) { FileClock clock = new FileClock(); Thread thread = new Thread(clock); thread.start(); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } thread.interrupt(); } } public class FileClock implements Runnable{ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.printf("%s\n", new Date()); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { System.out.printf("The FileClock has been interrupted"); } } } }
当你调用 sleep() 方法时,线程就会放弃占用CPU和停止执行一段时间。在这段时间里,它不占用CPU时间。
当线程在睡眠中并且被中断时,该方法立即抛出一个 InterruptedException 异常,并不会等待直到睡眠时间到达。
注意:yield() 方法也具有让线程放弃占用CPU的功能,不过,这个方法最好只用来表明自己可以放弃CPU占用,JVM的规范中没有强制要求其必须要放弃CPU占用。这个方法一般只用在调试环境中,让具有更高权限的线程可以抢占执行。
笔者总结:线程调用 sleep() 方法后,该线程处于挂起状态,自身不会去抢占CUP等资源来运行,但JVM内部有系统线程去感知是否该睡眠线程被中断,从而以抛出异常方式通知睡眠线程提前唤醒来运行代码处理此种情况。
六、等待线程的完成
某些场景下,我们可能需要等待线程的终结。举个例子:我们的程序在顺序上可能需要在执行某些步骤之前需要初始化某些资源。我们可以把初始化资源的任务作为一个单独的线程运行,并且在主任务线程中等待它完成后再进行下一步的处理。
我们可以使用 Thread 类的 join() 方法达到该目的。当我们调用一个 Thread 对象的 join() 方法时,调用此方法的线程就会挂起,直到 Thread 对象关联的线程执行完毕。
public class DataSourcesLoader implements Runnable{ @Override public void run() { System.out.printf("Beginning data sources loading: %s\n", new Date()); try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } System.out.printf("Data sources loading has finished: %s\n", new Date()); } } public class Main { public static void main(String[] args) { DataSourcesLoader dsLoader = new DataSourcesLoader(); Thread thread1 = new Thread(dsLoader, "DataSourceThread"); NetworkConnectionsLoader ncLoader = new NetworkConnectionsLoader(); Thread thread2 = new Thread(ncLoader, "NetworkConnectionLoader"); thread1.start(); thread2.start(); try { thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.printf("Main: Configuration has been loaded: %s\n", new Date()); } } public class NetworkConnectionsLoader implements Runnable{ @Override public void run() { System.out.printf("Beginning Network connection: %s\n", new Date()); try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } System.out.printf("Network connection has finished: %s\n", new Date()); } }
如果你运行此示例程序多次后就会发现,每次都是先运行 thread1 并结束后,才运行 thread2,thread2结束后才会结束 main 线程的运行。
join() 方法具有两个类似的方法,他们分别是:
(A) join(long milliseconds)
(B) join(long milliseconds, long nanos)
这两个方法通向会挂起调用线程直到该方法所属的 Thread 对象关联的线程执行结束,但是在指定的时间达到后,调用线程可以继续运行,不用一直等待。
笔者总结:这三个方法经常用来线程之间的同步,带参数的方法可以实现超时不等待逻辑。
重要:本系列翻译文档也会在本人的微信公众号(此山是我开)第一时间发布,欢迎大家关注。