并发编程(学习笔记-共享模型之JUC-ReentrantLock原理)-part6

Posted LL.LEBRON

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程(学习笔记-共享模型之JUC-ReentrantLock原理)-part6相关的知识,希望对你有一定的参考价值。


本文章视频指路👉 黑马程序员-并发编程

ReentrantLock原理

1.非公平锁实现原理

先从构造器开始看,默认为非公平锁实现:

//默认非公平锁
public ReentrantLock() 
    sync = new NonfairSync();

NonfairSync 继承自 AQS

没有竞争时:

第一个竞争出现时:

Thread-1 执行了:

  1. CAS 尝试将 state 由 0 改为 1,结果失败
  2. 进入 tryAcquire 逻辑,这时 state 已经是1,结果仍然失败
  3. 接下来进入 addWaiter 逻辑,构造 Node 队列
    • 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态
    • Node 的创建是懒惰的
    • 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程

当前线程进入 acquireQueued 逻辑:

  1. acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞

  2. 如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败

  3. 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,这次返回 false

  4. shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败

  5. 当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回 true

  6. 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)

再次有多个线程经历上述过程竞争失败,变成这个样子:

Thread-0 释放锁,进入 tryRelease 流程,如果成功:

  • 设置 exclusiveOwnerThread 为 null
  • state = 0

当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程。

找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1

回到 Thread-1 的 acquireQueued 流程

如果加锁成功(没有竞争),会设置:

  • exclusiveOwnerThread 为 Thread-1,state = 1
  • head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread
  • 原本的 head 因为从链表断开,而可被垃圾回收

如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了

如果不巧又被 Thread-4 占了先

  • Thread-4 被设置为 exclusiveOwnerThread,state = 1
  • Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞

2.可重入原理

这里以非公平锁为例:

获取锁:

// ReentrantLock.Sync.nonfairTryAcquire()
final boolean nonfairTryAcquire(int acquires) 
    //获取当前线程
    final Thread current = Thread.currentThread();
    //获取状态值
    int c = getState();
    if (c == 0) 
        //如果状态变量为0,再次尝试CAS更新状态变量的值
        //相对于公平锁模式少了!hasQueuedPredecessors()条件
        if (compareAndSetState(0, acquires)) 
            setExclusiveOwnerThread(current);
            return true;
        
    
    // 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入
    else if (current == getExclusiveOwnerThread()) 
        //state++
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    
    return false;

释放锁:

//ReentrantLock.Sync.tryRelease
protected final boolean tryRelease(int releases) 
    //state--
    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;

可以看出源码中通过state变量的计数来实现可重入,只有当state==0时才说明已经不持有锁。

3.可打断原理

3.1 不可打断模式

在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了。

//AbstractQueuedSynchronizer.acquireQueued()
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())
                // 如果是因为 interrupt 被唤醒, 返回打断状态为 true
                interrupted = true;
        
     finally 
        //如果失败了
        if (failed)
            cancelAcquire(node);
    

//AbstractQueuedSynchronizer.parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() 
    //如果打断标记已经是 true, 则 park 会失效
    LockSupport.park(this);
    //interrupted 会清除打断标记
    //清除打断标记,下次park就不会受到影响
    return Thread.interrupted();

public final void acquire(int arg) 
    if (
        !tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
    ) 
        // 如果打断状态为 true,acquireQueued方法就会返回成功。就会执行这个方法
        selfInterrupt();
    

static void selfInterrupt() 
    // 重新产生一次中断
    Thread.currentThread().interrupt();

3.2 可打断模式

与不可打断模式不同的是,当被interrupt时是会抛出异常的,不会继续执行for循环。此时等待的线程可以停止等待。

//ReentrantLock.lockInterruptibly()
public void lockInterruptibly() throws InterruptedException 
    sync.acquireInterruptibly(1);

//AbstractQueuedSynchronizer.acquireInterruptibly(int arg)
public final void acquireInterruptibly(int arg)
        throws InterruptedException 
    if (Thread.interrupted())
        throw new InterruptedException();
    //如果没有获得到锁,进入doAcquireInterruptibly方法
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);

private void doAcquireInterruptibly(int arg)
        throws InterruptedException 
    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;
            
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                //在park过程中如果被interrupt会进入这里
                //这时候抛出一次,而不会再次进入for循环
                throw new InterruptedException();
        
     finally 
        if (failed)
            cancelAcquire(node);
    

4.公平锁实现原理

//ReentrantLock.FairSync.lock()
final void lock() 
    //调用AQS的acquire()方法获取锁
    //注意,这里传的值为1
    acquire(1);

//AbstractQueuedSynchronizer.acquire()
public final void acquire(int arg) 
    //先尝试加锁
    //如果失败了,就排队
    if (!tryAcquire(arg) &&
            //注意addWaiter()这里传入的节点模式为“独占模式”
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();

//ReentrantLock.FairSync.tryAcquire()
//与非公平锁主要区别在于 tryAcquire 方法的实现
protected final boolean tryAcquire(int acquires) 
    //获取当前线程
    final Thread current = Thread.currentThread();
    //获取当前状态变量的值
    int c = getState();
    //如果为0,说明现在还没有人占有锁
    if (c == 0) 
        //先检查 AQS 队列中是否有前驱节点, 没有才去竞争
        //如果没有其他线程在排队,那么当前线程尝试更新state的值为1
        //如果成功了,说明当前线程获取了锁
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) 
            //当前线程获取了锁,把自己设置到exclusiveOwnerThread中
            //exclusiveOwnerThread是AQS的父类AbstractOwnableSynchronizer中提供的变量
            setExclusiveOwnerThread(current);
            //返回true,说明成功获得了锁
            return true;
        
    
    //如果当前线程本身就占有锁,现在又尝试获取锁
    //那么,直接让他获取锁,并返回true
    else if (current == getExclusiveOwnerThread()) 
        //状态变量state的值加一
        int nextc = c + acquires;
        //如果发送一出,则报错
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        //这里为什么不需要CAS更新state?
        //因为当前线程占有锁,其他线程只会CAS把state从0更新到1,是不会成功的
        //所以不存在竞争,自然不需要使用CAS来更新
        setState(nextc);
        //当线程获取锁成功
        return true;
    
    //当线程获取锁失败
    return false;

public final boolean hasQueuedPredecessors() 
    //尾部
    Node t = tail;
    //头部
    Node h = head;
    Node s;
    // h != t 时表示队列中有 Node
    return h != t &&
            // (s = h.next) == null 表示队列中还有没有老二
            ((s = h.ext) == null ||
                    // 或者队列中老二线程不是此线程
       
                    s.thread != Thread.currentThread());

5.条件变量实现原理

每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject

5.1 await 流程

开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程

创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部

接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁

unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功

park 阻塞 Thread-0

5.2 signal 流程

假设 Thread-1 调用signal要来唤醒 Thread-0

进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node

执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的 waitStatus 改为 -1

waitStatus == 0 默认状态,waitStatus == -1 表示当前node如果是head节点时,释放锁之后需要唤醒它的后继节点

Thread-1 释放锁,进入 unlock 流程,略

以上是关于并发编程(学习笔记-共享模型之JUC-ReentrantLock原理)-part6的主要内容,如果未能解决你的问题,请参考以下文章

并发编程(学习笔记-共享模型之管程)-part3

并发编程(学习笔记-共享模型之JUC-ReentrantLock原理)-part6

并发编程(学习笔记-共享模型之JUC-读写锁原理)-part6

Java高级工程师进阶学习:kafka应用场景

volatile 学习笔记

并发编程——共享模型之管程