逐行源码分析AbstractQueuedSynchronizer(AQS)中ReentrantLock的源码实现


1 介绍

java中,经常是我们去使用的一个技术,无论是使用jdk原生的synchronized,或者是使用jdk提供的无锁机制,对于java.util.concurrent 的认识,都会有或多或少的理解。

一提到高性能的jdk包,就不得不提Doug Lea 这位超级大佬。

如果IT的历史,是以人为主体串接起来的话,那么肯定少不了Doug Lea。这个鼻梁挂着眼镜,留着德王威廉二世的胡子,脸上永远挂着谦逊腼腆笑容,服务于纽约州立大学Oswego分校计算机科学系的老大爷。

上面的这句话摘自百度百度对于Doug Lea这位老大爷的介绍,我觉得无论任何人,在去学习jdk的无锁实现的时候,都应该去搜索一下这位老大爷,是真的牛皮的啊!!

本文主要分析的是无锁机制AQS的实现,以及ReentrantLock中使用AQS实现的公平锁的源码。

2 开头

java.util.concurrent 源码包中,AbstractQueuedSynchronizer是它的基石(这个类我们也是常叫它AQS),一切所有的无锁实现,包含ReentrantLock,CountDownLatch, Semaphore 等类,都是基于AbstractQueuedSynchronizer实现的。

首先,AQS是一个抽象类,该类继承至AbstractOwnableSynchronizer并标记为可序列化的抽象类

class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable

对于AbstractOwnableSynchronizer ,这个类也常叫它AOS,其内部持有一个Thread类型的成员变量:exclusiveOwnerThread,并包含了设置和获取该成员变量的set/get 方法

exclusiveOwnerThread成员变量的含义是:持有该同步器(也就是当前AOS对象)的线程。
在这里插入图片描述

对于AQS(AbstractQueuedSynchronizer) 类而言,其内部是持有一个内部的静态类Node(这个类及其之重要),并含有当前AQS所持有的head,和tail,以及 int 类型的 state 变量(实际上他们都是volatile 类型)

    /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    private transient volatile Node head;

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
    private transient volatile Node tail;

    /**
     * The synchronization state.
     */
    private volatile int state;

这里加一句,对于无锁而言:实际上内部实现就是通过:volatile + cas(原子操作) + 自旋 进行实现的。(这个后面的代码中,我们也会在源码中分析)

AQS中的静态类Node:官方文档给他的解释为:等待队列中的节点类
在这里插入图片描述
其内部主要含有以下几个成员变量:

 		/** Marker to indicate a node is waiting in shared mode */
 		// 共享模式
        static final Node SHARED = new Node();
        /** Marker to indicate a node is waiting in exclusive mode */
        // 独占模式
        static final Node EXCLUSIVE = null;

        /** waitStatus value to indicate thread has cancelled */
        // 代表线程取消竞争这个锁
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        // 其表示当前node的后继节点对应的线程需要被唤醒
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        // 等待状态值以指示线程在条件下等待
        // 对应的条件
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;
		
		// 该表的值为上面的几种情况,不同的情况下有对应不同的结果
		volatile int waitStatus;
		
		// 当前节点的前驱节点
		volatile Node prev;
		// 当前节点对应的后驱节点
		volatile Node next;
		
		// 当前node所持有的线程对象
		volatile Thread thread;

总之,对于AQS的源码理解,你要首先从大概的知道其内部的这些成员变量,有个印象。
在这里插入图片描述

3 机制

在正式分析之前,先贴出以下几个结论,
在这里插入图片描述
这里有一些专用名词需要去解释一下(实际上你去看AbstractQueuedSynchronizer#Node类的api文档的时候介绍的很详细)
CLH:是AQS内部关于等待队列(官方文档给的是:wait queue)的实现策略,是基于CLH锁的一种变体实现。(CLH是三个发明人的名词解析,具体实现可以搜索)
等待队列:(wait queue)本文一下都是会通过等待队列这个专有名词去分析,实际上AQS的内部实现:就是以双向链表去存储当前需要竞争锁的节点(Node)所组成的等待队列,并通过LockSupport.park unpark 来暂停和唤醒指定Node节点持有的线程来实现无锁机制

4 分析

(上面的一些结论我相信你去分析完了AQS再去回头看会更加明白。)
铺垫了半天,终于要进入正题,,再说明:本文首先我们要分析的是ReentrantLock的公平锁实现

重点, 重点, 重点:大部分的源码讲解,都贴到代码的注释中,实际上对于AQS的理解,我们的大脑中要带着多线程的思维去分析各种场景,这样,你才能理解的更透

    // 内部是是通过Sync进行控制,分为公平锁和非公平锁   NonfairSync FairSync
    private static ReentrantLock lock = new ReentrantLock(true);

    public static void main(String[] args) {
        for (int i =0 ;i < 10;i++) {
            Thread thread = new Thread(() -> {

                lock.lock();
                try{
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }


            });
            thread.start();
        }
    }

首先ReentrantLock源码中对于AQS的实现是通过内部类Sync 来实现,其内部含有两个Sync的实现类:FairSync(公平锁)NonfairSync(非公平锁),对于公平锁的构造,只需要再构造ReentrantLock 实例的时候,构造函数传true即使用公平锁

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
   // 加锁操作 
    final void lock() {
        acquire(1);
     }
     // 该实现来自于AQS
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 这玩意我没看懂。。。。 有啥用
            selfInterrupt();
    }


首先分析加锁操作,公平锁下,首先是会进入到tryAcquire(args)中

        protected final boolean tryAcquire(int acquires) {
            // 获取当前的线程
            final Thread current = Thread.currentThread();
            int c = getState();
            // 当前ReentrantLock 对应的state为0 说明没有任何线程持有这个锁
            if (c == 0) {
                // hasQueuedPredecessors 对应公平锁而言,当然是要看对应链表队列是否还有线程等待这个锁,毕竟是公平的,有人已经等了那你肯定靠边
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    // 获取了锁,设置同步器的独占线程值 AOS
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                // 可重入锁 ,这个也说明ReentrantLock 多lock() 就需要多unlock()
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            // 分析一下这一步跳转的情况
            // 1, c != 0 这种情况很明显,当前锁已经被指定线程所持有
            // 2, hasQueuedPredecessors 返回了true,说明等待队列中已经有线程在等待竞锁
            // 3, compareAndSetState(0, acquires) 执行一次cas失败,说明在同一个时间片段内,刚好有一个线程修改了state的值
            // 4, c != 0 current != getExclusiveOwnerThread 有其他线程竞争了锁,很明显你不能去获取
            return false;
        }
    	public final boolean hasQueuedPredecessors() {
        	// The correctness of this depends on head being initialized
        	// before tail and on head.next being accurate if the current
        	// thread is first in queue.
        	// 检测当前AQS的等待队列中是否已存在指定的节点要去获锁
        	Node t = tail; // Read fields in reverse initialization order
        	Node h = head;
        	Node s;
        	return h != t &&
            	((s = h.next) == null || s.thread != Thread.currentThread());
    	}

假如一个线程尝试获取一下锁tryAcquire(arg) 失败了,首先进入到addWaiter(Node.EXCLUSIVE) 方法

        private Node addWaiter(Node mode) {
        	// 将当前线程封装成Node加入到CLH的tail
        	Node node = new Node(Thread.currentThread(), mode);
        	// Try the fast path of enq; backup to full enq on failure

        	Node pred = tail;
        	if (pred != null) {
            	node.prev = pred;
            	if (compareAndSetTail(pred, node)) {
                	pred.next = node;
                	return node;
            	}
        	}
        	// 这里,我们会看到这两种情况
        	// 1,当前CLH锁等待队列链表为空(没有tail 肯定是空),
        	// 2,执行一次cas compareAndSetTail失败了,也就是说,与此同时(就在同一时刻) 另一个线程抢先一步把当前CLH的锁等待队列的tail设置成了它自己,
        	//      对于第二种情况自然不用担心,因为enp(node) 方法通过自锁操作不断cas把自己封装的Node 加入到了CLH锁等待队列链表中设置成了tail
        	//      对于第一种情况,enq(node) 方法通过自锁首先设置一个虚拟节点head,然后再通过自旋不断cas把自己封装的Node 加入到了CLH锁等待队列链表中设置成了tail
        	enq(node);
        	return node;
    	}
		private Node enq(final Node node) {
        	// 自旋操作
        	for (;;) {
            	Node t = tail;
            	// 锁等待队列为空的情况下,首选需要初始化一个虚拟的节点head
            	if (t == null) { // Must initialize
                	// 其实分析cas的时候,我们要带着多线程的思维进行分析,假如在这个时候,两个线程同时对head进行赋值操作的话,会怎么样?
                	// 结果很明显,必然是有一个线程抢先完成了head的赋值,而另一个线程cas执行失败,失败的情况下,继续执行for操作,进行compareAndSetTail操作
                	if (compareAndSetHead(new Node()))
                    	// 这个结果必然是及其短暂的(或者像是一个过度)
                    	tail = head;
            	} else {
                	node.prev = t;
                	// 不必多言
                	// 因此 对应一个node而言,实际上总是会放到tail节点上
                	if (compareAndSetTail(t, node)) {
                    	t.next = node;
                    	return t;
                	}
            	}
        	}
    	}

成功追加到了CLH的锁等待队列的链表的tail之后,执行acquireQueued(Node node) 方法

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 自旋
            for (;;) {
                // 返回这个node 的prev
                final Node p = node.predecessor();
                // p == head的情况下是只能有一个,有一个线程获取到了锁,而你没有,并且当前线程初始化了CLH锁等待队列即:new Node(Head)  -> node(tail)
                // 显然p == head 的情况下,我是能够再去尝试拿一下锁的(CLH都没有人等锁了,我当然要去看一下拿锁的那个人有没有把锁释放了,我去拿)
                if (p == head && tryAcquire(arg)) {
                    // 这种情况是:那个拿锁的哥们神奇的是在我tryAcquire 的时候刚好把锁给释放了
                    // 这种情况下太秒了,那就把我的节点node设置成当前的head,然后原始的head 的next置null ,等待gc
                    setHead(node); // node -1 thread
                    p.next = null; // help GC
                    failed = false;
                    // 这种我即没有失败,也没有被LockSupport.park()
                    return interrupted;
                }
                // 有以下两种情况导致执行这个方法
                // 1,tryAcquire(arg) 没拿到锁,即别人没有锁释放掉
                // p != head,这种情况下说明CLH锁等待队列里有人等了很久了,公平起见,你是没有资格去试一下锁的
                if (shouldParkAfterFailedAcquire(p, node) &&
                        // 挂起当前线程  ,
                        // 还是要带着分析的角度来看这个执行的条件, 首先shouldParkAfterFailedAcquire(p, node) 为true
                        // 即标识当前节点的前驱节点(prev) 的waitStatus 为 -1,我需要挂起你
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            //  这个????? 内部自旋报异常抛出?
            if (failed)
                cancelAcquire(node);
        }
    }

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            // 这个的意思是当前节点node 的prev节点为SINGLA,当前节点需要被挂起
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            // 这个是关键,标识当当前线程的waitStatus被设置为了CANCLED,标识取消了进程锁之后,实际上
            // 如果一个节点置为waitStatus = CANCELLED,其对应的子节点(next节点)对应的prev节点会重新向前链接到一个不为CANCELLED 节点上去
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 毫无意思,初始情况下:其prev 的waitStatus == 0 对应等待线程池等待很久的线程,是会把其对应的前驱节点设置 Node.SIGNAL
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

上面的shouldParkAfterFailedAcquire 逻辑需要你去慢慢理解,实际上,对于一个执行了这一步的的操作,主要有以下两种情况
1,如果当前节点的node 前驱节点为head的时候,很显然,此时有一个线程持有了锁,但是它在当前线程去再试一次的时候没有释放,此时当前节点的前驱节点(pred.waitStauts) 必然是0(都没人设置它呀),所以,会进入到compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 将当前节点的前驱节点设置为 SINGNAL, 然后再自旋,再获锁(万一又没获取到),返回true
2, 如果当前节点的node前驱节点pred不是head的时候,很显然,公平起见,pred必然是会优先你去获锁的。并且如果他的前驱节点pred的waitStauts为CANCELLED 的话,其对应的子节点(next节点)对应的prev会重新向前链接到一个不为CANCELLED 节点上去,然后再自旋的将新链接的前驱节点设置SIGNAL,然后再自旋一次返回true。

因此,当当前节点的前驱节点的waitStatus为SIGNAL,意味着当前节点需要挂起

shouldParkAfterFailedAcquire返回ture之后,执行到了parkAndCheckInterrupt 方法

    private final boolean parkAndCheckInterrupt() {
        // 挂起了当前的线程,这个线程在这里阻塞了
        LockSupport.park(this);
        // 获取当前线程是否被中断,这个应该由谁来出现处理这个线程的thread.interceptor 呢?
        return Thread.interrupted();
    }

然后分析解锁操作:

    public void unlock() {
        sync.release(1);
    }
    public final boolean release(int arg) {
        // 解锁操作
        if (tryRelease(arg)) {
            Node h = head;
            // 这个写法很牛皮呀
            // 很显然,只要是处于CLH锁等待队列种的Node,如果他的waitStauts不为初始值0 的情况下,都在等锁,你调用unlock也没有意义 的啊
            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)
            // 重新将head的node waitStatus 设置为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;
        // 递归获取当前head节点
        // head 得下一个节点,判断它是不是要挂起
        // 如果下一个是null得情况下,这个还真有可能,这个说明CLH得锁等待队列都拿到锁了
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 这个代码写的很牛皮,s.waitStatus > 0 说明 CANCELLED
            // 从尾部向前递归
            // 这种情况下是不可能由s == null 触发得
            // 由s.waitStatus > 0 触发,这个条件说明node.next 已经拿到锁了
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            // 唤醒后驱节点的线程
            LockSupport.unpark(s.thread);
    }


5 带着多线程的头脑去分析

下面是一个多线程的获锁场景,我们可以尝试分析。更加加深AQS的获锁逻辑

/**
 * 这样分析 1,假设一个线程无限长的lock持有了这个锁
 *         2,此使一个线程A试图lock这个锁,此使同步器中的CLH的锁等待链路实际上是
 *              new Node(waitStatus = 0) -> Node(Current Thread a, Node.EXCLUSIVE[NULL], waitStatus = 0)
 * 那么,我们分析此使线程A是会停落再哪呢?
 *         #parkAndCheckInterrupt() 这个方法中 内部调用了LockSupport.park(this)
 *         3,此使另一个线程B也试图lock这个锁(先不考虑线程几乎同时的情况),此时同步器中的CLH的锁等待链路实际上是:
 *              new Node(waitStatus = -1) -> Node(Current Thread A, Node.EXCLUSIVE[NULL], waitStatus = 0) -> Node(Current Thread B ,Node.EXCLUSIVE[NULL], waitStatus = 0)
 * 那么,我们再分析此时线程B会停落再哪呢?
 *         #parkAndCheckInterrupt() 这个方法中 内部调用了LockSupport.park(this),毫无疑问,但此时,同步器中的CLH的锁等待链路变成了
 *              new Node(waitStatus = -1) -> Node(Current Thread A, Node.EXCLUSIVE[NULL], waitStatus = -1) -> Node(Current Thread B ,Node.EXCLUSIVE[NULL], waitStatus = 0)
 *
 */
6 非公平锁

故名思意,非公平锁就是每一个线程进来获锁的时候,我都要尝试一下进行cas操作去判断我是否获取成功,如果成功了,更好,不成功了之后,我再去加入到CLH的锁等待队列里,然后锁被释放了之后,唤醒线程之后,我再去cas判断一下是否获锁成功过(注意此时是不会判断锁等待队列中是否有已经再等待的节点,直接去cas)。

        // 1,非公平锁很明显,每一次获取之前都要去竞争一下锁,如果这个时候刚好竞争到锁了之后,那就直接拿锁
        // 2,如果没有拿到锁之后,和公平锁一致,调用acquire(1)方法,但是会进入到非公平锁的tryAcquire() 方法,再试一下
        //      对于非公平锁而言,它不会去看CLH锁等待队列种是否有节点等待,而是直接再去cas获取锁,
        // 实际上对于公平锁和非公平锁的区别,就是这两点,一个是lock() 方法的区别 另一个是tryAcquire() 方法的区别
        // 相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

		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;
        }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值