可重入的独占锁——ReentrantLock源码分析
ReentrantLock面试题分析
1、ReentrantLock是怎么实现的?
2、ReentrantLock的公平锁和非公平锁是如何实现的?
1.ReentrantLock类图结构
从类图我们可以直观地了解到,ReentrantLock最终还是使用AQS来实现地,并且根据参数来决定其内部是一个公平🔒还是非公平锁🔒,默认是非公平锁🔒。
public ReentrantLock() { sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
其中Sync类直接继承自AQS,它的子类NonfairSync和FairSync分别实现了获取锁的非公平与公平策略。
如果读者对AQS还不了解的话,可以去看看我的这篇文章:抽象同步队列AQS——AbstractQueuedSynchronizer锁详解
在这里,AQS的state状态值表示线程获取该锁的可重入次数,在默认情况下,state的值为0表示当前锁没有被任何线程持有。当一个线程第一次获取该锁时,会尝试使用CAS设置state的值为1,
如果CAS成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程。在该线程没用释放锁的情况下第二次获取该锁后,状态值被设置为2,这就是可重入次数。
在该线程释放锁时,会尝试使用CAS让状态值减1,如果减1后状态值为0,则当前线程释放该锁。
2.获取锁的主要方法
2.1 void lock()方法
lock()获取锁,其实就是把state从0变成n(重入锁可以累加)。实际调用的是sync的lock方法,分公平和非公平。
public void lock() { sync.lock(); }
在如上代码中,ReentrantLock的lock()委托给sync类,根据创建的ReentrantLock构造函数选择sync的实现是NonfairSync还是FairSync,先看看sync的子类NonfairSync(非公平锁🔒)的情况
final void lock() { if (compareAndSetState(0, 1))//CAS设置状态值为1 setExclusiveOwnerThread(Thread.currentThread());//设置该锁的持有者为当前线程 else //CAS失败的话 acquire(1);//调用AQS的acquire方法,传递参数为1 }
下面是AQS的acquire的核心源码
public final void acquire(int arg) { if (!tryAcquire(arg) &&//调用ReentantLock重写tryAcquire方法 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//tryAcquire返回false会把当前线程放入AQS阻塞队列 selfInterrupt(); }
之前说过,AQS并没有提供可用的tryAcquire方法,tryAcquire方法需要子类自己定制化,所以这里代码会调用ReentantLock重写的tryAcquire方法。我们看下非公平锁🔒的实现
protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) {//当前AQS状态为0,acquires参数传递默认为1,因为之前CAS失败,再次获取锁 if (compareAndSetState(0, acquires)) {//CAS设置状态值为1 setExclusiveOwnerThread(current);//设置该锁的持有者为当前的线程 return true; } } else if (current == getExclusiveOwnerThread()) {//如果当前线程是该锁的持有者 int nextc = c + acquires;//获取过了就累加,因为可重入 if (nextc < 0) // overflow//说明可重入次数溢出了 throw new Error("Maximum lock count exceeded"); setState(nextc);//重新设置锁的状态 return true; } return false;//如果当前线程不是该锁的持有者,则返回false,然后会放入AQS阻塞队列 }
结束完非公平锁🔒的实现代码,回过头来看看非公平在这里是怎么体现的。首先非公平是说先尝试获取锁的线程并不一定比后尝试获取锁的线程优先获取锁🔒。
而是使用了抢夺策略。那么下面我们看看公平锁🔒是怎么实现公平的。
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) {//当前AQS状态为0 if (!hasQueuedPredecessors() &&//公平性策略,判断队列还有没有其它node,要保证公平 compareAndSetState(0, acquires)) {//CAS设置状态 setExclusiveOwnerThread(current);//设置获取锁的线程 return true; } } else if (current == getExclusiveOwnerThread()) {//如果当前线程是该锁的持有者 int nextc = c + acquires;//重入次数+1 if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc);//重新设置锁的状态 return true; } return false; } }
如上代码所示,公平的tryAcquire策略与非公平的类似,不同之处在于,代码在设置CAS操作之前添加了hasQueuedPredecessors()方法,该方法是实现公平性的核心代码。代码如下
public final boolean hasQueuedPredecessors() { Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
2.2void lockInterruptibly()方法
该方法与lock()方法类似,不同在于对中断进行响应,如果当前线程在调用该方法时,其它线程调用了当前线程的interrupt()方法,则该线程抛出异常而返回
public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); }
public final void acquireInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted())//如果当前线程被中断,则直接抛出异常 throw new InterruptedException(); if (!tryAcquire(arg))//尝试获取资源 doAcquireInterruptibly(arg);//调用AQS可被中断的方法 }
2.3 boolean tryLock()方法
尝试获取锁,如果当前锁没用被其它线程持有,则当前线程获取该锁并返回true,否则返回false。注意,该方法不会引起当前线程阻塞
public boolean tryLock() { return sync.nonfairTryAcquire(1); }
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
如上代码与非公平锁的tryAcquire()方法代码类似,所以tryLock()使用的是非公平策略。
2.4 boolean tryLock(long timeout, TimeUnit unit)方法
尝试获取锁,与tryLock()的不同之处在于,它设置了超时时间,如果超时时间到了,没用获取到锁,则返回false,以下是相关代码
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout));//调用AQS的tryAcquireNanos方法 }
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout); }
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (nanosTimeout <= 0L) return false; final long deadline = System.nanoTime() + nanosTimeout; final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return true; } nanosTimeout = deadline - System.nanoTime(); if (nanosTimeout <= 0L) return false; if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) LockSupport.parkNanos(this, nanosTimeout); if (Thread.interrupted()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
3 释放锁相关方法
3.1 void unlock()方法
尝试获取锁,如果当前线程持有锁,则调用该方法会让该线程持有的AQS状态值减1,如果减1后当前状态值为0,则当前线程会释放该锁,否则仅仅减1而已。
如果当前线程没用持有该锁而调用了该方法则会抛出异常,代码如下:
public void unlock() { sync.release(1); }
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
protected final boolean tryRelease(int releases) { int c = getState() - releases;//AQS状态值减1 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) {//如果当前可重入次数为0,则清空锁持有线程 free = true; setExclusiveOwnerThread(null); } setState(c);//设置可重入次数为原始值减1 return free; }
4.案例介绍
下面使用ReentrantLock来实现一个简单的线程安全的list集合
public class ReentrantLockList { //线程不安全的list private ArrayList<String>arrayList=new ArrayList<>(); //独占锁 private volatile ReentrantLock lock=new ReentrantLock(); //添加元素 public void add(String e){ lock.lock(); try { arrayList.add(e); }finally { lock.unlock(); } } //删除元素 public void remove(String e){ lock.lock(); try { arrayList.remove(e); }finally { lock.unlock(); } } //获取数据 public String get(int index){ lock.lock(); try { return arrayList.get(index); }finally { lock.unlock(); } } }
如上代码在操作arrayList元素前进行加锁保证同一时间只有一个线程可用对arrayList数组进行修改,但是也只能一个线程对arrayList进行访问。
如图,假如线程Thread-1,Thread-2,Thread-3同时尝试获取独占锁ReentrantLock,加上Thread-1获取到了🔒,则Thread-2和Thread-3就会被转换为Node节点并放入ReentrantLock对应的AQS阻塞队列,而后阻塞挂起。
如图,假设Thread-1获取锁后调用了对应的锁创建的条件变量1,那么Thread-1就会释放获取到的🔒,然后当前线程就会被转换为Node节点插入条件变量1的条件队列。由于Thread-1释放了🔒,所以阻塞到AQS队列里面的
Thread-2和Thread-3就会有机会获取到该锁,假如使用的是公平性策略,那么者时候Thread-2会获取到锁,从而从AQS队列里面移除Thread-2对应的Node节点。
小结:
本章介绍了ReentrantLock的实现原理,ReentrantLock的底层使用AQS实现的可重入独占锁。在这里AQS状态值为0表示当前🔒空闲,为大于1的值则说明该🔒已经被占用了。
该🔒内部有公平与非公平实现,默认情况下是非公平的实现,另外,由于该锁的独占锁,所以某一时刻只有一个线程可以获取到该🔒。
本文参考书籍
Java并发编程之美

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
分布式Redis深度历险-Sentinel
上一篇介绍了Redis的主从服务器之间是如何同步数据的。试想下,在一主一从或一主多从的结构下,如果主服务器挂了,整个集群就不可用了,单点问题并没有解决。Redis使用Sentinel解决该问题,保障集群的高可用。 如何保障集群高可用 保障集群高可用,要具备如下能力: 能监测服务器的状态,当主服务器不可用时,能及时发现 当主服务器不可用时,选择一台最合适的从服务器替代原有主服务器 存储相同数据的主服务器同一时刻只有一台 要实现上述功能,最直观的做法就是,使用一台监控服务器来监视Redis 服务器的状态。 监控服务器和主从服务器间维护一个心跳连接,当超出一定时间没有收到主服务器心跳时,主服务器就会被标记为下线,然后通知从服务器上线成为主服务器。 当原来的主服务器上线后,监控服务器会将其转换为从服务器。 按照上述流程似乎解决了集群高可用的问题,但似乎有哪里不对:如果监控服务器出了问题怎么办?我们可以在加上一个从监控服务器,当主服务器不可用的时候顶上。 但问题是谁来监控’监控服务器’呢?子子孙孙无穷尽也。。 先把疑问放在一旁,先来看下Redis Sentinel集群的实现 Sentinel...
- 下一篇
一篇文章搞定——JDK8中新增的StampedLock
一、StampedLock类简介 StampedLock类,在JDK1.8时引入,是对读写锁ReentrantReadWriteLock的增强,该类提供了一些功能,优化了读锁、写锁的访问,同时使读写锁之间可以互相转换,更细粒度控制并发。 首先明确下,该类的设计初衷是作为一个内部工具类,用于辅助开发其它线程安全组件,用得好,该类可以提升系统性能,用不好,容易产生死锁和其它莫名其妙的问题。 1.1 StampedLock的引入 上一篇文章,讲解了读写锁——ReentrantReadWriteLock原理详解 ,那么为什么有了ReentrantReadWriteLock,还要引入StampedLock? ReentrantReadWriteLock使得多个读线程同时持有读锁(只要写锁未被占用),而写锁是独占的。 但是,读写锁如果使用不当,很容易产生“饥饿”问题: 比如在读线程非常多,写线程很少的情况下,很容易导致写线程“饥饿”,虽然使用“公平”策略可以一定程度上缓解这个问题,但是“公平”策略是以牺牲系统吞吐量为代价的。 1.2 StampedLock的特点 try系列获取锁的函数,当获取锁失...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS8编译安装MySQL8.0.19
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- CentOS8安装Docker,最新的服务器搭配容器使用
- Linux系统CentOS6、CentOS7手动修改IP地址
- 设置Eclipse缩进为4个空格,增强代码规范
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库