构造线程
在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需要提供线程所需要 的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息。
private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc) { if (name == null) { throw new NullPointerException("name cannot be null"); } // 当前线程就是该线程的父线程 Thread parent = currentThread(); this.group = g; // 将daemon、priority属性设置为父线程的对应属性 this.daemon = parent.isDaemon(); this.priority = parent.getPriority(); this.name = name.toCharArray(); this.target = target; setPriority(priority); // 将父线程的InheritableThreadLocal复制过来 if (parent.inheritableThreadLocals != null) this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent. inheritableThreadLocals); // 分配一个线程ID tid = nextThreadID(); }
在上述过程中,一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程 继承了parent是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的 ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程。至此,一个能够运行的线程对 象就初始化好了,在堆内存中等待着运行。
启动线程
线程对象在初始化完成之后,调用start()方法就可以启动这个线程。线程start()方法的含义 是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用 start()方法的线程。
启动一个线程前,最好为这个线程设置线程名称,因为这样在使用jstack分析程 序或者进行问题排查时,就会给开发人员提供一些提示,自定义的线程最好能够起个名字。
理解中断
中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行 了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt() 方法对其进行中断操作。
线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否 被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。如果该 线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返 回false。
从Java的API中可以看到,许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法)这些方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false。
如下的代码,首先创建了两个线程,SleepThread和BusyThread,前者不停地睡眠,后者一直运行,然后对这两个线程分别进行中断操作,观察二者的中断标识位。
public class Interrupted { public static void main(String[] args) throws Exception { // sleepThread不停的尝试睡眠 Thread sleepThread = new Thread(new SleepRunner(), "SleepThread"); sleepThread.setDaemon(true); // busyThread不停的运行 Thread busyThread = new Thread(new BusyRunner(), "BusyThread"); busyThread.setDaemon(true); sleepThread.start(); busyThread.start(); // 休眠5s,让sleepThread和busyThread充分运行 TimeUnit.SECONDS.sleep(5); sleepThread.interrupt(); busyThread.interrupt(); System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted()); System.out.println("BusyThread interrupted is " + busyThread.isInterrupted()); //防止sleepThread和busyThread立刻退出 TimeUnit.SECONDS.sleep(2); } static class SleepRunner implements Runnable { @Override public void run() { while (true) { SleepUtils.second(10); } } } static class BusyRunner implements Runnable { @Override public void run() { while (true) { } } } }
输出如下
SleepThread interrupted is false BusyThread interrupted is true
从结果可以看出,抛出InterruptedException的线程SleepThread,其中断标识位被清除了, 而一直忙碌运作的线程BusyThread,中断标识位没有被清除
过期的suspend(),resume()和stop()
大家对于CD机肯定不会陌生,如果把它播放音乐比作一个线程的运作,那么对音乐播放做出的暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。
如下代码中,创建了一个线程PrintThread,它以1秒的频率进行打印,而主线程对其进行暂停、恢复和停止操作。
public class Deprecated { @SuppressWarnings("deprecation") public static void main(String[] args) throws Exception { DateFormat format = new SimpleDateFormat("HH:mm:ss"); Thread printThread = new Thread(new Runner(), "PrintThread"); printThread.setDaemon(true); printThread.start(); TimeUnit.SECONDS.sleep(3); // 将PrintThread进行暂停,输出内容工作停止 printThread.suspend(); System.out.println("main suspend PrintThread at " + format.format(new Date())); TimeUnit.SECONDS.sleep(20); // 将PrintThread进行恢复,输出内容继续 printThread.resume(); System.out.println("main resume PrintThread at " + format.format(new Date())); TimeUnit.SECONDS.sleep(3); // 将PrintThread进行终止,输出内容停止 printThread.stop(); System.out.println("main stop PrintThread at " + format.format(new Date())); TimeUnit.SECONDS.sleep(3); } static class Runner implements Runnable { @Override public void run() { DateFormat format = new SimpleDateFormat("HH:mm:ss"); while (true) { System.out.println(Thread.currentThread().getName() + " Run at " + format.format(new Date())); SleepUtils.second(1); } } } }
输出如下(输出内容与执行的具体时间有关)
PrintThread Run at 17:34:36 PrintThread Run at 17:34:37 PrintThread Run at 17:34:38 main suspend PrintThread at 17:34:39 main resume PrintThread at 17:34:42 PrintThread Run at 17:34:42 PrintThread Run at 17:34:43 PrintThread Run at 17:34:44 main stop PrintThread at 17:34:45
在执行过程中,PrintThread运行了3秒,随后被暂停,3秒后恢复,最后经过3秒被终止。
通过示例的输出可以看到,suspend()、resume()和stop()方法完成了线程的暂停、恢复和终 止工作,而且非常“人性化”。但是这些API是过期的,也就是不建议使用的。
不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资 源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结 一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会, 因此会导致程序可能工作在不确定状态下。
正因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建 议使用的过期方法,而暂停和恢复操作可以用后面提到的等待/通知机制来替代。
安全的终止线程
中断状态是线程的一个标识位,而中断操作是一种简便的线程间交互 方式,而这种交互方式最适合用来取消或停止任务。除了中断以外,还可以利用一个boolean变 量来控制是否需要停止任务并终止该线程
如下,创建了一个线程CountThread,它不断地进行变量累加,而 主线程尝试对其进行中断操作和停止操作。
public class Shutdown { public static void main(String[] args) throws Exception { Runner one = new Runner(); Thread countThread = new Thread(one, "CountThread"); countThread.start(); // 睡眠1秒,main线程对CountThread进行中断,使CountThread能够感知中断而结束 TimeUnit.SECONDS.sleep(1); countThread.interrupt(); Runner two = new Runner(); countThread = new Thread(two, "CountThread"); countThread.start(); // 睡眠1秒,main线程对Runner two 进行取消,使CountThread能够感知on为false而结束 TimeUnit.SECONDS.sleep(1); two.cancel(); } private static class Runner implements Runnable { private long i; private volatile boolean on = true; @Override public void run() { while (on && !Thread.currentThread().isInterrupted()) { i++; } System.out.println("Count i = " + i); } public void cancel() { on = false; } } }
输入如下
Count i = 543487324 Count i = 540898082
示例在执行过程中,main线程通过中断操作和cancel()方法均可使CountThread得以终止。 这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地 将线程停止,因此这种终止线程的做法显得更加安全和优雅。