时间:2021-05-20
问题
(1)重入锁是什么?
(2)ReentrantLock如何实现重入锁?
(3)ReentrantLock为什么默认是非公平模式?
(4)ReentrantLock除了可重入还有哪些特性?
简介
Reentrant = Re + entrant,Re是重复、又、再的意思,entrant是enter的名词或者形容词形式,翻译为进入者或者可进入的,所以Reentrant翻译为可重复进入的、可再次进入的,因此ReentrantLock翻译为重入锁或者再入锁。
重入锁,是指一个线程获取锁之后再尝试获取锁时会自动获取锁。
在Java中,除了ReentrantLock以外,synchronized也是重入锁。
那么,ReentrantLock的可重入性是怎么实现的呢?
继承体系
ReentrantLock实现了Lock接口,Lock接口里面定义了java中锁应该实现的几个方法:
// 获取锁void lock();// 获取锁(可中断)void lockInterruptibly() throws InterruptedException;// 尝试获取锁,如果没获取到锁,就返回falseboolean tryLock();// 尝试获取锁,如果没获取到锁,就等待一段时间,这段时间内还没获取到锁就返回falseboolean tryLock(long time, TimeUnit unit) throws InterruptedException;// 释放锁void unlock();// 条件锁Condition newCondition();Lock接口中主要定义了 获取锁、尝试获取锁、释放锁、条件锁等几个方法。
源码分析
主要内部类
ReentrantLock中主要定义了三个内部类:Sync、NonfairSync、FairSync。
abstract static class Sync extends AbstractQueuedSynchronizer {}static final class NonfairSync extends Sync {}static final class FairSync extends Sync {}(1)抽象类Sync实现了AQS的部分方法;
(2)NonfairSync实现了Sync,主要用于非公平锁的获取;
(3)FairSync实现了Sync,主要用于公平锁的获取。
在这里我们先不急着看每个类具体的代码,等下面学习具体的功能点的时候再把所有方法串起来。
主要属性
主要属性就一个sync,它在构造方法中初始化,决定使用公平锁还是非公平锁的方式获取锁。
主要构造方法
(1)默认构造方法使用的是非公平锁;
(2)第二个构造方法可以自己决定使用公平锁还是非公平锁;
上面我们分析了ReentrantLock的主要结构,下面我们跟着几个主要方法来看源码。
lock()方法
彤哥贴心地在每个方法的注释都加上方法的来源。
1.公平锁
这里我们假设ReentrantLock的实例是通过以下方式获得的:
ReentrantLock reentrantLock = new ReentrantLock(true);下面的是加锁的主要逻辑:
// ReentrantLock.lock()public void lock() {// 调用的sync属性的lock()方法// 这里的sync是公平锁,所以是FairSync的实例sync.lock();}// ReentrantLock.FairSync.lock()final void lock() {// 调用AQS的acquire()方法获取锁// 注意,这里传的值为1acquire(1);}// AbstractQueuedSynchronizer.acquire()public final void acquire(int arg) {// 尝试获取锁// 如果失败了,就排队if (!tryAcquire(arg) &&// 注意addWaiter()这里传入的节点模式为独占模式acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}// ReentrantLock.FairSync.tryAcquire()protected final boolean tryAcquire(int acquires) {// 当前线程final Thread current = Thread.currentThread();// 查看当前状态变量的值int c = getState();// 如果状态变量的值为0,说明暂时还没有人占有锁if (c == 0) {// 如果没有其它线程在排队,那么当前线程尝试更新state的值为1// 如果成功了,则说明当前线程获取了锁if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {// 当前线程获取了锁,把自己设置到exclusiveOwnerThread变量中// exclusiveOwnerThread是AQS的父类AbstractOwnableSynchronizer中提供的变量setExclusiveOwnerThread(current);// 返回true说明成功获取了锁return true;}}// 如果当前线程本身就占有着锁,现在又尝试获取锁// 那么,直接让它获取锁并返回trueelse if (current == getExclusiveOwnerThread()) {// 状态变量state的值加1int nextc = c + acquires;// 如果溢出了,则报错if (nextc < 0)throw new Error("Maximum lock count exceeded");// 设置到state中// 这里不需要CAS更新state// 因为当前线程占有着锁,其它线程只会CAS把state从0更新成1,是不会成功的// 所以不存在竞争,自然不需要使用CAS来更新setState(nextc);// 当线程获取锁成功return true;}// 当前线程尝试获取锁失败return false;}// AbstractQueuedSynchronizer.addWaiter()// 调用这个方法,说明上面尝试获取锁失败了private Node addWaiter(Node mode) {// 新建一个节点Node node = new Node(Thread.currentThread(), mode);// 这里先尝试把新节点加到尾节点后面// 如果成功了就返回新节点// 如果没成功再调用enq()方法不断尝试Node pred = tail;// 如果尾节点不为空if (pred != null) {// 设置新节点的前置节点为现在的尾节点node.prev = pred;// CAS更新尾节点为新节点if (compareAndSetTail(pred, node)) {// 如果成功了,把旧尾节点的下一个节点指向新节点pred.next = node;// 并返回新节点return node;}}// 如果上面尝试入队新节点没成功,调用enq()处理enq(node);return node;}// AbstractQueuedSynchronizer.enq()private Node enq(final Node node) {// 自旋,不断尝试for (;;) {Node t = tail;// 如果尾节点为空,说明还未初始化if (t == null) { // Must initialize// 初始化头节点和尾节点if (compareAndSetHead(new Node()))tail = head;} else {// 如果尾节点不为空// 设置新节点的前一个节点为现在的尾节点node.prev = t;// CAS更新尾节点为新节点if (compareAndSetTail(t, node)) {// 成功了,则设置旧尾节点的下一个节点为新节点t.next = node;// 并返回旧尾节点return t;}}}}// AbstractQueuedSynchronizer.acquireQueued()// 调用上面的addWaiter()方法使得新节点已经成功入队了// 这个方法是尝试让当前节点来获取锁的final boolean acquireQueued(final Node node, int arg) {// 失败标记boolean failed = true;try {// 中断标记boolean interrupted = false;// 自旋for (;;) {// 当前节点的前一个节点final Node p = node.predecessor();// 如果当前节点的前一个节点为head节点,则说明轮到自己获取锁了// 调用ReentrantLock.FairSync.tryAcquire()方法再次尝试获取锁if (p == head && tryAcquire(arg)) {// 尝试获取锁成功// 这里同时只会有一个线程在执行,所以不需要用CAS更新// 把当前节点设置为新的头节点setHead(node);// 并把上一个节点从链表中删除p.next = null; // help GC// 未失败failed = false;return interrupted;}// 是否需要阻塞if (shouldParkAfterFailedAcquire(p, node) &&// 真正阻塞的方法parkAndCheckInterrupt())// 如果中断了interrupted = true;}} finally {// 如果失败了if (failed)// 取消获取锁cancelAcquire(node);}}// AbstractQueuedSynchronizer.shouldParkAfterFailedAcquire()// 这个方法是在上面的for()循环里面调用的// 第一次调用会把前一个节点的等待状态设置为SIGNAL,并返回false// 第二次调用才会返回trueprivate static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {// 上一个节点的等待状态// 注意Node的waitStatus字段我们在上面创建Node的时候并没有指定// 也就是说使用的是默认值0// 这里把各种等待状态再贴出来//static final int CANCELLED = 1;//static final int SIGNAL = -1;//static final int CONDITION = -2;//static final int PROPAGATE = -3;int ws = pred.waitStatus;// 如果等待状态为SIGNAL(等待唤醒),直接返回trueif (ws == Node.SIGNAL)return true;// 如果前一个节点的状态大于0,也就是已取消状态if (ws > 0) {// 把前面所有取消状态的节点都从链表中删除do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {// 如果前一个节点的状态小于等于0,则把其状态设置为等待唤醒// 这里可以简单地理解为把初始状态0设置为SIGNAL// CONDITION是条件锁的时候使用的// PROPAGATE是共享锁使用的compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}// AbstractQueuedSynchronizer.parkAndCheckInterrupt()private final boolean parkAndCheckInterrupt() {// 阻塞当前线程// 底层调用的是Unsafe的park()方法LockSupport.park(this);// 返回是否已中断return Thread.interrupted();}下面我们看一下主要方法的调用关系,可以跟着我的 → 层级在脑海中大概过一遍每个方法的主要代码:
ReentrantLock#lock()->ReentrantLock.FairSync#lock() // 公平模式获取锁->AbstractQueuedSynchronizer#acquire() // AQS的获取锁方法->ReentrantLock.FairSync#tryAcquire() // 尝试获取锁->AbstractQueuedSynchronizer#addWaiter() // 添加到队列->AbstractQueuedSynchronizer#enq() // 入队->AbstractQueuedSynchronizer#acquireQueued() // 里面有个for()循环,唤醒后再次尝试获取锁->AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire() // 检查是否要阻塞->AbstractQueuedSynchronizer#parkAndCheckInterrupt() // 真正阻塞的地方获取锁的主要过程大致如下:
(1)尝试获取锁,如果获取到了就直接返回了;
(2)尝试获取锁失败,再调用addWaiter()构建新节点并把新节点入队;
(3)然后调用acquireQueued()再次尝试获取锁,如果成功了,直接返回;
(4)如果再次失败,再调用shouldParkAfterFailedAcquire()将节点的等待状态置为等待唤醒(SIGNAL);
(5)调用parkAndCheckInterrupt()阻塞当前线程;
(6)如果被唤醒了,会继续在acquireQueued()的for()循环再次尝试获取锁,如果成功了就返回;
(7)如果不成功,再次阻塞,重复(3)(4)(5)直到成功获取到锁。
以上就是整个公平锁获取锁的过程,下面我们看看非公平锁是怎么获取锁的。
2.非公平锁
相对于公平锁,非公平锁加锁的过程主要有两点不同:
(1)一开始就尝试CAS更新状态变量state的值,如果成功了就获取到锁了;
(2)在tryAcquire()的时候没有检查是否前面有排队的线程,直接上去获取锁才不管别人有没有排队呢;
总的来说,相对于公平锁,非公平锁在一开始就多了两次直接尝试获取锁的过程。
lockInterruptibly()方法
支持线程中断,它与lock()方法的主要区别在于lockInterruptibly()获取锁的时候如果线程中断了,会抛出一个异常,而lock()不会管线程是否中断都会一直尝试获取锁,获取锁之后把自己标记为已中断,继续执行自己的逻辑,后面也会正常释放锁。
题外话:
线程中断,只是在线程上打一个中断标志,并不会对运行中的线程有什么影响,具体需要根据这个中断标志干些什么,用户自己去决定。
比如,如果用户在调用lock()获取锁后,发现线程中断了,就直接返回了,而导致没有释放锁,这也是允许的,但是会导致这个锁一直得不到释放,就出现了死锁。
lock.lock();if (Thread.currentThread().interrupted()) {return ;}lock.unlock();当然,这里只是举个例子,实际使用肯定是要把lock.lock()后面的代码都放在try...finally...里面的以保证锁始终会释放,这里主要是为了说明线程中断只是一个标志,至于要做什么完全由用户自己决定。
tryLock()方法
尝试获取一次锁,成功了就返回true,没成功就返回false,不会继续尝试。
// ReentrantLock.tryLock()public boolean tryLock() {// 直接调用Sync的nonfairTryAcquire()方法return sync.nonfairTryAcquire(1);}// ReentrantLock.Sync.nonfairTryAcquire()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) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}tryLock()方法比较简单,直接以非公平的模式去尝试获取一次锁,获取到了或者锁本来就是当前线程占有着就返回true,否则返回false。
tryLock(long time, TimeUnit unit)方法
尝试获取锁,并等待一段时间,如果在这段时间内都没有获取到锁,就返回false。
// ReentrantLock.tryLock()public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException {// 调用AQS中的方法return sync.tryAcquireNanos(1, unit.toNanos(timeout));}// AbstractQueuedSynchronizer.tryAcquireNanos()public final boolean tryAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {// 如果线程中断了,抛出异常if (Thread.interrupted())throw new InterruptedException();// 先尝试获取一次锁return tryAcquire(arg) ||doAcquireNanos(arg, nanosTimeout);}// AbstractQueuedSynchronizer.doAcquireNanos()private boolean doAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {// 如果时间已经到期了,直接返回falseif (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 GCfailed = false;return true;}nanosTimeout = deadline - System.nanoTime();// 如果到期了,就直接返回falseif (nanosTimeout <= 0L)return false;// spinForTimeoutThreshold = 1000L;// 只有到期时间大于1000纳秒,才阻塞// 小于等于1000纳秒,直接自旋解决就得了if (shouldParkAfterFailedAcquire(p, node) &&nanosTimeout > spinForTimeoutThreshold)// 阻塞一段时间LockSupport.parkNanos(this, nanosTimeout);if (Thread.interrupted())throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}}tryLock(long time, TimeUnit unit)方法在阻塞的时候加上阻塞时间,并且会随时检查是否到期,只要到期了没获取到锁就返回false。
unlock()方法
释放锁。
// java.util.concurrent.locks.ReentrantLock.unlock()public void unlock() {sync.release(1);}// java.util.concurrent.locks.AbstractQueuedSynchronizer.releasepublic final boolean release(int arg) {// 调用AQS实现类的tryRelease()方法释放锁if (tryRelease(arg)) {Node h = head;// 如果头节点不为空,且等待状态不是0,就唤醒下一个节点// 还记得waitStatus吗?// 在每个节点阻塞之前会把其上一个节点的等待状态设为SIGNAL(-1)// 所以,SIGNAL的准确理解应该是唤醒下一个等待的线程if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;}// java.util.concurrent.locks.ReentrantLock.Sync.tryReleaseprotected final boolean tryRelease(int releases) {int c = getState() - releases;// 如果当前线程不是占有着锁的线程,抛出异常if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;// 如果状态变量的值为0了,说明完全释放了锁// 这也就是为什么重入锁调用了多少次lock()就要调用多少次unlock()的原因// 如果不这样做,会导致锁不会完全释放,别的线程永远无法获取到锁if (c == 0) {free = true;// 清空占有线程setExclusiveOwnerThread(null);}// 设置状态变量的值setState(c);return free;}private void unparkSuccessor(Node node) {// 注意,这里的node是头节点// 如果头节点的等待状态小于0,就把它设置为0int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);// 头节点的下一个节点Node s = node.next;// 如果下一个节点为空,或者其等待状态大于0(实际为已取消)if (s == null || s.waitStatus > 0) {s = null;// 从尾节点向前遍历取到队列最前面的那个状态不是已取消状态的节点for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}// 如果下一个节点不为空,则唤醒它if (s != null)LockSupport.unpark(s.thread);}释放锁的过程大致为:
(1)将state的值减1;
(2)如果state减到了0,说明已经完全释放锁了,唤醒下一个等待着的节点;
彩蛋
为什么ReentrantLock默认采用的是非公平模式?
答:因为非公平模式效率比较高。
为什么非公平模式效率比较高?
答:因为非公平模式会在一开始就尝试两次获取锁,如果当时正好state的值为0,它就会成功获取到锁,少了排队导致的阻塞/唤醒过程,并且减少了线程频繁的切换带来的性能损耗。
非公平模式有什么弊端?
答:非公平模式有可能会导致一开始排队的线程一直获取不到锁,导致线程饿死。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
声明:本页内容来源网络,仅供用户参考;我单位不保证亦不表示资料全面及准确无误,也不保证亦不表示这些资料为最新信息,如因任何原因,本网内容或者用户因倚赖本网内容造成任何损失或损害,我单位将不会负任何法律责任。如涉及版权问题,请提交至online#300.cn邮箱联系删除。
java线程公平锁与非公平锁详解在ReentrantLock中很明显可以看到其中同步包括两种,分别是公平的FairSync和非公平的NonfairSync。公平
获取非公平锁(基于JDK1.7.0_40)非公平锁和公平锁在获取锁的方法上,流程是一样的;它们的区别主要表现在“尝试获取锁的机制不同”。简单点说,“公平锁”在每
释放公平锁(基于JDK1.7.0_40)1.unlock()unlock()在ReentrantLock.java中实现的,源码如下:publicvoidunl
公平锁,顾名思义,它是公平的,可以保证获取锁的线程按照先来后到的顺序,获取到锁。非公平锁,顾名思义,各个线程获取到锁的顺序,不一定和它们申请的先后顺序一致,有可
读写锁ReentrantReadWriteLock概述读写锁ReentrantReadWriteLock,使用它比ReentrantLock效率更高。读写锁表示