深入理解重入锁

本篇的内容介绍

​ 本篇文章会详细分析reentrantlock的工作原理,会分为三部分讲解,第一部分是 reentrantlock 大体上的脉络,第二部分是分析公平锁的实现,第三部分再简单对比非公平锁的实现


第一部分 reentrantlock 的一个大概设计

我们简单的看一下reentrantlock 的一个大的设计是怎么样的,我们简单用一个伪代码表示

public class ReentrantLock {
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    class Sync extends AbstractQueuedSynchronizer{
        abstract void lock(); 
        // 下面是其他一些公共实现
    }
    class NonfairSync extends Sync{
        void lock() {
            //实现
        }
    }
    class FairSync extends Sync{
        void lock() {
           //实现
        }
    }
    public void lock() {
        sync.lock();
    }
}

总结:
1 从内部类的结构可以比较清晰的看出来,ReentrantLock 分为 NonfairSync(非公平锁) 和 FairSync(公平锁), 都是依赖与 AQS 同步器实现的。

2 默认为非公平锁

3 初始化后 ReentrantLock 持有Sync实例,ReentrantLock 大部分实现,是调用Sync方法实现的


第二部分 公平锁的实现原理

一 我们先看看公平锁,加锁过程

在这里插入图片描述

步骤总结:

1 先尝试获取锁,如果获取锁成功,则加锁成功

2 如果获取锁失败,则走AQS 的入队流程,下面详细会讲


加锁过程源码分析:

公平的 lock实现

 final void lock() {
     acquire(1); 
 }

lock 会调用 AQS(AbstractQueuedSynchronizer) 的 acquire方法


  public final void acquire(int arg) {
        if (!tryAcquire(arg) && // 尝试获取锁,这个一般由父类提供
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 尝试获取锁失败,会进入AQS队列中参与排队
            selfInterrupt(); 
    }

总结:
1 该方法是先尝试获取锁,如果成功则加锁成功

2 如果失败则 调用 addWaiter 方法入队,然后调用 acquireQueued 方法 判断是否需要让出CPU,下面我们一步步的看


 protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() && // 判读是否需要排队
                    compareAndSetState(0, acquires)) { // 如果不需要排队则 尝试获取锁
                    setExclusiveOwnerThread(current); //将当前线程设置为将拥有锁的线程
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {  //锁重入,直接返回true
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

总结:
1 getState()==0, 先判断锁是否已经被释放了

2 如果锁已经被释放了,判断是否需要排队,如果需要排队,则返回false,否则尝试加锁

3 如果锁还未被释放,判断当前线程是否为当前拥有锁的线程,如果是,则返回true,说明是锁重入了

4 其他情况即是锁即没被释放,又不是锁重入,则返回false,尝试加锁不成功


 public final boolean hasQueuedPredecessors() {
        Node t = tail; 
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

总结:
1 这是判断是否需要排队的方法,如果要则返回true,否则返回false上面代码,可以分为几种情况讨论,我们使用如下代码表示


/*
什么情况下  AQS 队列  head != tail
第一种是 队列还没有初始化的时候, 此时 head  和 tail 都为 null。
第二种是 已经初始化了队列,但是此时队列没有任何等待线程
*/
if((h!=t) == false){
    return false;
}

/*
要使 ((s = h.next) == null || s.thread != Thread.currentThread())  ====> 返回false 那么
(s = h.next) == null   和  s.thread != Thread.currentThread() 都需要同时返回 false

什么情况下 (s = h.next) == null  且 s.thread != Thread.currentThread() 都为 false 呢?

1 (s = h.next) == null 为false ,就是队列不为空,且至少又一个元素
2  s.thread != Thread.currentThread()  ===为false , 当前线程就是第一个等待元素,意思就是该线程是队列中的除head的第一个元素,不需要排队
总结:如果队列至少有一个元素,且当前线程就是第一个等待元素,则不需要排队

什么情况下下面条件会成立呢?
如果是该元素就是第一个等待元素,那么就是以下场景===》尝试获取锁失败时,调用addWaiter加入队列,如果是队列中的除head的第一个元素,
那么acquireQueued 会再次调用 tryAcquire 获取锁,此时 该元素就是第一个等待元素 这个条件就会满足,此处可以看完下面再倒回来看
*/
if((h!=t) == true &&  ((s = h.next) == null || s.thread != Thread.currentThread()) ==false){
    return false;
}
        
/*
要让整个表达式为 true,必须是 
h!=t 和   ((s = h.next) == null || s.thread != Thread.currentThread())  同时为 true

((s = h.next) == null || s.thread != Thread.currentThread())  为true 分为两种情况,
(s = h.next) == null  为 true 或 s.thread != Thread.currentThread() 为true

如果 h!=t 为true 那么 (s = h.next) == null 必然为false, 那么使条件成立,则
h!=t 为 true 
s.thread != Thread.currentThread() 为 true
(s = h.next) == null 为 false

这种情况也就是说,队列不为空,且当前 线程,不是第一个非head 元素。
*/
if(h!=t == true &&  ((s = h.next) == null || s.thread != Thread.currentThread()) ==true){
    return true;
}

总结:

1 如果队列没有被初始化或者队列没有任何元素,则不需要排队

2 如果队列至少有一个元素,且当前线程就是第一个等待元素,则不需要排队,这种场景一般发生在,刚入队完,判断是否需要让出CPU的时候发生

3 队列不为空,且当前 线程,不是第一个非head 元素,则需要排队


    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // 如果队列不为空,先尝试一次入队,如果入队不成功,会进入 enq 方法。
        // 如果有竞争的情况下,A,B线程 只能有一个成功,所以会有入队失败的情况
        Node pred = tail;
        if (pred != null) {
            // 下面就是维护队列的元素关系,这里就不细说了
            node.prev = pred;
            if (compareAndSetTail(pred, node)) { 
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

总结:
1 这是队列节点入队操作,如果队列不为空,则会尝试直接做入队操作

2 如果队列为空或者入队失败,则进入 enq 方法进行操作


    private Node enq(final Node node) {
        for (;;) {  // 多线程竞争的情况下,可以不断尝试入队
            Node t = tail;
            if (t == null) { // 队列为空,则进行队列初始化操作
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {  // 进行入队操作
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

总结:
情景一:A,B两个线程竞争入队,且队列没有被初始化。

我们一起来缕一缕这个逻辑:

1 第一次循环,因为队列为空,A,B线程同时进入 t == null 这个条件,此时无论谁 compareAndSetHead 成功,队列都会被初始化,进入第二次循环.

2 第二次循环,因为队列已经被初始化了,所以会进入条件2,如果A 成功入队,即 compareAndSetTail 成功,则A直接返回。在竞争的情况下,A成功,则B就入队失败,B会进入第三次循环。

3 第三次循环,没有线程与B竞争,则B线程入队成功。



 final boolean acquireQueued(final Node node, int arg) {  // 判断是否需要让出CPU
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {  // 如果该元素是非head得首个元素,那么可以有资格尝试 获取锁
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&  // 判断是否需要让出cpu
                    parkAndCheckInterrupt()) // 如果需要则调用  LockSupport.park(this) 让出cpu
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

总结:
1 判断是否需要让出CPU,如果当前线程是队列的非head得首个元素,那么可以有资格尝试 获取锁

2 否则调用 shouldParkAfterFailedAcquire 方法判断是否需要让出CPU,我们结合 shouldParkAfterFailedAcquire 一起看



    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL) //如果前一个结点时 SIGNAL 状态,该节点则可以让出CPU
            return true;
        if (ws > 0) { 
            // 如果前一个节点的状态为 cancelled,则跳过前一个节点,将前一个节点设置为前面
            //最靠近当前节点的 signal状态的节点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 前一个节点的等待状态为 null 或 PROPAGATE,此时将前一个节点的状态设置为 SIGNAL
            // 进入第二遍循环
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

总结:
1 如果前一个节点是 SIGNAL,则可以让出CPU


这里有一个场景,因为独占锁刚加入节点,waitStatus都是null

所以通常进入 acquireQueued,会进行两次循环。

1 第一次 循环 将 前一个节点 的 waitStatus 设置为 SIGNAL

2 第二次 循环 shouldParkAfterFailedAcquire 就可以返回true了,因为第一次循环已经将前一个节点设置为 SIGNAL


换句话说,在acquireQueued里,如果该元素是非head得首个元素,那么可以有两次资格尝试 获取锁

公平锁,加锁过程就说到这里,下面我们来看看公平锁,解锁过程




二 我们再看看公平锁,解锁过程

   public final boolean release(int arg) {
        if (tryRelease(arg)) { // 尝试释放锁,一般由父类提供
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);  //重队列中唤醒符合条件的线程,我们下面来分析
            return true;
        }
        return false;
    }

总结:
1 tryRelease 是产生释放锁,这一般由父类提供

2 释放锁成功后,会唤醒队列中一个符合条件的线程



     protected final boolean tryRelease(int releases) {
            int c = getState() - releases; //state大于一的情况下,为锁重入了,所以state减至为0,则锁释放
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

总结:
1 state 减至0则 释放锁
2 释放成功,设置占有锁的线程


  private void unparkSuccessor(Node node) { // 唤醒后继线程
        /*
       将node 的 waitStatus设置为0,这样再次进入 acquireQueued 可以多一次获取锁的机会,可以回头看看acquireQueued的实现
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
       如果node.next 不为null 且 waitStatus 不为CANCELLED 状态,则唤醒线程,
       否则,从tail 往前开始找到一个符合条件的节点
         */
        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); // 唤醒线程
    }

总结:
1 将node 的 waitStatus重置为0。这样非head的头节点,就会有再多一次的获取锁的机会

2 获取头节点的下一个节点,如果不满足条件(为空或则waitStatus为CANCELLED 状态),则从tail 往前开始找到一个符合条件的节点

3 如果找到符合条件的节点,则唤醒线程




第二部分,简单对比非公平锁的实现

我们来看个图

在这里插入图片描述

 final void lock() {
            if (compareAndSetState(0, 1)) // 先尝试获取锁
                setExclusiveOwnerThread(Thread.currentThread()); // 设置该线程为当前拥有锁的线程
            else
                acquire(1); // 如果获取锁失败,走AQS的实现
        }

总结:
非公平锁的lock实现

1 先cas尝试获取一次锁

2 获取锁失败则走AQS实现



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

1 如果当前锁已经释放,不判断是否需要排队,直接cas 获取一次锁

2 其他跟公平锁差不多



这里总结一个非公平锁和公平锁的一些区别:

1 非公平锁在 lock 的时候,直接cas 一次尝试获取锁,公平锁则 直接走 AQS的aquire

2 非公平锁TryAcquire 的时候,直接cas 一次尝试获取锁,公平锁则判断是否需要排队,不需要排队才尝试获取锁




参考资源:

https://blog.csdn.net/fuyuwei2015/article/details/83719444

https://blog.csdn.net/java_lyvee/article/details/98966684

https://www.cnblogs.com/micrari/p/6937995.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值