本文将介绍Java线程的状态、线程的中断、线程间通信和线程的实现。
线程的状态
Java语言定义了6种不同的线程状态,在给定的一个时刻,线程只能处于其中的一个状态。
线程状态的转换关系,如下图所示:
线程的中断
当希望终止一个线程时,并不是简单的调用 stop 命令。虽然 api 仍然可以调用,但是和其他的线程控制方法如 suspend、resume 一样都是过期了的不建议使用的方法。就拿 stop 来说,stop 方法在结束一个线程时并不会保证线程的资源正常释放,因此会导致程序可能出现一些不确定的状态。要优雅的终止一个线程,可以使用中断的方式。
中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt()方法对其进行中断操作。
线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。
另外许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法)这些方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false。
public void Thread.interrupt() //中断线程
public boolean Thread.isInterrupted() //判断是否被中断
public static boolean Thread.interrupted() //判断是否被中断,并清除当前中断状态
通过下面这个例子,可实现线程终止的逻辑
//通过中断的方式
public class InterruptDemo {
private static int i;
public static void main(String[] args) throws
InterruptedException {
Thread thread=new Thread(()->{
while(!Thread.currentThread().isInterrupted()){
i++;
}
System.out.println("Num:"+i);
},"interruptDemo");
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();
}}
//通过标记符的方式
public class VolatileDemo {
private volatile static boolean stop=false;
public static void main(String[] args) throws
InterruptedException {
Thread thread=new Thread(()->{
int i=0;
while(!stop){
i++;
}
});
thread.start();
System.out.println("begin start thread");
Thread.sleep(1000);
stop=true;
}
}
这种通过标识符或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅。
线程的通信
为了支持多线程之间的协作,JDK提供了两个非常重要的方法wait()和notify()。这两个方法并不是在Thread类中,而是Object类。这也意味着任何对象都可以调用这两个方法。
这两个方法的签名如下,都是本地方法:
public final native void wait(long timeout) throws InterruptedException;
public final native void notify();
调用wait()方法,首先会获取监视器锁,获得成功以后,会让当前线程进入等待状态进入等待队列并且释放锁;然后当其他线程调用notify()或者notifyAll()以后,会选择从等待队列中唤醒任意一个线程,而执行完notify()方法以后,并不会立马唤醒线程,原因是当前的线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要等到当前的线程执行完按monitorexit指令以后,也就是锁被释放以后,处于等待队列中的线程就可以开始竞争锁。竞争是不公平的,并不是先等待的线程就会被优先选择。
public class ThreadWait extends Thread{
private Object lock;
public ThreadWait(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock){
System.out.println("开始执行 thread wait");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行结束 thread wait");
}
}
}
public class ThreadNotify extends Thread{
private Object lock;
public ThreadNotify(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock){
System.out.println("开始执行 thread notify");
lock.notify();
System.out.println("执行结束 thread notify");
}
}
}
注:因为在执行wait()和notify()时都必须获取到锁,所以wait和notify都需要在synchronized里面。wait()与sleep()的区别,sleep()不会释放释放锁。
join():在很多情况下,一个线程的输入可能非常依赖于另外一个或者多个线程的输出,此时,这个线程就需要等待依赖线程执行完毕,才能继续执行。JDK提供了join()操作来实现这个功能。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(long millis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回。例如下面的demo,创建了10个线程,编号0~9,每个线程调用前一个线程的join()方法。
public class Join {
public static void main(String[] args) throws Exception {
Thread previous = Thread.currentThread();
for (int i = 0; i < 10; i++) {
// 每个线程拥有前一个线程的引用,需要等待前一个线程终止,才能从等待中返回
Thread thread = new Thread(new Domino(previous), String.valueOf(i));
thread.start();
previous = thread;
}
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + " terminate.");
}
static class Domino implements Runnable {
private Thread thread;
public Domino(Thread thread) {
this.thread = thread;
}
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + " terminate.");
}
}
}
//输出如下:
main terminate.
0 terminate.
1 terminate.
2 terminate.
3 terminate.
4 terminate.
5 terminate.
6 terminate.
7 terminate.
8 terminate.
9 terminate.
yield():表示当前线程让出CPU,当前线程让出CPU后,还会进行CPU资源的争夺。
线程的实现
我们注意到Thread类和大部分的Java API有显著的差别,它的所有关键方法都是声明为Native的。一个Native方法往往意味着这个方法没有使用或无法使用平台无关的手段来实现(当然也可能是为了执行效率而使用Native方法,不过,通常最高效率的手段也就是平台相关的手段)。
实现线程主要有3种方式:使用内核线程实现、使用用户线程实现和使用用户线程加轻量级进程混合实现。
使用内核线程(Kernel-Level Thread,KLT)实现:就是直接由操作系统内核支持的线程,由内核完成线程切换,内核通过操纵调度器进行线程调度,并且负责将线程的任务映射到各个处理器上。程序一般不会直接使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process ,LWP),轻量级进程就是我们通常意义上讲的线程。轻量级进程和内核线程之间1:1的关系称为一对一线程模型。
局限性:
基于内核实现,各种线程操作需要进行系统调用,而系统调用的代价相对较高,需要在用户态(User Model)和内核态(Kernel Model)中来回切换。
每个轻量级进程都需要一个内核线程支持,轻量级进程要消耗一定的内核资源,因此一个系统支持轻量级进程的数量是有限的。
使用用户线程实现:狭义的用户线程指的是完全建立在用户空间的线程库,系统内核不能感知线程存在。进程与线程之间的1:N的关系称为一对多的线程模型。优势在于不需要内核支援,劣势也在于没有内核支援,所以的线程操作都需要用户程序自己处理。目前使用用户线程的程序越来越少。
使用用户线程加轻量级进程混合实现:用户线程完全建立在用户空间,用户线程的创建、切换、析构的操作比较廉价,并且支持大规模的用户线程并发。操作系统提供支持的轻量级进程则作为用户线程和内核线程的桥梁使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞的风险。用户线程与轻量级进程的数量比是不定的,即为N:M的关系。
线程的调度
线程调度是指系统为线程分配处理器使用权的过程,主要包括两种方式。
协同式线程调度:执行时间由线程本身决定,执行完后,要主动通知系统切换到另一个线程。好处是实现简单;切换操作对线程自己可知,所以没有什么同步问题;坏处是执行时间不可控,可能会出现程序一直阻塞的情况。
抢占式线程调度:线程由系统分配执行时间,切换不由线程本身决定。执行时间系统可控,不会有一个线程导致整个进程阻塞的情况。Java使用的线程调度方式是抢占式线程调度