JUC
线程等待唤醒机制

3 种让线程等待和唤醒的方法

  • 使用 Object 中的 wait() 方法让线程等待,使用 Object 中的 notify() 方法唤醒线程
  • 使用 JUC 包中 Conditionawait() 方法让线程等待,使用 signal() 方法唤醒线程
  • LockSupport 类可以阻塞当前线程以及唤醒指定被阻塞的线程

Object 类中的 wait 和 notify 方法实现线程等待和唤醒

示例代码

正常情况

LockSupportDemo.java
public class LockSupportDemo {
    public static void main(String[] args) {
        Object objectLock = new Object();
        new Thread(() -> {
            synchronized (objectLock) {
                System.out.println(Thread.currentThread().getName() + "\t come in");
                try { objectLock.wait(); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println(Thread.currentThread().getName() + "\t 被唤醒");
            }
        }, "t1").start();
 
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
 
        new Thread(() -> {
            synchronized (objectLock) {
                objectLock.notify();
                System.out.println(Thread.currentThread().getName() + "\t 发出通知");
            }
        }, "t2").start();
    }
}

错误情况1

wait() 方法和 notify() 方法,两个都去掉同步代码块

LockSupportDemo.java
public class LockSupportDemo {
    public static void main(String[] args) {
        Object objectLock = new Object();
        new Thread(() -> {
//            synchronized (objectLock) {
                System.out.println(Thread.currentThread().getName() + "\t come in");
                try { objectLock.wait(); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println(Thread.currentThread().getName() + "\t 被唤醒");
//            }
        }, "t1").start();
 
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
 
        new Thread(() -> {
//            synchronized (objectLock) {
                objectLock.notify();
                System.out.println(Thread.currentThread().getName() + "\t 发出通知");
//            }
        }, "t2").start();
    }
}

控制台报错

Terminal
t1	 come in
Exception in thread "t1" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at dev.matrixlab.juc.locks.LockSupportDemo.lambda$main$0(LockSupportDemo.java:11)
	at java.lang.Thread.run(Thread.java:750)
Exception in thread "t2" java.lang.IllegalMonitorStateException
	at java.lang.Object.notify(Native Method)
	at dev.matrixlab.juc.locks.LockSupportDemo.lambda$main$1(LockSupportDemo.java:20)
	at java.lang.Thread.run(Thread.java:750)

错误情况2

notify() 放在 wait() 方法前面,程序无法执行,无法唤醒。

LockSupportDemo.java
public class LockSupportDemo {
    public static void main(String[] args) {
        Object objectLock = new Object();
        new Thread(() -> {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            synchronized (objectLock) {
                System.out.println(Thread.currentThread().getName() + "\t come in");
                try { objectLock.wait(); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println(Thread.currentThread().getName() + "\t 被唤醒");
            }
        }, "t1").start();
 
 
        new Thread(() -> {
            synchronized (objectLock) {
                objectLock.notify();
                System.out.println(Thread.currentThread().getName() + "\t 发出通知");
            }
        }, "t2").start();
    }
}

控制台报错

Terminal
t2	 发出通知
t1	 come in

总结

  • wait()notify() 方法必须要在同步块或者方法里面,且成对出现使用
  • wait()notify()

Condition 接口中先 await 后 signal 方法实现线程的等待和唤醒

示例代码

正常情况

LockSupportDemo.java
public class LockSupportDemo {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
 
        new Thread(() -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t come in");
                condition.await();
                System.out.println(Thread.currentThread().getName() + "\t 被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "t1").start();
 
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
 
        new Thread(() -> {
            lock.lock();
            try {
                condition.signal();
                System.out.println(Thread.currentThread().getName() + "\t 发出通知");
            } finally {
                lock.unlock();
            }
        }, "t2").start();
    }
}

错误情况1

去掉 lock()unlock()

LockSupportDemo.java
public class LockSupportDemo {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
 
        new Thread(() -> {
//            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t come in");
                condition.await();
                System.out.println(Thread.currentThread().getName() + "\t 被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
//                lock.unlock();
            }
        }, "t1").start();
 
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
 
        new Thread(() -> {
//            lock.lock();
            try {
                condition.signal();
                System.out.println(Thread.currentThread().getName() + "\t 发出通知");
            } finally {
//                lock.unlock();
            }
        }, "t2").start();
    }
}

condition.await()condition.signal() 都触发了 IllegalMonitorStateException 异常

错误情况2

LockSupportDemo.java
public class LockSupportDemo {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
 
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t come in");
                condition.await();
                System.out.println(Thread.currentThread().getName() + "\t 被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "t1").start();
 
//        try {
//            TimeUnit.SECONDS.sleep(1);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
 
        new Thread(() -> {
            lock.lock();
            try {
                condition.signal();
                System.out.println(Thread.currentThread().getName() + "\t 发出通知");
            } finally {
                lock.unlock();
            }
        }, "t2").start();
    }
}

signal()await(),程序无法正确执行

总结

  • lock()unlock() 对里面,才能正确调用 condition 中线程等待和唤醒的方法。
  • 一定要先 await()signal()

Object 和 Condition 使用的限制条件

  • 线程先要获得并持有锁,必须在锁块(synchronizedlock)中
  • 必须要先等待后唤醒,线程才能够被唤醒

LockSupport 类中的 park 等待和 unpark 唤醒

概述

通过 park()unpark(thread) 方法来实现阻塞和唤醒线程的操作

Basic thread blocking primitives for creating locks and other synchronization classes. This class associates, with each thread that uses it, a permit (in the sense of the Semaphore class). A call to park will return immediately if the permit is available, consuming it in the process; otherwise it may block. A call to unpark makes the permit available, if it was not already available. (Unlike with Semaphores though, permits do not accumulate. There is at most one.)

LockSupport 是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport 类使用了一种名为 Permit(许可)的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个许可(permit),permit 只有两个值 10,默认是零。可以把许可看成是一种 (0,1) 信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是 1

主要方法

API

static void park()
static void park(Object blocker)
static void unpark(Thread thread)

阻塞

park() / park(Object blocker) 阻塞当前线程/阻塞传入的具体线程

LockSupport.java
public static void park() {
    UNSAFE.park(false, 0L);
}
Unsafe.java
/**
 * Block current thread, returning when a balancing
 * <tt>unpark</tt> occurs, or a balancing <tt>unpark</tt> has
 * already occurred, or the thread is interrupted, or, if not
 * absolute and time is not zero, the given time nanoseconds have
 * elapsed, or if absolute, the given deadline in milliseconds
 * since Epoch has passed, or spuriously (i.e., returning for no
 * "reason"). Note: This operation is in the Unsafe class only
 * because <tt>unpark</tt> is, so it would be strange to place it
 * elsewhere.
 */
public native void park(boolean isAbsolute, long time);

permit 默认是零,所以一开始调用 park() 方法,当前线程就会阻塞,直到别的线程将当前线程的 permit 设置为 1 时,park() 方法会被唤醒,然后会将 permit 再次设置为零并返回。

唤醒

unpark(Thread thread) 唤醒处于阻塞状态的制定线程

LockSupport.java
public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}
Unsafe.java
/**
 * Unblock the given thread blocked on <tt>park</tt>, or, if it is
 * not blocked, cause the subsequent call to <tt>park</tt> not to
 * block.  Note: this operation is "unsafe" solely because the
 * caller must somehow ensure that the thread has not been
 * destroyed. Nothing special is usually required to ensure this
 * when called from Java (in which there will ordinarily be a live
 * reference to the thread) but this is not nearly-automatically
 * so when calling from native code.
 * @param thread the thread to unpark.
 *
 */
public native void unpark(Object thread);

调用 unpark(thread) 方法后,就会将 thread 线程的许可 permit 设置成 1 (注意多次调用 unpark 方法,不会累加,permit 值还是 1)会自动唤醒 thread 线程,即之前阻塞中的 LockSupport.park() 方法会立即返回。

代码

无锁块要求

LockSupportDemo
public class LockSupportDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t 开始");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + "\t 被唤醒");
        }, "t1");
        t1.start();
 
      	try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
 
        new Thread(() -> {
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName() + "\t 发出通知");
        }, "t2").start();
    }
}

先唤醒后等待

LockSupportDemo
public class LockSupportDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName() + "\t 开始\t" + System.currentTimeMillis());
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + "\t 被唤醒\t" + System.currentTimeMillis());
        }, "t1");
        t1.start();
      
      	try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        
        new Thread(() -> {
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName() + "\t 发出通知");
        }, "t2").start();
    }
}

解释:可以发现第5行和第7行的时间戳是一样的。线程 t1 的时间等待 3 秒后醒来,执行 park() 无效,没有阻塞效果,先执行了 unpark(t1) 导致上面的 park() 方法形同虚设无效,时间一样。

重点说明⭐️

LockSupport 是用来创建锁和其他同步类的基本线程阻塞原语。

LockSupport 是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底 LockSupport 调用的 Unsafe 中的 native 方法。

LockSupport 提供 park()unpack() 方法实现阻塞线程和解除阻塞线程的功能。
  • LockSupport 和每个使用它的线程都有一个许可(permit)关联;
  • 每个线程都有一个相关的 permit,permit 最多只有一个,重复调用 unpark 也不会积累凭证。

形象理解

线程阻塞需要消耗凭证(permit),这个凭证最多只有 1 个。

  • 当调用 park() 方法时

    • 如果有凭证,则会直接消耗这个凭证然后正常退出;
    • 如果没有凭证,就必须阻塞等待凭证可用。
  • unpack() 则相反,它会增加一个凭证,但凭证最多只能有 1 个,累加无效。

面试题

  • 为什么可以突破 wait/notify 的原有调用顺序?
    因为 unpack() 获得了一个凭证,之后再调用 park() 方法,就可以名正言顺的凭证消费,故不会阻塞。

  • 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
    因为凭证的数量最多为 1,连续调用两次 unpark() 和调用一次 unpark() 效果一样,只会增加一个凭证;而调用两次 park() 却需要消费两个凭证,凭证不够,则不能方形。