聊聊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实现原理的主要内容,如果未能解决你的问题,请参考以下文章

ReentrantLock实现原理深入探究

多线程(十AQS原理-ReentrantLock实现)

ReentrantLock实现原理-何为可重入

java并发之ReentrantLock.Condition实现原理

ReentrantLock实现原理

打通JAVA与内核系列之一ReentrantLock锁的实现原理