为了保证同步的安全性,除了synchronized关键字,java并发包中java.util.concurrent.locks中的ReentrantLock和ReentrantReadWriteLock也是常用的锁实现。本篇从源码方面,分析一下重入锁ReentrantLock的原理。
先说一下什么的重入锁:某个线程获得锁以后,还可以多次重复获得锁,不会自己阻塞自己。
ReentrantLock基于抽象类AbstractQueuedSynchronizer(以下简称AQS)实现。
源码
首先从构造器上可以看出,ReentrantLock有公平锁和非公平锁两种机制。
复制代码
1 | //默认非公平锁 |
先简要说明一下公平锁和非公平锁的区别,然后在分析两者的不同实现方式。
公平锁:多个线程之间讲究先来后到。类似于排队,后面来的线程依次排在队列最后。
非公平锁:进行锁的争抢。抢到就执行,没抢到就阻塞。等待获得锁的线程释放后,再参与竞争。
所以通常使用非公平锁。其效率比公平锁高。
获取锁
公平锁
1 | final void lock() { |
第一步tryAcquire(arg)尝试加锁,由FairSync实现,具体代码如下:
1 | protected final boolean tryAcquire(int acquires) { |
- 获取当前线程
- 获取AQS中的state。如果state为0,表示此时没有线程获得锁。
- 在if判断中,先要判断AQS的Node队列是否为空。如果不是空的,就需要排队。此时不获取锁。
- 尝试使用CAS算法,将state更新为1。更新成功,获取锁,将此时的线程设置为独占线程exclusiveOwnerThread。返回true。
- 如果state不为0,表示已经有线程获得了锁。所以要判断获得锁的线程(独占线程)是否为当前线程。
- 如果是,说明是重入情况。将state增加1。返回true。
- 走到最后一步,就是没有获得锁了。返回false;
继续上面的步骤,如果获取锁失败,先执行addWaiter(Node.EXCLUSIVE),将当前线程写入队列
1 | private Node addWaiter(Node mode) { |
- 封装一个新节点node
- 判断链表尾是否为空,不是就把新节点node‘写入最后
- 链表尾为空,则用enq(node)写入最后。
写入队列以后,acquireQueued()方法,挂起当前线程。
1 | final boolean acquireQueued(final Node node, int arg) { |
- 在循环中,如果node的上一个是头节点,则再尝试获取锁。成功就结束循环,返回false
- 不是头节点,就根据上一个节点的waitStatus,判断是否需要挂起当前线程。waitStatus用来记录节点状态,如节点取消,节点等待等。
- 判断需要挂起,则使用parkAndCheckInterrupt()方法,挂起线程。具体使用LockSupport.park(this)挂起线程。
- 如果在这里的第一步就获取锁成功了,就可以取消此节点的获取锁操作了。
非公平锁
非公平锁在锁的获取策略上有差异。
1 | final void lock() { |
- 非公平锁先直接尝试使用CAS算法更新state,获取锁
- 更新失败以后,在尝试获取锁
1 | final boolean nonfairTryAcquire(int acquires) { |
与公平锁相比,非公平锁尝试获取锁的过程中,无需判断队列中是否存在其他线程。
释放锁
公平锁和非公平锁释放锁的步骤都一样
1 | public void unlock() { |
值得注意的是,因为是重入锁的关系,在tryRelease()方法中,需要将state更新为0,才认为完全释放锁。释放以后,再唤醒挂起线程。
ReenTrantLock可重入锁和synchronized的区别
可重入性:
从名字上理解,ReenTrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入的,两者关于这个的区别不大。两者都是同一个线程没进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
锁的实现:
Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的,有什么区别,说白了就类似于操作系统来控制实现和用户自己敲代码实现的区别。前者的实现是比较难见到的,后者有直接的源码可供阅读。
性能的区别:
在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。
功能区别:
便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。
锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized
ReenTrantLock独有的能力:
ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
ReenTrantLock实现的原理:
在网上看到相关的源码分析,本来这块应该是本文的核心,但是感觉比较复杂就不一一详解了,简单来说,ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。
什么情况下使用ReenTrantLock:
答案是,如果你需要实现ReenTrantLock的三个独有功能时。