再造管程的意义
java的管程的关键字是synchronized,在1.6后对管程做了升级,引入了偏向锁,轻量级锁的概念,性能几乎和Lock相近。然而在某些场景还是没法取到Lock。
主要集中synchronized如下的几个痛点
- 无法主动响应中断:在synchronzed代码块中,如果已经获取了资源a,在获取b失败后,就直接进入了阻塞,无法主动响应中断,这时候遇到死锁我们是无法处理的;
- 无法设置过期时间:synchronzed无法设置过期时间;
- 无法在非阻塞情况下竞争锁资源:
1 | void lockInterruptibly() throws InterruptedException; |
lock的原理
我们以ReentrantLock为例子,类似管程,每个资源都一个等待队列。它是用一个双向链表实现的等待队列,同时每个链表的节点包含一个volatile的变量state作为锁标记位:0-release,1-lock。在调用lock/unlock方法中会通过cas修改volatile来实现state的变化。他是如何来保证在上锁解锁时候的可见性的呢?我们用下面的代码说明
rtl.lock上锁–>之后value+=1–>解锁,他们在并发场景下根据HB原则.
- 顺序原则:线程1中:rtl.lock (HB于) value+=1 ;value+=1(HB于)rtl.unlock()
- volatile的原则:此情况是针对线程1的rtl.unlock已经修改state成功但是由于可见性线程2的rtl.lock也申请锁的时候是否会失败?答案是不会,因为但当线程1的rtl.unlock修改state()和线程2的lock修改sate()发生冲突时候,由于rlt.lock需要先读取state,根据volatile的原则,写HB于读,所以unlock一定HB于lock,这时候lock一定读到的是最新的值
- 传递性原则:由于线程1的value+=1(HB于)rtl.unlock(),所以线程1的value+=1一定(HB于)于线程2的rtl.lock()
1 | class X { |
lock的特点
可重入锁
lock和synchronized一样都是可重入锁:即同一个线程在获取锁之后,可以重复获取锁资源。(不可重入锁:即在此尝试获取锁资源会进入阻塞)
公平锁和非公平数锁
lock可以实现贡公平锁,如:ReentrantLock的构造方法,见下面代码,具体可以看Sync的代码,如果是非公平锁,有可能最后获取资源的反而最先释放。详细可以去看
ReentrantLock内部类Sync的tryAcquire俩种实现。其中FairSync里有一个判断hasQueuedPredecessors
1 | /** |
Lock中的等待通知机制-Condition
管程中最主要的一个功能就是wait的等待通知机制。那么lock的等待通知机制则是Condition
1 | ReentrantLock lock=new ReentrantLock(); |
操作异步转同步
有些操作本身是异步的,但是我们在使用中需要同步的场景,比如dubbo中我们的客户端通过tcp发送数据给服务端后,我们需要接收到返回值才能继续处理请求。而tcp本身就是异步的。这时候就需要同步转异步操作是如何实现的呢,见DefaultFeature,伪代码如下
1 | public class Request { |
lock的最佳实践
永远只用于锁要修改的成员表变量
永远只在访问可变的成员标量加锁
永远不要在调用其他对象的方法加锁—因为你不确定他内部是如何实现的
推荐阅读:
Doug Lea《Java 并发编程:设计原则与模式》