Condition
Lock替换synchronized方法和语句的使用, Condition取代了对象监视器方法的使用。
- 一个Condition实例本质上绑定到一个锁。
- 要获得特定Condition实例的Condition实例,请使用其newCondition()方法。
例如,假设我们有一个有限的缓冲区,它支持put和take方法。 如果在一个空的缓冲区尝试一个take ,则线程将阻塞直到一个项目可用; 如果put试图在一个完整的缓冲区,那么线程将阻塞,直到空间变得可用。 我们希望在单独的等待集中等待put线程和take线程,以便我们可以在缓冲区中的项目或空间可用的时候使用仅通知单个线程的优化。 这可以使用两个Condition实例来实现。
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock(); try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally { lock.unlock(); }
}
public Object take() throws InterruptedException {
lock.lock(); try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally { lock.unlock(); }
}
}
Object中wait()、notify()和notifyAll() 必须和Synchronized搭配使用 和 Condition 的 await()/signal()只能在同步代码块中使用,这是为什么?
Lost Wake-Up Problem
举个例子,一个消费者线程、一个生产者线程。生产者的任务为count+1,然后唤醒消费者;消费者的任务为count-1,等到count为0时陷入沉睡。
生产者伪代码:
count++;
notify();
消费者伪代码:
while(count <= 0){ //用while防止虚假唤醒
wait();
}
count--;
问题所在:
首先我们先假设count = 0,这个时候消费者检查count的值,发现count <= 0的条件成立;就在这个时候,发生了上下文切换,生产者进来了,噼噼啪啪一顿操作,把两个步骤都执行完了,也就是发出了通知,准备唤醒一个线程。这个时候消费者刚决定睡觉,还没睡呢,所以这个通知就会被丢掉。紧接着,消费者就睡过去了……
所以这是Java设计者为了避免使用者出现Lost Wake-Up 问题而设计的
Condition.await, signal 与 Object.wait, notify 的区别
对应关系:
Condition.await() 对应于 Object.wait()
Condition.signal() 对应于 Object.notify ()
Condition.signalAll() 对应于 Object.notifyAll()
不同点:
Object 中的 wait,notify 对应的是 synchronized 方式的锁,内部有一个条件队列
Condition 中的 await,singal则对应的是 ReentrantLock (实现 Lock 接口的锁对象)对应的锁,内部有n个条件队列(n为Lock的Condition数量)
(关键!!!)Condition原理,为什么Condition可以精确唤醒指定的线程类型
精确唤醒 与 精确执行 的区别?
例如规定 A B C 三个线程按 A B C 顺序执行,不使用 lock 和 condition ,我就使用 内置锁synchronized 和 wait、notify ,发现也可以实现, 但是 这种方式却并不是 “精确唤醒”,虽然它也让线程执行顺序达到了你的预期,但它的代价更大。
为什么 内置锁的方式代价更大 ?
因为使用内置锁,可能存在 从 A执行 --> B执行 期间, 其它线程被唤醒 并持有了锁,但发现没轮到自己执行,又释放了锁,这无疑增加了线程切换的消耗;
而使用 Condition ,那么 A执行后,唤醒的必然是B线程,不可能是是其它线程。
☆☆☆为什么 condition 可以唤醒指定的线程?
现在举个例子:
1、新建A B C 三个线程
2、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 {
if(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();
}
}
}
这里是引用先搞清楚几个概念:
条件谓词 : 使某个操作成为状态依赖操作的前提操作;说人话就是:线程执行的条件,若该条件为假,则阻塞,为真 则继续执行
条件队列:它使得一组线程(等待线程集合)能够通过某种方式来等待特定条件变成真,其元素是一个个正在等待相关条件的线程。
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 循环
自己写了一个测试代码,便于理解,什么线程被唤醒,然后进入条件队列的一系列过程
public class Test {
public static void main(String[] args) {
Data1 data1 = new Data1();
Thread thread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
data1.A();
}
}, "A");
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
data1.B();
}
}, "B");
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
data1.C();
}
}, "C");
thread.start();
thread2.start();
thread1.start();
}
}
class Data1 {
private int num = 1;
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
public void A() {
lock.lock();
try {
System.out.println("A拿到了锁");
while (num != 1) {
System.out.println("A方法中条件不满足,A释放锁,加入condition1条件队列,处于等待状态,等待被唤醒");
condition1.await();
System.out.println("A被唤醒了");
}
num = 2;
System.out.println(Thread.currentThread().getName() + "Print->AAAAAAAAAAAAAAAAAA");
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void B() {
lock.lock();
try {
System.out.println("B拿到了锁");
while (num != 2) {
System.out.println("B方法中条件不满足,B释放锁,加入condition2条件队列,处于等待状态,等待被唤醒");
condition2.await();
System.out.println("B被唤醒了");
}
num = 3;
System.out.println(Thread.currentThread().getName() + "线程:Print->BBBBBBBBBBBBBBBBBB");
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void C() {
lock.lock();
try {
System.out.println("C拿到了锁");
while (num != 3) {
System.out.println("C方法中条件不满足,C释放锁,加入condition3条件队列,处于等待状态,等待被唤醒");
condition3.await();
System.out.println("C被唤醒了");
}
num = 1;
System.out.println(Thread.currentThread().getName() + "线程:Print->CCCCCCCCCCCCCCCCCC");
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void D() {
lock.lock();
try {
while (num != 100) {
condition3.await();
}
num = 1;
System.out.println(Thread.currentThread().getName() + "线程:Print->CCCCCC");
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
看下面的运行结果,就能非常明了这个锁的获取和条件队列的进出了。
运行结果: