Java线程状态
Java线程共有6个状态,在java.lang.Thread.State中明确定义。
线程状态图:
1.NEW 尚未启动的线程的状态
2.Runnable 可运行的线程的状态,包括等待CPU调度和正在运行的线程的状态
3.Blocked 等待监视器锁的阻塞线程的状态,处于synchronized同步代码块或方法中被阻塞
4.Waiting 等待线程的状态。通过下列不带超时的方式:Object.wait、Thread.join、LockSupport.park
处于等待状态的线程正在等待另一个线程执行特定的动作,如一个线程调用了Object.wait(),此时会等待另一个线程调用同一个对象的Object.notify()或Object.notifyAll()
5.Timed Waiting 指定等待时间的等待线程的状态,通过下列带超时的方式:
Thread.sleep、Object.wait、Thread.join、LockSupport.parkNanos、LockSupport.parkUntil
6.Terminated 终止线程的状态。线程正常执行完成或出现异常而终止
线程间通信
要想实现多个线程之间的协同,如:线程执行先后顺序、获取某个线程执行的结果,终止某个线程等,涉及到线程之间的相互通讯,分为下面四类:
- 文件共享
- 网络共享
- 共享变量
- jdk提供的线程协调API
这里主要讲通过JDK提供的API实现线程间的协作,JDK中对于需要多线程协作完成某一任务的场景,提供了对应的API支持。多线程协作的经典场景是:生产者-消费者模型(线程等待、线程唤醒)。以生产者-消费者模型为例,当消费者线程获取消费对象时,如果获取消费对象失败,则消费者线程会进入等待状态,此时需要生产者线程通知唤醒消费者线程,每当生产者线程生产消费对象时,会通知唤醒消费者线程。
1.suspend和resume
调用Thread.suspend可以挂起目标线程,通过Thread.resume可以恢复线程的执行。由于suspend挂起之后并不会释放锁,而且必须先suspend再resume才能唤醒,使用suspend容易出现死锁代码,因此suspend和resume已被弃用。
suspend死锁的两个示例
public static Object baozidian = null;
/** 死锁的suspend/resume。 suspend并不会像wait一样释放锁,故此容易写出死锁代码 */
public void suspendResumeDeadLockTest1() throws Exception {
// 启动线程
Thread consumerThread = new Thread(() -> {
if (baozidian == null) { // 如果没包子,则进入等待
System.out.println("1、进入等待");
// 当前线程拿到锁,然后挂起
synchronized (this) {
Thread.currentThread().suspend();
}
}
System.out.println("2、买到包子,回家");
});
consumerThread.start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
baozidian = new Object();
// 争取到锁以后,再恢复consumerThread
synchronized (this) {
consumerThread.resume();
}
System.out.println("3、通知消费者");
}
/** 由于先后顺序导致程序永久挂起的suspend/resume */
public void suspendResumeDeadLockTest2() throws Exception {
// 启动线程
Thread consumerThread = new Thread(() -> {
if (baozidian == null) {
System.out.println("1、没包子,进入等待");
try { // 为这个线程加上一点延时
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 这里的挂起执行在resume后面
Thread.currentThread().suspend();
}
System.out.println("2、买到包子,回家");
});
consumerThread.start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
baozidian = new Object();
consumerThread.resume();
System.out.println("3、通知消费者");
consumerThread.join();
}
2.wait和notify
只能由同一对象锁的持有者线程调用Object.wait,也就是在该对象的synchronized同步代码块里,否则会抛出IllegalMonitorStateException。wait方法导致当前线程等待,加入到该对象的等待集合中,并且放弃当前持有的对象锁。notify/notifyAll方法唤醒一个/所有该对象等待集合中的的线程。
虽然wait会自动解锁,但是对顺序有要求,如果在notify被调用之后,才调用wait方法,也会出现与suspend和resume一样由于先后顺序导致的线程永远处于等待状态。
3.park和unpark
park和unpark可以理解为”许可“机制,线程调用LockSupport.park则会进入等待状态,等待“许可”,unpark方法唤醒线程,为特定线程提供“许可”。park和unpark方法不要求调用顺序,如果先调用unpark,再调用park,线程会直接被”许可“继续运行。但是unpark不会叠加,也就是说,连续多次调用unpark后第一次park会被“许可”直接运行,后续的park会进入等待。park/unpark虽然解决了顺序问题,但是如果线程持有锁,不会自动释放锁。
伪唤醒
伪唤醒是指线程并非因为notify、notifyAll、unpark等方法调用而被唤醒,是由于更底层的其他原因导致的。
官方建议应该在循环中检查等待条件,而不是简单的用if判断,原因是处于等待状态的线程可能会收到错误报警和伪唤醒,如果不在循环中检查等待条件,程序就可能会在没有满足条件的情况下继续运行。
4.stop、destory和interrupt
用于线程终止
线程终止有多种方式:
- Thread.stop 强制终止线程,并且清除监视器锁的信息,但是可能导致线程安全问题,JDK不建议用
- Thread.destory JDK为实现该方法
- Thread.interrupt 现在常用的方法,如果目标线程处于Waiting或Timed Waiting等待状态时,interrupt会生效,并且中断状态将被清除,并抛出InterruptedException。如果目标线程被I/O或NIO中的Channel阻塞,同样,I/O操作会被中断或者返回特殊异常值,达到终止线程的目的。如果以上条件都不满足,则会设置此线程的中断状态
- 标志位 代码逻辑中如果是循环执行,可以增加一个共享变量标志位,用于判断程序是否继续执行