Java并发世界的两把锁
关于 ReentrantLock ,我们必须弄明白Java有了synchronized,为何再造ReentrantLock ?
这是理解ReentrantLock 和synchronized 区别的基础。
相信你曾经也看过不少关于Lock和synchronized比对,或者面试被问过ReentrantLock 和synchronized 的区别。诸如:
1、synchronized是关键字,ReentrantLock是类;
2、synchronized不需要显示释放锁,ReentrantLock必须显示释放锁
3、ReentrantLock支持响应中断、支持超时等;
4、ReentrantLock支持多个条件变量,更灵活。
5、JDK 1.6对synchronized进行了锁优化,性能和ReentrantLock相当。
性能,也一直是synchronized和Lock讨论的重点,例如在 Java 的 1.5 版本中,synchronized 性能不如 SDK 里面的 Lock,但 1.6 版本之后,synchronized 做了很多优化,将性能追了上来,所以 1.6 之后的版本又有人推荐使用 synchronized 了。
这样回答,都没有错;只是更深层次的,Lock实现的每一个接口都是经过深思熟虑的,可以说Lock 是继synchronized之后,专门设计的一把锁,全面弥补 synchronized 的问题。
面试官有时希望看到的是不一样的回答(体现你更深的掌握程度)。
synchronized和ReentrantLock 二者绝不是什么“替代”关系,性能也绝不是衡量选择二者的主要原因。
如果性能不是,那会是什么?答案在 Java有了synchronized,为何再造ReentrantLock ?
Lock的Condition是什么?
回归正题,如果面试官问你 Lock的Condition是什么?
直接告诉他:JUC condition[本质上就]是一个队列。今天我们就来细聊Condition。
ReentrantLock 一把锁可以有多个队列。
condition1.await()就是进入condition1的队列;
condition1.signal()唤醒condition1队列中的一个阻塞线程(唤醒后还需重新获得锁);
Condition本质上是个队列;让线程进入队列进行阻塞等待、或被唤醒出队列。具体被阻塞和唤醒的条件完全取决于业务。
下面这段,是摘自Oracle官网对Condition的描述:
https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/Condition.html
Conditions (also known as condition queues or condition variables)
provide a means for one thread to suspend execution (to "wait")
until notified by another thread that some state condition may now be true.
Because access to this shared state information occurs in different threads,
it must be protected, so a lock of some form is associated with the condition.
The key property that waiting for a condition provides is that it
atomically releases the associated lock and suspends the current thread, just like Object.wait.
翻译如下:
Condition(也称为 条件队列 或 条件变量),提供了一种能力:能够将一个线程暂停执行(“等待”)直到满足某些状态条件、被另一线程通知。
由于对该共享状态信息的访问发生在不同的线程中,因此必须对其进行保护,因此某种形式的锁与该条件相关联。
就像调用Object.wait一样,能够自动释放锁、并阻塞当前线程(将当前线程加入阻塞队列)。
Condition本质上是个队列,除了官网的描述,下篇将会详细分析RetrantLock的Condition源码,从源码层面看Condition提供的队列能力,敬请期待。
用Condition 解决同步问题
14 | 并发编程之Lock和Condition 文中我们介绍了 Java SDK 并发包通过 Lock 和 Condition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题。
下面我们来看Condition是如何实现线程间同步、协作的。
在Java 1.5 JUC出现之前,我们一直使用synchronized、搭配Object.wait和Object.notify实现线程间的协作。
用“等待-通知”机制优化循环等待 文中我们介绍了基于Object的wait/notify 线程间协作的方式,主要通过两个函数完成,Object.wait()和Object.notify(),这两个函数搭配synchronized关键字使用。
而Condition有着大致相同的功能,它与重入锁RenentrantLock相关联,因此Condition一般都是作为Lock的内部实现。通过Lock接口的Condition newCondition()方法生成一个和重入锁关联的Condition实例。利用Condition对象,可以让线程在合适的时间等待,或在某一时刻得到通知继续执行。
使用Condition实现线程同步的经典例子,就是用两个Condition实现阻塞队列:
public class BlockedQueue<T>{
final Lock lock =
new ReentrantLock();
// 条件变量:队列不满
final Condition notFull =
lock.newCondition();
// 条件变量:队列不空
final Condition notEmpty =
lock.newCondition();
List<T> items = new LinkedList<T>();
// 入队
void put(T x) {
lock.lock();
try {
while (队列已满){
// 等待队列不满
notFull.await();
}
// 省略入队操作...
// 入队后, 通知可出队
notEmpty.signal();
}finally {
lock.unlock();
}
}
// 出队
void take(){
lock.lock();
try {
while (队列已空){
// 等待队列不空
notEmpty.await();
}
// 省略出队操作...
// 出队后,通知可入队
notFull.signal();
}finally {
lock.unlock();
}
}
}
Lock为什么需要使用Condition
因为有时候获得锁的线程,发现其某个条件不满足、导致不能继续后面的业务逻辑,此时该线程只能先释放锁,等待条件满足。那可不可以不释放锁的等待呢?比如将await方法替换为sleep方法(这也是面试经常问的await和sleep的区别)?
显然不行,因为等待的条件显然和共享的资源是有关的。
在这个例子里,take方法会等待notEmpty条件,notEmpty指的是items不为空;(notEmpty不满足时)意味着此时items是空的,那么就只有对items执行add操作,即其它线程调用put方法才有机会达到notEmpty的条件,所以如果使用sleep(不释放锁)来等待而不是await(释放锁)来等待,则会导致notEmpty这个条件永远满足不了。
Condition使用事项
在 08-管程:并发编程的万能钥匙 我们提到了使用Object.wait() 的正确姿势,提到,Object.wait()必须在synchronized中使用,且Object.wait()必须在while循环中使用。
同样的Condition.await()也是如此。
Oracle官网 Implementation Considerations 对此也进行了说明
翻译如下:当使用Condition.await时,通常允许进行“ 虚假唤醒 ”,作为对底层平台语义的让步。这对大多数应用程序几乎没有实际影响,因为 Condition应始终循环等待,测试正在等待的状态谓词。一个实现可以自由地消除虚假唤醒的可能性,但是建议开发者始终假定它们会发生,因此总是在循环中等待。——在while循环中调用await。
总结
Condation就是加强版的Object.wait/notify;必须在lock/unlock()之前使用,就像wait/notify必须在synchronized块中使用一样。
一个Condition就是一个队列,理论上ReentrantLock可以有无数个Condition队列;这是synchronized无法比拟的,synchronized只有一个队列。
下篇将会详细分析RetrantLock的Condition源码,从源码层面看Condition是如何实现条件队列的。