Java多线程— condition原理
Java中,关于如何精确唤醒你想指定的线程类型,大家都会做,不就是使用显式的 lock 和 condition 吗。
但我不知道大家是否思考过这样的问题: 精确唤醒 与 精确执行 的区别
我规定 A B C 三个线程按 A B C 顺序执行,不使用 lock 和 condition ,我就使用 内置锁synchronized 和 wait、notify ,发现也可以实现, 但是 这种方式却并不是 “精确唤醒”,虽然它也让线程执行顺序达到了你的预期,但它的代价更大。
为什么 内置锁的方式代价更大 ?
因为使用内置锁,可能存在 从 A执行 --> B执行 期间, 其它线程被唤醒 并持有了锁,但发现没轮到自己执行,又释放了锁,这无疑增加了线程切换的消耗;
而使用 Condition ,那么 A执行后,唤醒的必然是B线程,不可能是是其它线程。
为什么 condition 可以唤醒指定的线程?
我们举个例子,规定
- 新建A B C 三个线程
- A线程先执行,然后 B 执行,再 C执行
- A B C 依次循环
代码如下:
public class ExactSignal {
public static void main(String[] args) {
Example example = new Example();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.printA();
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.printB();
}
},"B").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.printC();
}
},"C").start();
}
}
class Example {
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
// 给线程指定下标
private int index = 1;
public void printA(){
lock.lock();
try {
while(index != 1){
condition1.await();
}
System.out.println(Thread.currentThread().getName()+"---AAA");
index = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
while(index != 2){
condition2.await();
}
System.out.println(Thread.currentThread().getName()+"---BBB");
index = 3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
while(index != 3){
condition3.await();
}
System.out.println(Thread.currentThread().getName()+"---CCC");
index = 1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
测试结果: 确实 A B C 循环依次执行
先搞清楚几个概念:
条件谓词 : 使某个操作成为状态依赖操作的前提操作;说人话就是:线程执行的条件,若该条件为假,则阻塞,为真 则继续执行
条件队列:它使得一组线程(等待线程集合)能够通过某种方式来等待特定条件变成真,其元素是一个个正在等待相关条件的线程。
Lock 对应 synchronized 的内置锁,而condition 对应 内置锁的条件队列
在 synchronized 中,一把锁只维护一个条件队列,一个 “条件队列” 却与多个 “条件谓词” 相关。
当 使用 notify 方法时,JVM 会从条件队列上等待的多个线程中 选择一个来唤醒,notifyAll 唤醒所有,所以 notify 可能会导致 “信号丢失” 的问题。( A 执行完通知 B,但 JVM 唤醒了C,B没醒过来(信号丢失),锁 被C 抢了(虚假唤醒,又叫 过早唤醒),所以设置 while 循环去判断条件谓词真假,既可以解决信号丢失,又可以解决 虚假唤醒)
一个 lock 可以有多个 condition,每一个condition 就是一个 条件队列,在同一个条件队列中的所有线程,他们的条件谓词是一样的,即他们被阻塞的原因是一样的
以上代码中,A B C 顺序执行的原因:
- 存在 3 个条件队列 , 姑且叫做 a b c
- a 队列的条件谓词是 index = 1 ;b 队列的条件谓词是 index = 2; c 队列的条件谓词是 index = 3
- 当 index = 1 时,B 和 C 进入各自的方法,分别被 condition2 和 condition3 塞入各自的条件队列 分别是 b 和 c, 即 b 队列有 B 线程, c 队列有 C线程
- A 执行完,index = 2, 调用 b 队列的通知方法 condition2.signal() ,此时唤醒的线程只可能是 b 队列中的某个线程, 不可能是c 队列,所以C 线程并不会被唤醒; 如果使用notify的话,C 线程可能就会被唤醒,之后C又阻塞,增大了线程切换的开销 和锁请求的次数
- 所以 信号丢失 的问题 , condition 是可以解决的,但注意 condition 不能解决 虚假唤醒, 所以 你 使用condition 时,还是需要使用 while 循环
综上所述,按照我的理解,使用 condition 可以将 等待线程 根据他们的条件谓词 进行分类,确保程序员可以唤醒指定类型的线程,避免唤醒没必要执行的线程,减少了 循环执行await 的时间 ,线程切换的开销 和锁的请求次数