线程安全的实现方法概述
线程安全的实现方法,可以分为 阻塞同步,非阻塞同步,无同步方案三大类:
其中阻塞同步最基本的两个实现手段就是通过 synchronized 或 ReentrantLock 关键字。这篇文章主要记录下 RentrantLock 的源码实现。
ReentrantLock 是基于 抽象类 AbstractQueuedSynchronizer 实现的,AQS内部使用了一个volatile的变量state 来作为资源的标识,并提供了一些模版方法,ReentrantLock 通过实现 AQS 的 tryAcquire 实现获取锁的逻辑。
ReentrantLock 的锁类型
ReentrantLock 分为公平锁和非公平锁,可以通过构造方法来指定具体类型:
1 | //默认非公平锁 |
ReentrantLock 使用示例模版
通常的使用方式如下:
1 | private ReentrantLock lock = new ReentrantLock(); |
ReentrantLock 的源码实现
公平锁的实现
1 | public void lock() { |
可以看到,ReentrantLock 的lock 方法,其实是通过调用 AQS 提供的模版方法 acquire 来获取独占锁的。
AbstractQueuedSynchronizer 的模版方法如下:
1 | //通过 中的 acquire() 获取独占资源 |
获取锁
ReentrantLock 的 tryAcquire实现如下:
1 | protected final boolean tryAcquire(int acquires) { |
可以看到在 ReentrantLock 中判断锁是否被获取,是通过 state 来判断的, state =0 则表示目前没有其他线程获得锁,当前线程就可以尝试获取锁;state 大于0 则表示锁已经被占用,则判断占用锁的线程是否就是当前线程,如果是当前线程,则 state +1, 表示重入数 +1。
注意:尝试之前会利用 hasQueuedPredecessors() 方法来判断 AQS 的队列中是否有其他线程,如果有则不会尝试获取锁(获取公平锁才有这个判断)。
如果队列中没有线程就利用 CAS 来将 AQS 中的 state 修改为1,也就是获取锁,获取成功则将当前线程置为获得锁的独占线程(setExclusiveOwnerThread(current))。
获取锁失败则添加到队列中
如果 tryAcquire(arg) 获取锁失败,则需要用 addWaiter(Node.EXCLUSIVE) 将当前线程包装为一个Node节点写入队列中(AQS 中的双端队列是由 Node 节点组成的双向链表实现的)。其中传入的参数代表要插入的Node是独占式的。
入队的代码如下:
1 |
|
首先判断队列是否为空,不为空时则将封装好的 Node 利用 CAS 写入队尾,如果出现并发写入失败就需要调用 enq(node); 来写入了。在enq中通过 自旋加上 CAS 来保证写入队列成功。
挂起等待线程
1 | final boolean acquireQueued(final Node node, int arg) { |
首先会根据 node.predecessor() 获取到上一个节点是否为头节点,如果是则尝试获取一次锁,获取锁成功后将。node 节点设置成头节点。
如果不是头节点,或者获取锁失败,则会根据上一个节点的 waitStatus 状态来处理(shouldParkAfterFailedAcquire(p, node))。
waitStatus 用于记录当前节点的状态,如节点取消、节点等待等。
shouldParkAfterFailedAcquire(p, node) 返回当前线程是否需要挂起,如果需要则调用 parkAndCheckInterrupt():
1 | private final boolean parkAndCheckInterrupt() { |
非公平锁的实现
公平锁和非公平锁的主要差异在于实现上:公平锁会先判断队列中是否已经有在排队的线程,如果有则自觉加入到队列尾排队,而非公平锁则不会有此顾忌,非公平锁的实现上是抢占模式的,线程一进来就会先尝试获取锁,不需要考虑先来后到之类的规则。
非公平锁与公平锁获取的差异:
1 | //非公平锁 |
还要一个重要的区别是在尝试获取锁时tryAcquire(arg),非公平锁是不需要判断队列中是否还有其他线程,也是直接尝试获取锁:
1 | final boolean nonfairTryAcquire(int acquires) { |
释放锁
公平锁和非公平锁的释放流程都是一样的:
1 | public void unlock() { |
首先会判断当前线程是否为获得锁的线程,由于是重入锁所以需要将 state 减到 0 才认为完全释放锁。释放之后需要调用 unparkSuccessor(h) 来唤醒被挂起的线程。
公平锁与非公平锁小结
非公平锁的效率会被公平锁更高。因为公平锁需要关心队列的情况,得按照队列里的先后顺序来获取锁(会造成大量的线程上下文切换),而非公平锁的实现上是抢占模式的,线程一进来就会先尝试获取锁,没有先来后到的规则限制。
ReentrantLock 与 synchronized 的一个对比(怎么选择)
synchronized 与 ReentrantLock 都是可重入的,但相比于 synchronized,ReentrantLock增加了一些高级的功能,主要有以下三项:等待可中断,可实现公平锁,以及锁可以绑定多个条件。
等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以放弃等待,该为处理其他事情,可中断特性对于处理执行时间非常长的同步块很有帮助。
公平锁是指多个线程等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁上非公平的,ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。
锁绑定多个条件是指 ReentrantLock 对象可以同时绑定多个 Condition 对象,而在 synchronized 中,锁对象的 wait()、notify() 和 notifyAll() 方法只可以实现一个隐含的条件,如果要和多余一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则可以通过多次调用 newCondition() 方法实现。
通过互斥同步来实现线程安全时,如果需要用到上述功能,则选用 ReentrantLock。如果不需要上面的功能则选用synchronized。因为性能方面,JDK1.6 发布后,synchronized 与 ReentrantLock 的性能方面已经基本上没什么差异了,并且JVM 更倾向于优化改进更偏向于原生的 synchronized。
HotSpot 虚拟机团队在 JDK1.6 开始,已对synchronized 进行了多种锁优化,比如自适应自旋、锁消除、锁粗化、轻量级锁、和偏向锁等。这些技术都是为了在线程之间优先以更高效的方式决绝竞争问题,从而提供程序的执行效率。
参考文献
- 《深入理解Java虚拟机》
- ReentrantLock 实现原理
- 从ReentrantLock的实现看AQS的原理及应用