参考: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中线程的BLOCKEDWAITING两个状态的区别

他俩都是线程被阻塞无法运行(让出了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中

再来说明下三个概念实际是怎么使用的:

  1. 如下简单代码, 实际线程执行时发生了如下事情:

    Object o = new Object();
    synchronized(o) {
        // do something
    }
    
  • 线程执行synchronized(o)会导致:

    • 线程尝试去抢占object monitor;
    • 如果抢占到, 则该线程拥有了该object monitor, 进行临界区代码执行.
    • 如果抢占不到, 则该线程被放入该对象的blocked set中. 具体
      1. 什么时候唤醒: 可以简化认为为JVM会在底层时刻轮询object monitor占用情况, 一旦object monitor被释放, 立刻从blocked set中找个线程开始执行.
      2. 如果多个线程都在block set中, 该唤醒哪个, 由JVM来决定(下文解决).
  • 线程退出synchronized(o)临界区会导致:

    • 当前线程释放掉object monitor
    • JVM轮询到object monitor处于空闲, 立刻从blocked set中取出一个线程, 让该线程开始临界区代码执行.

JVM始终是在blocked set中取线程换锁

  1. 再加上简单的wait/notify:
    如下, 一个最简单的producer/consumer程序:
// consumer
Object o = new Object();
synchronized(o) {
    o.wait();
    // consume
}
// producer
synchronized(o) {
    // produce
    o.notify();
}

先假设consumer先执行

  • consumer执行链路:

    1. synchronized(o): 获取到object monitor, 开始临界区代码执行o.wait().

      ​ 虽然可以拆解为如下几步, 但wait本身是原子操作

      1. 把consumer线程放入到该对象的 wait set
      2. 释放掉object monitor
      3. JVM将producer从block set中取出, (触发JVM/OS的轮询, 引发producer获取到object monitor从而进入临界区)
  • producer执行链路:

    1. synchronized(o): 获取不到object monitor, 被放入block set

    2. consumer退出临界区, producer被自动从block set中取出, 获取到object monitor从而进入临界区 (与consumer执行链路的2.3步骤重叠)

    3. o.notify():

      • producer并不会因为notify()而释放掉object monitor: notify并不会导致当前线程释放掉object monitor!, 而是继续往下执行代码.

      • JVM将consumer从wait set中取出, 并放入到了block set

    4. 退出临界区:

      • producer释放掉object monitor

      • JVM将consumer从block set中取出

      • 触发JVM内部的轮询, 引发consumer获取到object monitor, 从而继续o.wait()之后的代码片段执行

  • consumer继续执行, 注意:

    1. 此时consumer是继续从object.wait()之后的代码开始执行. (即之前中断的地方继续).
    2. 而不是重新通过synchronized(o)object monitor, 然后从头开始执行临界区代码. 因为consumer目前是调用wait()阻塞的.
    3. 退出临界区:
      • consumer释放掉object monitor
      • JVM尝试从block set中取出线程, 由于block set为空, nothing happens
  • 终态: producer执行完成, consumer也执行完成.

再假设producer先执行

  • producer执行链路:
    1. synchronized(o): producer获取到object monitor, 开始临界区代码执行.
  • consumer执行链路:
    1. synchronized(o): consumer获取不到object monitor, 被放入block set
  • producer执行链路:
    1. o.notify()
      1. producer不会因为notify()而释放掉object monitor
      2. JVM从wait set中寻找一个线程, 移出wait set, 并放入到blocked set中. 由于此时wait set为空(consumer本来就在block set中). 因此nothing happens
    2. 退出临界区:
      1. producer释放掉object monitor
      2. JVM将consumer从block set中取出, 由于object monitor已经被producer释放, 因此consumer直接获取到object monitor, 开始执行临界区代码 (consumer状态 block set -> RUNNABLE)
  • consumer执行链路:
    1. o.wait()
      1. 把consumer线程放入到该对象的 wait set
      2. 释放掉object monitor
      3. JVM尝试从block set中取出一个线程, 由于此时block set为空, 因此nothing happens
  • 终态: producer执行完成, consumer一直卡在WAITING状态.

因此会有并发问题

小总结

调用 synchronized(object) 时会发生:

  1. 当前线程尝试抢占 object monitor
  2. 如果抢占到, 则进入临界区.
  3. 如果抢占不到, 把当前线程放入到blocked set中. JVM会监控object monitor, 当object monitor归还时, 从blocked set中挑选一个线程继续代码执行(可能是进入临界区, 也可能是继续之前中断的代码)

出 synchronized(object) 时会发生:

  1. 释放掉object monitor;
  2. JVM会监控object monitor, 当它归还时, 从blocked set中挑选一个线程继续代码执行(可能是进入临界区, 也可能是继续之前中断的代码)

调用 object.wait() 时会发生:

  1. 把当前线程放入到 wait set
  2. 释放掉object monitor
  3. JVM会监控object monitor, 当它归还时, 从blocked set中挑选一个线程继续代码执行(可能是进入临界区, 也可能是继续之前中断的代码)

调用 object.notify() 时会发生:

  1. 不会因为notify()而释放掉object monitor, 而是继续往下执行代码.
  2. JVM从wait set中寻找一个线程, 移出wait set, 并放入到blocked set中.(JVM会持续监控object monitor状态)

总结线程可能的状态变化

  • RUNNABLE -> block set: 没进去synchronized
  • RUNNABLE -> wait set: 进去synchronized里wait
  • wait set -> block set: wait之后被其他线程调用的notify唤醒
  • block set -> wait set: 不存在该链路, 可能当前线程在block set, 但被赋予object monitor之后, 肯定进入了RUNNABLE状态. 可能RUNNABLE之后主动调用了wait, 但也不是直接从blocked setwait set
  • block 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()之后:
    1. 把当前线程移到wait set里
    2. 释放掉object monitor
    3. 线程暂停执行, 让出CPU时间片
  • Thread.sleep()之后:
    1. 线程暂停执行, 让出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

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐