Java中BLOCK态和有WAITING态有啥区别?
当前线程尝试抢占如果抢占到, 则进入临界区.如果抢占不到, 把当前线程放入到中. JVM会监控, 当归还时, 从中挑选一个线程继续代码执行(可能是进入临界区, 也可能是继续之前中断的代码)释放掉;JVM会监控, 当它归还时, 从中挑选一个线程继续代码执行(可能是进入临界区, 也可能是继续之前中断的代码)把当前线程放入到wait set中释放掉JVM会监控, 当它归还时, 从中挑选一个线程继续代码执
参考:Java线程BLOCKED与WAITING状态深入研究
Java中BLOCK态和有WAITING态有啥区别?
背景:Java线程状态
public enum State {
/**
* 线程被创建,未执行和运行的时候
*/
NEW,
/**
* 可运行线程的线程状态。对应OS中的执行态和就绪态
*/
RUNNABLE,
/**
* 等待某个监视器锁时的阻塞状态。
* 为了进入同步块的线程会等待监视器锁而进入 BLOCKED 态
*/
BLOCKED,
/**
* 处于等待状态的线程正在等待另一个线程执行特定操作。
* 由于调用以下方法之一,线程处于等待状态:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
* 例如,对某个对象调用<tt>Object.wait()</tt>的线程正在等待另一个线程对该对象调用<tt>Object.notify()</tt>或<tt>Object.notifyAll()</tt>。
*/
WAITING,
/**
* 具有指定等待时间的等待线程的线程状态。
* 由于使用指定的正等待时间调用以下方法之一,线程处于定时等待状态:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* 终止线程的线程状态。线程已完成执行。
*/
TERMINATED;
}
Java中线程的BLOCKED和WAITING两个状态的区别
他俩都是线程被阻塞无法运行(让出了CPU的时间片)的状态
这两个状态具体有什么区别?
三个重要概念
- object monitor: 每个java对象都有一个object monitor
- blocked set: 每个java对象都有一个blocked set
- wait set: 每个java对象都有一个wait set
写在前面
线程调用了o.wait() 后,会放到wait set中
下一个抢锁的线程必须处于block set中,Wait Set 里的线程完全不参与锁竞争
线程不会因为调用了o.notify() 就释放掉object monitor锁,只会唤醒wait set中的某个线程起来,放入block set中(换个地方),想要执行必须等当前线程退出临界区
- 对应两个set,blocked态的线程位于blocked set中,waiting态的线程位于wait set中
再来说明下三个概念实际是怎么使用的:
-
如下简单代码, 实际线程执行时发生了如下事情:
Object o = new Object(); synchronized(o) { // do something }
-
线程执行
synchronized(o)会导致:- 线程尝试去抢占
object monitor; - 如果抢占到, 则该线程拥有了该
object monitor, 进行临界区代码执行. - 如果抢占不到, 则该线程被放入该对象的
blocked set中. 具体- 什么时候唤醒: 可以简化认为为JVM会在底层时刻轮询
object monitor占用情况, 一旦object monitor被释放, 立刻从blocked set中找个线程开始执行. - 如果多个线程都在
block set中, 该唤醒哪个, 由JVM来决定(下文解决).
- 什么时候唤醒: 可以简化认为为JVM会在底层时刻轮询
- 线程尝试去抢占
-
线程退出
synchronized(o)临界区会导致:- 当前线程释放掉
object monitor - JVM轮询到
object monitor处于空闲, 立刻从blocked set中取出一个线程, 让该线程开始临界区代码执行.
- 当前线程释放掉
JVM始终是在blocked set中取线程换锁
- 再加上简单的wait/notify:
如下, 一个最简单的producer/consumer程序:
// consumer
Object o = new Object();
synchronized(o) {
o.wait();
// consume
}
// producer
synchronized(o) {
// produce
o.notify();
}
先假设consumer先执行
-
consumer执行链路:
-
synchronized(o): 获取到object monitor, 开始临界区代码执行o.wait(). 虽然可以拆解为如下几步, 但wait本身是原子操作
- 把consumer线程放入到该对象的
wait set中 - 释放掉
object monitor - JVM将producer从
block set中取出, (触发JVM/OS的轮询, 引发producer获取到object monitor从而进入临界区)
- 把consumer线程放入到该对象的
-
-
producer执行链路:
-
synchronized(o): 获取不到object monitor, 被放入block set中 -
consumer退出临界区, producer被自动从
block set中取出, 获取到object monitor从而进入临界区 (与consumer执行链路的2.3步骤重叠) -
o.notify():-
producer并不会因为
notify()而释放掉object monitor:notify并不会导致当前线程释放掉object monitor!, 而是继续往下执行代码. -
JVM将consumer从
wait set中取出, 并放入到了block set中
-
-
退出临界区:
-
producer释放掉
object monitor -
JVM将consumer从
block set中取出 -
触发JVM内部的轮询, 引发consumer获取到
object monitor, 从而继续o.wait()之后的代码片段执行
-
-
-
consumer继续执行, 注意:
- 此时consumer是继续从
object.wait()之后的代码开始执行. (即之前中断的地方继续). - 而不是重新通过
synchronized(o)抢object monitor, 然后从头开始执行临界区代码. 因为consumer目前是调用wait()阻塞的. - 退出临界区:
- consumer释放掉
object monitor - JVM尝试从
block set中取出线程, 由于block set为空, nothing happens
- consumer释放掉
- 此时consumer是继续从
-
终态: producer执行完成, consumer也执行完成.
再假设producer先执行
- producer执行链路:
synchronized(o): producer获取到object monitor, 开始临界区代码执行.
- consumer执行链路:
synchronized(o): consumer获取不到object monitor, 被放入block set中
- producer执行链路:
o.notify()- producer不会因为
notify()而释放掉object monitor - JVM从
wait set中寻找一个线程, 移出wait set, 并放入到blocked set中. 由于此时wait set为空(consumer本来就在block set中). 因此nothing happens
- producer不会因为
- 退出临界区:
- producer释放掉
object monitor - JVM将consumer从
block set中取出, 由于object monitor已经被producer释放, 因此consumer直接获取到object monitor, 开始执行临界区代码 (consumer状态block set->RUNNABLE)
- producer释放掉
- consumer执行链路:
o.wait()- 把consumer线程放入到该对象的
wait set中 - 释放掉
object monitor - JVM尝试从
block set中取出一个线程, 由于此时block set为空, 因此nothing happens
- 把consumer线程放入到该对象的
- 终态: producer执行完成, consumer一直卡在
WAITING状态.
因此会有并发问题
小总结
调用 synchronized(object) 时会发生:
- 当前线程尝试抢占
object monitor - 如果抢占到, 则进入临界区.
- 如果抢占不到, 把当前线程放入到
blocked set中. JVM会监控object monitor, 当object monitor归还时, 从blocked set中挑选一个线程继续代码执行(可能是进入临界区, 也可能是继续之前中断的代码)
出 synchronized(object) 时会发生:
- 释放掉
object monitor; - JVM会监控
object monitor, 当它归还时, 从blocked set中挑选一个线程继续代码执行(可能是进入临界区, 也可能是继续之前中断的代码)
调用 object.wait() 时会发生:
- 把当前线程放入到
wait set中 - 释放掉
object monitor - JVM会监控
object monitor, 当它归还时, 从blocked set中挑选一个线程继续代码执行(可能是进入临界区, 也可能是继续之前中断的代码)
调用 object.notify() 时会发生:
- 不会因为
notify()而释放掉object monitor, 而是继续往下执行代码. - JVM从
wait set中寻找一个线程, 移出wait set, 并放入到blocked set中.(JVM会持续监控object monitor状态)
总结线程可能的状态变化
RUNNABLE->block set: 没进去synchronizedRUNNABLE->wait set: 进去synchronized里waitwait set->block set: wait之后被其他线程调用的notify唤醒block set->wait set: 不存在该链路, 可能当前线程在block set, 但被赋予object monitor之后, 肯定进入了RUNNABLE状态. 可能RUNNABLE之后主动调用了wait, 但也不是直接从blocked set到wait setblock set->RUNNABLE: 其他线程wait之后, 自动释放掉object monitor, 当前线程可以继续执行
生产者-消费者例子
两个线程, 分别扮演消费者&生产者的角色, 假设队列为1, 无限循环. 如何写?
import org.junit.Test;
/**
* 两个线程, 分别扮演消费者&生产者的角色, 假设队列为1, 无限循环.
*/
public class ProducerConsumerTest2 {
static boolean isEmpty = true;
static Object o = new Object();
@Test
public void simple_test() throws InterruptedException {
ConsumerThread t1 = new ConsumerThread("consumer", o);
ProducerThread t2 = new ProducerThread("producer", o);
t1.start();
t2.start();
t1.join();
t2.join();
}
public static class ConsumerThread extends Thread {
Object lock;
public ConsumerThread(String name, Object lock) {
super(name);
this.lock = lock;
}
@Override
public void run() {
try {
while (true) {
synchronized (lock) {
while (isEmpty) {
lock.wait();
}
System.out.println("consuming ...");
Thread.sleep((long) (Math.random() * 1000l));
isEmpty = true;
System.out.println("finish consuming ...");
lock.notify();
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public static class ProducerThread extends Thread {
Object lock;
public ProducerThread(String name, Object lock) {
super(name);
this.lock = lock;
}
@Override
public void run() {
try {
while (true) {
synchronized (lock) {
while (!isEmpty) {
lock.wait();
}
System.out.println("producing ...");
Thread.sleep((long) (Math.random() * 1000l));
isEmpty = false;
System.out.println("finish producing ...");
lock.notify();
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
变体写法-1
如果在调用lock.notify()之后再生产或者再消费, 会怎么样?正常执行
// consumer
while (true) {
synchronized (lock) {
while (isEmpty) {
System.out.println("consumer is waiting");
lock.wait();
}
lock.notify();
System.out.println("start consuming");
Thread.sleep((long) (Math.random() * 10000L));
System.out.println("finished consuming");
isEmpty = true;
}
}
// producer
while (true) {
synchronized (lock) {
while (!isEmpty) {
System.out.println("producer is waiting");
lock.wait();
}
lock.notify();
System.out.println("start producing");
Thread.sleep((long) (Math.random() * 10000L));
isEmpty = false;
System.out.println("finished producing");
}
}
变体写法-2
如下例子中, 基于变体写法-1把synchronized(lock)放在while(true)外层, 会正常执行么?会
// consumer
synchronized (lock) { // 1. 获取object monitor
while (true) {
while (isEmpty) {
System.out.println("consumer is waiting");
lock.wait(); // 3. 把自己放到wait set里, 释放object monitor; 5. JVM把consumer从wait set里移出, 移入到block set里; 8. JVM把consumer从block set里移出, consumer获取object monitor
}
lock.notify(); // 9. 通知JVM把producer从wait set里移出, 移入到block set里; consumer继续往下执行(仍然保有object monitor)
System.out.println("start consuming");
Thread.sleep((long) (Math.random() * 10000L));
System.out.println("finished consuming");
isEmpty = true; // 10. consumer消费完成, 继续执行到第3步, 依次循环.
}
}
// producer
synchronized (lock) { // 2. 把自己放到block set里, 等待获取object monitor; 4. 从block set移出, 获取 object monitor
while (true) {
while (!isEmpty) {
System.out.println("producer is waiting");
lock.wait(); // 7. 把自己放到wait set里, 释放object monitor; 9. JVM把producer从wait set里移出, 移入到block set里; 4.2 producer从block set移出, 获取 object monitor
}
lock.notify(); // 5. 通知JVM把consumer从wait set里移出, 移入到block set里; producer继续往下执行(仍然保有object monitor)
System.out.println("start producing");
Thread.sleep((long) (Math.random() * 10000L));
isEmpty = false; // 6. producer开始生产
System.out.println("finished producing");
}
}
其他几种情况
除了本文, 其实还会有多种情况会导致线程进入BLOCKED, WAITING状态, 如下:
JVM为什么要进行上边两个状态的区分? 为什么不只用一个状态标识
WAITING 让 notify 有“精确唤醒”的语义
补充:Object.wait() 与 Thread.sleep() 的区别
行为上区别:
- Object.wait()之后:
- 把当前线程移到wait set里
- 释放掉object monitor
- 线程暂停执行, 让出CPU时间片
- Thread.sleep()之后:
- 线程暂停执行, 让出CPU时间片; 不会有1, 2的操作.
结果上区别:
- Object.wait()之后: 线程进入 WAITING (on object monitor) 或者 TIMED_WAITING (on object monitor) 状态
- Thread.sleep()之后: 线程进入 TIMED_WAITING (sleeping) 状态
补充:如果多个线程都在block set中, 该唤醒哪个?
当多个线程在 Block Set 中竞争同一个 monitor 时, JVM 不保证唤醒顺序,也不保证公平性, 具体由 JVM 的实现、锁的状态(偏向 / 轻量级 / 重量级)以及底层 OS 调度器共同决定。
不是 Java 代码能决定的
Refs
更多推荐


所有评论(0)