本文是学习尚硅谷周阳老师《JUC并发编程》的总结(文末有链接)。
在 Java 并发编程:实现线程等待和唤醒有 3 种方法:
- Object 类的 wait/notify 方法
- Condition 类的 await/signal 方法
- LockSupport 类的 park/unpark 方法
Object 类的 wait/notify 方法
实现代码:
package juc;import java.util.concurrent.TimeUnit;public class LockSupportDemo {public static void main(String[] args){Object objectLock = new Object();new Thread(() -> {synchronized (objectLock) {System.out.println(Thread.currentThread().getName() + " come in");try {objectLock.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 被唤醒");}}, "t1").start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {synchronized (objectLock) {System.out.println(Thread.currentThread().getName() + " come in");objectLock.notify();System.out.println(Thread.currentThread().getName() + " 发出通知");}}, "t2").start();}
}
注意:
- wait 和 notify 方法必须要在同步块或同步方法里面,且成对出现使用
- 必须先 wait 后 notify
Condition 类的 await/signal 方法
实现代码:
package juc;import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;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() + " come in");condition.await();System.out.println(Thread.currentThread().getName() + " 被唤醒");} 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 {System.out.println(Thread.currentThread().getName() + " come in");condition.signal();System.out.println(Thread.currentThread().getName() + " 发出通知");} finally {lock.unlock();}}, "t2").start();}
}
注意:
- await 和 signal方法必须先获取锁,即在 lock/unlock 对里面才能使用
- 必须先 await 后 signal
LockSupport 类的 park/unpark 方法
上述两种方法在使用时都有如下限制:
- 线程要先获得并持有锁,必须在锁块(synchronized 或 lock)中
- 必须要先等待再唤醒,线程才能够被唤醒
因此才出现了 LockSupport 类,通过此类可以不受上面的限制更方便地实现线程等待和唤醒。
LockSupport 是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法,底层调用的是 UnSafe 类的 native 代码。
LockSupport 提供 park() 和 unpark() 方法提供阻塞线程和唤醒线程的功能。
实现代码:
package juc;import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;public class LockSupportDemo {public static void main(String[] args){Thread t1 = new Thread(() -> {System.out.println(Thread.currentThread().getName() + " come in");LockSupport.park();System.out.println(Thread.currentThread().getName() + " 被唤醒");}, "t1");t1.start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {System.out.println(Thread.currentThread().getName() + " come in");LockSupport.unpark(t1);System.out.println(Thread.currentThread().getName() + " 发出通知");}, "t2").start();}
}
通过 LockSupport 类可以先 unpark 给 t1 线程颁发许可证,等到 t1 运行到 park 时直接放行通过,不会阻塞。
package juc;import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;public class LockSupportDemo {public static void main(String[] args){Thread t1 = new Thread(() -> {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " come in");LockSupport.park();System.out.println(Thread.currentThread().getName() + " 被唤醒");}, "t1");t1.start();new Thread(() -> {System.out.println(Thread.currentThread().getName() + " come in");LockSupport.unpark(t1);System.out.println(Thread.currentThread().getName() + " 发出通知");}, "t2").start();}
}
输出:
t2 come in
t2 发出通知
t1 come in
t1 被唤醒Process finished with exit code 0
LockSupport 和每个使用它的线程都有一个许可(permit)关联,每个线程都有一个相关的 permit ,permit 最多只有 1 个,重复调用 unpark() 也不会累积 permit。
可以通过如下代码验证:
package juc;import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;public class LockSupportDemo {public static void main(String[] args){Thread t1 = new Thread(() -> {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " come in");LockSupport.park();System.out.println(Thread.currentThread().getName() + " 通过第1个park");LockSupport.park();System.out.println(Thread.currentThread().getName() + " 通过第2个park");System.out.println(Thread.currentThread().getName() + " 被唤醒");}, "t1");t1.start();new Thread(() -> {System.out.println(Thread.currentThread().getName() + " come in");LockSupport.unpark(t1);LockSupport.unpark(t1);System.out.println(Thread.currentThread().getName() + " 发出通知");}, "t2").start();}
}
输出:
t2 come in
t2 发出通知
t1 come in
t1 通过第1个park// 程序阻塞到这里
虚假唤醒
实现线程等待和唤醒时需要注意是否有虚假唤醒问题。
例如下面示例代码,本意是线程 A 和 线程 C 在 number 为 0 时对其加 1,线程 B 和线程 D 在 number 为 1 时对其减 1,最后结果预期是 0。
package juc;/*** 虚假唤醒Demo*/class share {private int number = 0;public synchronized void increment() throws InterruptedException {if (number != 0) {this.wait();}number++;System.out.println(Thread.currentThread().getName() + " : " + number);this.notifyAll();}public synchronized void decrement() throws InterruptedException {if (number == 0) {this.wait();}number--;System.out.println(Thread.currentThread().getName() + " : " + number);this.notifyAll();}
}public class SpuriousWakeupDemo {public static void main(String[] args) {share share = new share();new Thread(() -> {for (int i = 0; i < 10; i++) {try {share.increment();} catch (InterruptedException e) {e.printStackTrace();}}}, "A").start();new Thread(() -> {for (int i = 0; i < 10; i++) {try {share.decrement();} catch (InterruptedException e) {e.printStackTrace();}}}, "B").start();new Thread(() -> {for (int i = 0; i < 10; i++) {try {share.increment();} catch (InterruptedException e) {e.printStackTrace();}}}, "C").start();new Thread(() -> {for (int i = 0; i < 10; i++) {try {share.decrement();} catch (InterruptedException e) {e.printStackTrace();}}}, "D").start();}
}
实际运行结果可能是:
A : 1
D : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
D : -1
D : -2
D : -3
D : -4
D : -5
D : -6
D : -7
D : -8
D : -9
A : -8// 程序还可能阻塞住
出现这个问题的原因就是虚假唤醒,上述代码运行情况可能是:
-
线程 A 先抢到锁,将 number 加 1 ,number 为 1,通知其它线程,第 1 次方法执行结束,释放锁(因为 synchronized 是加在方法上面的,方法执行完后释放锁)
-
线程 C 抢到锁,判断 if (number != 0) 不满足条件,调用 this.wait() 等待,释放锁;
-
线程 A 抢到锁,第 2 次执行方法,判断 if (number != 0) 不满足条件,调用 this.wait() 等待,释放锁;
-
线程 B 抢到锁,将 number 减 1, number 为 0,通知其它线程,第 1 次方法执行结束释放锁;
-
线程 A 抢到锁,收到线程 B 的通知结束等待,执行 wait 后面的代码将 number 加 1, number 为 1,通知其它线程,第 2 次方法执行结束,释放锁;
-
线程 C 抢到锁,收到线程 A 的通知结束等待,执行 wait 后面的代码将 number 加 1,number 为 2,通知其它线程,第 2 次方法执行结束,释放锁;
… …
这里就出现了 number 大于 1 的情况,虽然和上面输出结果不一样,但都是不符合预期的结果。
解决方法是将判断条件 if (number == 0) 改为 while (number == 0):
package juc;/*** 虚假唤醒Demo*/class share {private int number = 0;public synchronized void increment() throws InterruptedException {while (number != 0) {this.wait();}number++;System.out.println(Thread.currentThread().getName() + " : " + number);this.notifyAll();}public synchronized void decrement() throws InterruptedException {while (number == 0) {this.wait();}number--;System.out.println(Thread.currentThread().getName() + " : " + number);this.notifyAll();}
}public class SpuriousWakeupDemo {public static void main(String[] args) {share share = new share();new Thread(() -> {for (int i = 0; i < 10; i++) {try {share.increment();} catch (InterruptedException e) {e.printStackTrace();}}}, "A").start();new Thread(() -> {for (int i = 0; i < 10; i++) {try {share.decrement();} catch (InterruptedException e) {e.printStackTrace();}}}, "B").start();new Thread(() -> {for (int i = 0; i < 10; i++) {try {share.increment();} catch (InterruptedException e) {e.printStackTrace();}}}, "C").start();new Thread(() -> {for (int i = 0; i < 10; i++) {try {share.decrement();} catch (InterruptedException e) {e.printStackTrace();}}}, "D").start();}
}
修改后的输出为:
A : 1
D : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0Process finished with exit code 0
参考
- 尚硅谷JUC并发编程: 第 50 - 55 节 (https://www.bilibili.com/video/BV1ar4y1x727?spm_id_from=333.788.videopod.episodes&vd_source=9266b9af652d5902d068c94b9d60116f&p=50)
- 【尚硅谷】大厂必备技术之JUC并发编程:第10节(https://www.bilibili.com/video/BV1Kw411Z7dF?p=10&spm_id_from=333.788.videopod.episodes&vd_source=9266b9af652d5902d068c94b9d60116f)