天天看点

Java线程状态与线程通讯wait/notify,park/unpark机制

Java线程状态

Java线程共有6个状态,在java.lang.Thread.State中明确定义。

线程状态图:

Java线程状态与线程通讯wait/notify,park/unpark机制

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  终止线程的状态。线程正常执行完成或出现异常而终止

线程间通信

要想实现多个线程之间的协同,如:线程执行先后顺序、获取某个线程执行的结果,终止某个线程等,涉及到线程之间的相互通讯,分为下面四类:

  1. 文件共享
  2. 网络共享
  3. 共享变量
  4. 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操作会被中断或者返回特殊异常值,达到终止线程的目的。如果以上条件都不满足,则会设置此线程的中断状态
  • 标志位  代码逻辑中如果是循环执行,可以增加一个共享变量标志位,用于判断程序是否继续执行

继续阅读