聊聊ReentrantLock实现原理
Posted huxuhong
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊聊ReentrantLock实现原理相关的知识,希望对你有一定的参考价值。
ReentrantLock 是常用的锁,相对于Synchronized ,lock锁更人性化,阅读性更强
从LOCK切入
考虑下面的场景如果有A,B线程,同时去执行lock.lock(Lock lock = new ReentrantLock 为全局属性),当A线程抢到锁以后,此时B线程做了哪些事情
要知道B线程做了那些事情,就要知道一个类AbstractQueueSynchronizer(简称AQS),它负责将锁竞争失败线程存储起来
Lock lock = new ReentrantLock(); try{ lock.lock(); System.out.println("测试"); }finally { lock.unlock(); }
AQS 分析
要理解锁的运行原理,至少要了解下面的几个问题
①,如何锁定(或者说锁定的标准是什么)
②,竞争锁失败,怎么处理
③,如何释放锁
④,如何唤醒其他线程重新竞争
针对上面的4个问题,分析一下ReenTrantLock
如何锁定
//当执行lock.lock()竞争锁 final void lock() {
//1,A,B线程竞争ReenTrantLock->AQS属性->state属性
//2,乐观锁的形式更新state,如A线程先将state更新为1,就代表A线程竞争锁成功
if (compareAndSetState(0, 1))
//设置锁持有线程 setExclusiveOwnerThread(Thread.currentThread()); else
//B线程竞争锁失败,执行下面方法 acquire(1); }
要理解AQS的原理中,理解state的状态值变化很重要
state=0 ,node节点创建时state=0
state=-1,后继线程可以考虑阻塞啦,因为后继线程不是下一个需要执行竞争锁的(在添加一个新的节点后,那么的它的pre节点的state会变成-1)
state = 1,节点失效,如:当tryLock(1000,TimeUnit.MILLISECOND) 超时时,节点状态值
B线程竞争锁失败,处理方式
1,尝试重新获取锁(目的)
1.1,看看前面锁是否释放(先下手为强,假如成功尼,哈哈,满满的求生欲)
1.2,查看是否同一个线程竞争锁,考虑重入锁
2,仍然失败,将B线程封装一个新的node节点,插入一个双向队列的尾部
3,开始自旋(死循环),直到获取锁为止
3.1,判断当前线程是否有资格竞争锁
3.2,无资格或者竞争失败的情况下,考虑是否暂停B线程线程(A线程释放锁的时候,会唤醒B线程)
从源码角度,来解析一下上面的内容
public final void acquire(int arg) { //尝试获取锁,失败,考虑可重入锁 if (!tryAcquire(arg) && //addWaiter 添加到双向队列的尾结点 //acquireQueued,开始自旋(死循环) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //如果当前线程在竞争过程中存在中断情况,设置当前线程中断 selfInterrupt(); } 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) { 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; } final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor();
//判断当前这个节点前节点是否是head节点,是的话,重新进行竞争操作(此时A线程可能还木有释放锁) if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; }
//如果失败或者前节点不是head节点,根据实际情况,考虑一下暂停当前线程 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
背景:A线程竞争锁成功,B线程竞争锁失败,AQS队列为空,此时B线程准备加入队列
1,首先添加一个空节点,head,tail 链接到此节点
2,B线程节点,加入AQS队列,A线程释放锁的时候,直接唤醒B线程竞争锁,竞争成功,队列前进一位即可
A线程释放锁
1,做减法--->AQS属性state一直被减到0(考虑可重入锁),代表这个A线程彻底释放锁
2,在双向队列中,查找下一个节点,唤醒这个节点的线程
如果A线程直接抢到锁啦说明A线程所在的节点并木有添加到双向队列中,那么下一个节点就是双向队列的头节点
如果A线程所在的节点加入了双向队列,那么head节点为A线程所在的节点,下一个节点为head节点的下一个节点
public void unlock() { sync.release(1); }
//做减法--->AQS属性state一直被减到0(考虑可重入锁),代表这个A线程彻底释放锁 protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; } public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */
//在双向队列中,查找下一个节点,唤醒这个节点的线程
Node s = node.next; 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); }
以上是关于聊聊ReentrantLock实现原理的主要内容,如果未能解决你的问题,请参考以下文章