ReentrantLock 公平锁和非公平锁源码分析
ReentrantLock 与 sychronized
JUC包是Doug Lea大神在jdk1.5以后提供的一个java并发编程的工具包。
今天我们主要简单的分析一下重入锁ReentrantLock的源码。提到ReentrantLock,不得不说一下jvm为我们提供的sychronized关键字
区别:
1、ReentrantLock只能加在代码块上,而sychronized可以加在代码块里,也可以加在静态方法和对象方法上。
2、ReentrantLock需要显式的加锁和释放锁,sychronized不需要
3、ReentrantLock可以实现公平锁和非公平锁,sychronized只能是非公平锁,后面我们会具体分析如何实现,也是本文的重点
4、ReentrantLock基于AQS实现,sychronized基于ObjectMonitor实现,底层原理都是使用操作系统的mutex实现加锁和阻塞。
多线程场景
首先多线程程序有多种情况
1、无线程竞争
2、少量线程竞争
3、竞争激烈
其实我们的程序运行的绝大多数场景下是少量线程竞争甚至是无线程竞争的情况,举一个比较极端的例子,下面的程序只可能有一个线程执行。如果每次都在操作系统的层面使用mutex互斥锁来加锁解锁,势必会引起上下文切换,上下文切换是可能相对于我们的 dosomething() 方法是一个非常重量级操作。下面代码执行时会执行1000000次上下文切换。
不要惊讶,jdk1.6 sychronized同步锁就是这样的同步机制,这就是为什么sychronized被称为重量级锁的原因。注意这只是jdk1.6之前,jdk1.6之后由于jvm提供的锁消除、锁膨胀、偏向锁、轻量级锁、自旋锁等等机制,sychronized的性能已经是非常好了。
分析上面的三种多线程程序运行的情况,我们期望
无线程竞争情况下,程序不加锁
少量线程竞争,程序加轻量级锁,不引起用户态和内核态的切换
竞争激烈,毫无疑问程序使用mutex互斥量加锁,会引起用户态和内核态的切换
ReentrantLock就实现了这样一个逻辑。本文中就是简单的分析一下ReentrantLock在三种情况下的加锁逻辑和探秘公平锁、非公平锁的实现
源码分析
下面我们从一段源码入手,分析一下ReentrantLock的加锁逻辑,公平锁、非公平锁的实现。
1、构造函数
2、lock()方法 非公平锁
sync是ReentrantLock的内部类,继承自 AbstractQueuedSynchronizer (AQS)抽象同步队列,juc大部分都是基于这个AQS实现的,今天我们不分析AQS,主要还是讲ReentrantLock的加锁逻辑,它内部有两个实现类 FairSync 和 NonfairSync。我们首先接下来分析一下公平锁FairSync的实现
3、acquire(1) 方法,
该方法在AbstractQueuedSynchronizer,我们可以非常浅显的理解为它就是一个FIFO的队列,队列里面的元素是线程。然后队列有一个私有属性int state,我们可以把它理解为一个互斥变量,如果state=0,我们的线程可以把它通过cas操作改为1,说明当前线程争抢到了锁,如果state=1且当前线程已经获取锁,此时设置state=2(重入)。如果state=1且不是当前前线程操作的,当前线程就会自旋或者挂起来等待锁。
我们看下面的代码。
如果tryAcquire返回true,&& 后面方法不会执行,说明获取锁成功
tryAcquire返回false,
4、tryAcquire(arg)
先看tryAcquire(arg),如果这个方法返回为false,acquireQueued()方法才会执行。
5、hasQueuedPredecessors()
接下来看一下tryAcquire()方法中的的hasQueuedPredecessors()方法,这个地方其实就是公平锁的关键,如果队列里有其它线程等待获取锁,因为公平锁的原因,当前线程是不能获取锁的,应该去排队。
这个地方也是AQS的核心
6、acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
如果第四步返回tryAcquire(int acquires) 方法返回false,说明当前线程获取锁失败了,接下来就需要排队了,我们回到第三步,看到该执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)。
这种情况下就是有线程竞争了,自己竞争失败了。这个方法的主要逻辑就是,自旋获取锁,获取不到则挂起。
先看addWaiter(Node.EXCLUSIVE)
接下来当前线程已经入队,看acquireQueued()方法的执行逻辑,这个地方也是轻量级锁的实现。如果当前线程入队了,但是接下来执行的概率非常大,它也不会挂起,而是再次获取锁,如果获取不到那就挂起,知道被唤醒。
流程总结
以下是ReentrantLock公平模式下,线程获取锁的流程图
总结以下公平锁的逻辑
1、当前线程尝试获取锁
2、如果state=0说明当前锁没有线程占用,进入第三步,否则进入第四步
3、判断是否需要入队,如果需要入队则当前线程进入队列,进入第四步;否则则尝试获取修改state=1,修改成功则说明获取锁成功,否则获取锁失败,进入第四步
4、当前线程进入AQS队列
5、判断当前线程的前继节点是否为头节点,如果为头节点,则自旋等待获取锁。如果仍然获取锁失败,当前线程挂起。
6、如果当前线程能修改state=1,则说明当前线程获取锁成功。
分析一下使用ReentrantLock在多线程程序运行下的三个状态下获取锁的逻辑
1、无线程竞争,当前线程永远不需要进入队列,能直接获取锁
2、少量线程竞争,线程可能获取不到锁,进入队列等待,都尝试获取锁,比如之前提到的自旋操作,尝试获取锁失败以后才会挂起。
3、多线程竞争激烈,线程进入队列,挂起和唤醒,上下文切换
之前我们说过,jdk1.6之后由于jvm提供的锁消除、锁膨胀、偏向锁、轻量级锁、自旋锁等等机制,sychronized的性能已经是非常好了。但是有一点它还是不如ReentrantLock,sychronized的锁升级是不可逆的,而ReentrantLock是可逆的,锁是可以降级的。
FairSync 和 NonfairSync
上面我们看了一下ReentrantLock的公平锁的实现逻辑,接下来我们看一下非公平锁的实现。
其实大多数人都会误会,公平锁用的FIFO的队列,非公平锁是不是换成Set就可以实现非公平了(sychronized的wait和notify就是使用的Set数据结构)。答案是否,非公平锁和公平锁的区别很简单。
公平锁:如果当前state=0,说明当前锁没有被线程获取,当前线程需要判断是否需要进队列,也就是我们上面说到的hasQueuedPredecessors()方法,如果队列里面有其它线程等待,则当前线程进队列
非公平锁:如果当前state=0,说明当前锁没有被线程获取,当前线程尝试获取锁,如果队列里有其它线程等待,就会竞争,如果获取不到才会进入队列排队等待。
综上所属,非公平锁只的是进入队列之前也不要竞争获取锁,一旦进入队列就需要FIFO等待获取锁。
下面是非公平锁的代码
所以说非公平锁更能压榨CPU的性能,效率更好。
版权声明: 本文为 InfoQ 作者【张sir】的原创文章。
原文链接:【http://xie.infoq.cn/article/eb6803d2ba80006dbbbe590dd】。文章转载请联系作者。
评论 (1 条评论)