线程简介
多线程
一个Java程序的运行不仅仅是main()方法的运行,而是main线程和多个其他线程的同时运行。
1 | public class MultiThread { |
1 | [5] Attach Listener //提供一种jvm进程间通信的能力,能让一个进程传命令给另外一个进程,并让它执行内部的一些操作 |
线程优先级
在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。
在不同的JVM以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定,示例如下:
1 | public class Priority { |
1 | Job Priority : 1,Count : 6448504 |
从输出可以看到线程优先级没有生效,优先级1和优先级10的Job计数的结果非常相近,没有明显差距。这表示程序正确性不能依赖线程的优先级高低。
注意:线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java线程对于优先级的设定。
经验证,Mac OS X 10.10,Java版本为1.7.0_71环境下所有Java线程优先级均为5(通过jstack查看),对线程优先级的设置会被忽略。另外,尝试在Ubuntu 14.04环境下运行该示例,输出结果也表示该环境忽略了线程优先级的设置。
线程状态
下图所示的6种不同状态,在给定的一个时刻,线程只能处于其中的一个状态。
下面我们使用jstack工具(可以选择打开终端,键入jstack或者到JDK安装目录的bin目录下执行命令),尝试查看示例代码运行时的线程信息,更加深入地理解线程状态,示例如下:
1 | public class SleepUtils { |
1 | public class ThreadState { |
运行该示例,打开终端或者命令提示符,键入“jps”,输出如下:
可以看到运行示例对应的进程ID是12632,接着再键入“jstack 12632”(这里的进程ID需要和读者自己键入jps得出的ID一致),部分输出如下所示:
通过示例,我们了解到Java程序运行中线程状态的具体含义。线程在自身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换,Java线程状态变迁如下所示:
注意:Java将操作系统中的运行和就绪两个状态合并称为运行状态。阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在java.concurrent包中Lock接口的线程状态却是等待状态,因为java.concurrent包中Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法。
1 | public static void main(String[] agrs) throws InterruptedException { |
1 | /* 先唤醒th线程,再阻塞th线程,最终th线程没有被阻塞,从广义上来说park和unpark代表阻塞和唤醒 */ |
LockSupport的设计思路是通过许可证来实现的,就像汽车上高速公路,入口处要获取通行卡,出口处要交出通行卡,如果没有通行卡你就无法出站,当然你可以选择补一张通行卡。
LockSupport会为使用它的线程关联一个许可证(permit)状态,permit的语义「是否拥有许可」,0代表否,1代表是,默认是0。
- LockSupport.unpark:指定线程关联的permit直接更新为1,如果更新前的permit<1,唤醒指定线程
- LockSupport.park:当前线程关联的permit如果>0,直接把permit更新为0,否则阻塞当前线程
因park
阻塞的线程不仅仅会被unpark
唤醒,还可能会被线程中断(Thread.interrupt
)唤醒,而且不会抛出InterruptedException
异常,所以建议在park
后自行判断线程中断状态,来做对应的业务处理。
为什么推荐使用LockSupport
来做线程的阻塞与唤醒(线程间协同工作)?
- 操作更精准,可以准确地唤醒某一个线程(notify随机唤醒一个线程,notifyAll唤醒所有等待的线程)
- 无需竞争锁对象(以线程作为操作对象),不会因竞争锁对象产生死锁问题
- unpark与park没有严格的执行顺序,不会因执行顺序引起死锁,比如「Thread.suspend和Thread.resume」没按照严格顺序执行,就会产生死锁
LockSupport
还提供了park
的重载函数,提升灵活性:
- void parkNanos(long nanos):增加了超时机制
- void parkUntil(long deadline):加入超时机制(指定到某个时间点,1970年到指定时间点的毫秒数)
- void park(Object blocker):设置blocker对象,当线程没有许可证被阻塞时,该对象会被记录到该线程的内部,方便后续使用诊断工具进行问题排查
- void parkNanos(Object blocker, long nanos):设置blocker对象,加入超时机制
- void parkUntil(Object blocker, long deadline):设置blocker对象,加入超时机制(指定到某个时间点,1970年到指定时间点的毫秒数)
守护线程
守护线程简述
- 指的是程序运行时在后台提供的一种通用服务的线程。
- 在操作系统里面是没有所谓的守护线程的概念,只有守护进程一说。
- Java平台把操作系统的底层给屏蔽了,在它自己虚拟的JVM平台里面构造出对自己有利的机制。
守护线程是一种支持型线程,它主要被用作程序中后台调度以及支持性工作。比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不是程序中不可或缺的部分。
User Thread(用户线程)和Daemon Thread(守护线程)本质没有区别,唯一不同之处在于虚拟机的退出:
- 当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程,Java虚拟机将会退出(守护线程也就没有工作可做了,也就没有继续运行程序的必要了)。
- 反过来说,只要任何非守护线程还在运行,程序就不会终止。
- 可以通过调用Thread.setDaemon(true)将线程设置为守护线程。
使用注意事项
- thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个IllegalThreadStateException异常。这也就意味着不能把正在运行的常规线程设置为守护线程。 这点与操作系统中的守护进程有着明显的区别,守护进程是创建后,让进程摆脱原会话的控制+让进程摆脱原进程组的控制+让进程摆脱原控制终端的控制;所以说寄托于虚拟机的语言机制跟系统级语言有着本质上面的区别。
- 在Daemon线程中产生的新线程也是Daemon的。关于这一点又是与操作系统中的守护进程有着本质的区别:守护进程fork()出来的子进程不再是守护进程,尽管它把父进程的进程相关信息复制过去了,但是子进程的进程的父进程不是init进程,所谓的守护进程本质上说就是,当父进程挂掉,init就会收养该进程,然后文件0、1和2都是/dev/null,当前目录到/。
- 不是所有的应用都可以分配给Daemon线程来进行服务的,比如读写操作或者计算逻辑。因为这种应用可能在Daemon Thread还没来得及进行操作时,虚拟机已经退出了。这也就意味着,守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
示例1: 一个完成文件输出的守护线程任务
1 | class TestRunnable implements Runnable { |
上面这段代码的运行结果是文件daemon.txt中没有daemon字符串。
但是如果把thread.setDaemon(true);这行代码注释掉,文件daemon.txt是可以被写入daemon字符串的,因为这个时候这个线程就是普通的用户线程了。
简单理解就是,JRE判断程序是否执行结束的标准是所有的前台线程(用户线程)执行完毕了,而不管后台线程(守护线程)的状态。
示例2:Daemon线程被用作完成支持性工作,在Java虚拟机退出时Daemon线程中的finally块并不一定会执行
1 | public class Daemon { |
运行Daemon程序,可以看到在终端或者命令提示符上没有任何输出。main线程(非Daemon线程)在启动了线程DaemonRunner之后随着main方法执行完毕而终止,而此时Java虚拟机中已经没有非Daemon线程,虚拟机需要退出。Java虚拟机中的所有Daemon线程都需要立即终止,因此DaemonRunner立即终止,但是DaemonRunner中的finally块并没有执行。
注意:在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。
应用场景
Web服务器中的Servlet,在容器启动时,后台都会初始化一个服务线程,即调度线程,负责处理http请求,然后每个请求过来,调度线程就会从线程池中取出一个工作者线程来处理该请求,从而实现并发控制的目的。也就是说,一个实际应用在Java的线程池中的调度线程。
启动和终止
构造线程
如下摘自java.lang.Thread中对线程进行初始化的部分:
1 | private void init(ThreadGroup g, Runnable target, String name, |
上述过程中,一个新构造的线程对象是由其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,前者不停地睡眠,后者一直运行,然后对这两个线程分别进行中断操作,观察二者的中断标识位。
1 | public class Interrupted { |
1 | SleepThread interrupted is false |
从结果可以看出,抛出InterruptedException的线程SleepThread,其中断标识位被清除了,而一直忙碌运作的线程BusyThread,中断标识位没有被清除。
线程中断这里分两种情况:
- 线程在sleep或wait、join,此时如果别的进程调用此进程的interrupt()方法,此线程会被唤醒并被要求处理InterruptedException;(thread在做IO操作时也可能有类似行为,见java thread api)
- 线程在运行中,则不会收到提醒。但是 此线程的中断标识位会被设置,可以通过isInterrupted()查看并作出处理。
过期方法
过期的suspend()、resume()和stop()
如果把它播放音乐比作一个线程的运作,那么对音乐播放做出的暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。
如下例子以1秒的频率进行打印,而主线程对其进行暂停、恢复和停止操作。
1 | public class Deprecated { |
1 | PrintThread Run at 19:26:14 |
可以看到,suspend()、resume()和stop()方法完成了线程的暂停、恢复和终止工作,而且非常“人性化”。但是这些API是过期的,也就是不建议使用的。
不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。
注意:正因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法,而暂停和恢复操作可以用后面提到的等待/通知机制来替代。
终止线程
上述提到的中断状态是线程的一个标识位,而中断操作是一种简便的线程间交互方式,而这种交互方式最适合用来取消或停止任务。除了中断以外,还可以利用一个boolean变量来控制是否需要停止任务并终止该线程。
如下创建了一个线程CountThread,它不断地进行变量累加,而主线程尝试对其进行中断操作和停止操作。
1 | public class Shutdown { |
1 | Count i = 737662019 |
示例在执行过程中,main线程通过中断操作和cancel()方法均可使CountThread得以终止。这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅。