本文是学习尚硅谷周阳老师《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 : 0
Process 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)
379

被折叠的 条评论
为什么被折叠?



