一篇文章彻底搞懂AQS(独占锁)、ReentrantLock“加锁”分析上(深度剖析)

本文深入解析了AQS框架及其在ReentrantLock中的应用,详细介绍了AQS的内部结构、工作原理以及ReentrantLock的加锁流程。通过源码分析,帮助读者理解公平锁与非公平锁的区别及其实现细节。


前言

什么是AQS?
AQS的实现原理是什么样?
AQS源码分析
ReentrantLock加锁流程图

想要lock加锁全流程图,请看到最后,在最下方。

传送门:AQS应用之ReentrantLock解锁分析下(源码级别、流程图)


一、 什么是AQS?

Java并发编程核心在于java.util.concurrent包,而JUC当中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS,AQS定义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器

AQS特性
阻塞等待队列,共享/独占,公平/非公平,可重入,允许中断


1、基于AQS实现一个自定义同步器一般分为2步

1、一般通过定义内部类Sync继承AQS
2、将同步器所有调用都映射到Sync对应的方法


自定义同步器实现主要实现以下几种方法:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)独占方式尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)独占方式尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

按照需要的实现即可,不是全部都要实现。
核心功能在AQS在顶层已经实现好了,直接调用即可,如具体线程等待队列的维护(如获取资源失败入队/唤醒出队等)。


2、AQS的二种队列

1、同步等待队列
也被称为CLH队列,是一种基于双向链表数据结构的队列,是FIFO先进先出的等待队列,是一种阻塞队列(如ReentrantLock就采用这种队列)。主要用于维护获取锁失败时入队的线程。

先看一下这个流程图,有个印象,后面对照源码集合看,便于理解。
在这里插入图片描述

2、条件等待队列
是一种多线程协调的通信的工具类,使得某些线程一起等待某个条件,等待条件具备时,这些线程才会被唤醒。(工具类如CountDownLatch倒计时锁就采用这种队列)。

源码的使用场景
调用await()的时候会释放锁,然后线程会加入到条件队列,调用
signal()唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁
在这里插入图片描述


3 AQS的两种资源共享方式

Exclusive-独占,只有一个线程能执行,如ReentrantLock
Share-共享多个线程可以同时执行,如Semaphore/CountDownLatch。


4 可重入

可重入是指,在线程已持有锁的情况下,无需重新去争夺锁,节约不必要的开销
AQS内部维护的核心属性,AQS就是基于这个状态属性实现的重入。
如果第一次进来直接抢锁,拿到了就设置锁线程是当前线程,并且状态为1.
第二次进来加锁,发现线程锁的线程对象就是当前线程,则直接获取到锁,状态码进行累加1.

    /**
     * 同步资源状态
     */
    private volatile int state;


简单理解就是,可能A方法内部有个加锁,然后这时候去调用B方法,B方法也有不同加锁的操作的场景。
假设线程1执行A方法已经抢到锁了,这时候在A方法里面调用B方法,B方法去加锁的时候,不需要再重新去抢锁,而是看是不是重入的,重入就直接累加1,就好了。

如果没有重入锁机制
上述场景就会出现,A方法抢到锁了,去执行B方法,B方法没抢到锁,最终就死锁了。


5 公平/非公平

说到公平、非公平,就需要拿ReentrantLock来说明了。
ReentrantLock是一种基于AQS框架实现的同步器,是JDK中的一种线程并发访问的同步手段,它的功能和synchronized类似,是一把互斥锁,可以保证线程安全。他相较于synchronized具有更多的特性,比如它支持手动加锁、解锁。支持加锁的公平性

ReentrantLock内部定义了一个Sync的内部类,该类继承AbstractQueuedSynchronized,对该抽象类的部分方法做了实现;并且还定义了两个子类
1、FairSync 公平锁的实现,按队列排序去获取锁;
2、NonfairSync 非公平锁的实现,不排队,谁抢到就是谁的。
这两个类都继承自Sync,也就是间接继承了AbstractQueuedSynchronized,所以这一个ReentrantLock同时具备公平与非公平特性

为什么要搞一个公平和非公平的概念实现?
1、公平可以保证按顺序来
2、非公平适合那些不排序的情况,并发量高的时候还可以提升并发量。

这里涉及到一个设计模式
这是加锁方法都会调用的acquire方法,里面的tryAcquire方法都是由各自的子类实现。

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

在这里插入图片描述

上面主要涉及的设计模式:模板模式-子类根据需要做具体业务实现


二、AQS、ReentrantLock源码分析

以ReentrantLock这3行代码开始拆解,,主要以讲核心AQS源码为主。

ReentrantLock lock = new ReentrantLock(true);//false为非公平锁,tru e为公平锁
lock.lock(); //加锁
lock.unlock(); //解锁

2.0、 AQS的核心内部类Node和核心属性

先大致看一下核心的属性,方便后面源码的理解。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    /**
     * Wait queue node class.

     * <pre>
     *      +------+  prev +-----+       +-----+
     * head |      | <---- |     | <---- |     |  tail
     *      +------+       +-----+       +-----+
     * </pre>
    static final class Node {
        /** 标记节点为共享模式 */
        static final Node SHARED = new Node();
        /** 标记节点为独占模式 */
        static final Node EXCLUSIVE = null;

        /** 在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待 */
        static final int CANCELLED =  1;
        /** 后继节点的线程处于等待状态,而当前的节点如果释放了同步状态或者被取消,
         *  将会通知后继节点,使后继节点的线程得以运行。 */
        static final int SIGNAL    = -1;
        /** 节点在等待队列中,节点的线程等待在Condition上,当其他线程对Condition调用了signal()方法后,
         *  该节点会从等待队列中转移到同步队列中,加入到同步状态的获取中 */
        static final int CONDITION = -2;
        /**
         * 表示下一次共享式同步状态获取将会被无条件地传播下去
         */
        static final int PROPAGATE = -3;

        /**
         * 标记当前节点的信号量状态 (1,0,-1,-2,-3)5种状态
         * 使用CAS更改状态,volatile保证线程可见性,高并发场景下,
         * 即被一个线程修改后,状态会立马让其他线程可见。
         */
        volatile int waitStatus;

        /**
         * 前驱节点,当前节点加入到同步队列中被设置
         */
        volatile Node prev;

        /**
         * 后继节点
         */
        volatile Node next;

        /**
         * 节点同步状态的线程
         */
        volatile Thread thread;

        /**
         * 等待队列中的后继节点,如果当前节点是共享的,那么这个字段是一个SHARED常量,
         * 也就是说节点类型(独占和共享)和等待队列中的后继节点共用同一个字段。
         */
        Node nextWaiter;

        /**
         * 如果节点在共享模式下等待,则返回true。
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        /**
         * 返回前驱节点
         */
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

		// 空节点,用于标记共享模式
        Node() {    // Used to establish initial head or SHARED marker
        }
	
		//用于同步队列CLH
        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }
		//用于条件队列
        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

    /**
     * 指向同步等待队列的头节点
     */
    private transient volatile Node head;

    /**
     * 指向同步等待队列的尾节点
     */
    private transient volatile Node tail;

    /**
     * 同步资源状态.
     */
    private volatile int state;
}

Node节点说明:
不管是条件队列,还是CLH等待队列
* 都是基于Node类
*
* AQS当中的同步等待队列也称CLH队列,CLH队列是Craig、Landin、Hagersten三人
* 发明的一种基于双向链表数据结构的队列,是FIFO先入先出线程等待队列,Java中的
* CLH队列是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制。
*

2.1、AQS的源码核心实现方法

lock.lock(); //加锁

这里以公平锁的实现来开始拆解内部实现,lock()就是去获取公平锁。

获取锁的三种情况:
1、如果锁没有被其他线程持有,则获取锁并立即返回,并将锁持有计数设置为1
2、如果当前线程已经持有该锁,那么持有计数将增加1,该方法将立即返回。
3、如果锁被另一个线程持有,那么当前线程就会因为线程调度的目的而被禁用,并处于休眠状态,直到获得锁,此时锁持有计数被设置为1。

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

2.2、acquire(AQS的核心方法)

	/**
     * 获取独占锁
     */
    public final void acquire(int arg) {
        //尝试获取锁
        if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//独占模式
            selfInterrupt();
    }

acquire做了3个事情,
1、tryAcquire拿到锁了就直接结束,
2、拿不到就去排队(执行addWaiter(Node.EXCLUSIVE)和acquireQueued方法)。
3、如果中断了,则


2.3、tryAcquire方法-公平锁的实现(尝试获取锁)

        /**
         * tryAcquire的公平版。除非递归调用或没有等待者或是第一个,否则不要授予访问权。
         */
        protected final boolean tryAcquire(int acquires) {
	        // 1、获取当前执行的线程对象的引用,getState()获取当前状态值。
            final Thread current = Thread.currentThread();
            int c = getState();
            // 2、如果状态值为0,并且同步队列为空,则去CAS抢锁,抢到锁了,就设置锁的线程为当前线程,并结束。
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 3、如果不=0,并且当前线程已经持有该锁,状态码累加1,更新下状态值结束。
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            // 4、以上2个条件都不满足,说明没抢到锁,直接返回false,就去排队;
            return false;
        }
    }

tryAcquire方法做了几件事情:
1、获取当前执行的线程对象的引用,getState()获取当前状态值。
2、如果状态值为0,并且同步队列为空,则去CAS抢锁,抢到锁了,就设置锁的线程为当前线程,并结束。
3、如果不=0,并且当前线程已经持有该锁,状态码累加1,更新下状态值结束。
4、以上2个条件都不满足,说明没抢到锁,直接返回false,就去排队;


2.4、addWaiter-AQS核心方法(添加新节点到队列尾部)

为当前线程和给定模式创建和排队节点。参数:mode—Node。EXCLUSIVE表示EXCLUSIVE, Node表示节点。返回:新节点

private Node addWaiter(Node mode) {
		// 1、创建新节点,新节点的下一节点为null。
        Node node = new Node(Thread.currentThread(), mode);
        // 2、获取队队列尾部节点,尾节点不为null(说明队列已初始化)
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            //  2.1、判断能不能cas添加到尾部节点,成功则直接返回。
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 3、执行enq方法,进行队列初始化
        enq(node);
        return node;
    }

主要做如下几件事情:
1、创建新节点,新节点的下一节点为null。
2、获取队队列尾部节点,尾节点不为null(说明队列已初始化)
2.1、判断能不能cas添加到尾部节点,成功则直接返回。
3、执行enq方法,进行队列初始化


2.5、enq-AQS核心方法(初始化队列,和插入队列)

将节点插入队列,必要时初始化

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // 1、获取队列尾部节点,队列尾部节点为空,则创建新节点,自旋尝试CAS添加头部节点。
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                // 2、如果队列尾部节点不为空,设置新创建的节点上一节点为队列尾部节点,并且CAS自旋尝试设置为队列尾部节点
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

做了如下几件事情:
1、获取队列尾部节点,队列尾部节点为空,则创建新节点,自旋尝试CAS添加头部节点。
2、如果队列尾部节点不为空,设置新创建的节点上一节点为队列尾部节点,并且CAS自旋尝试设置为队列尾部节点


2.6、acquireQueued-AQS核心方法(再次抢锁、抢不到堵塞当前线程)

拿不到锁的情况下,才会进入这个方法,去对当前线程以及同步队列进行处理。
以独占不可中断模式获取已在队列中的线程。用于条件等待方法以及获取方法。参数:node -节点如果在等待时被中断,则返回:true

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 1、自旋
            for (;;) {
             	// 2、获取队列原尾节点
                final Node p = node.predecessor();
                // 3、如果原尾节点是队列头部节点,并且可尝试获取锁成功,则直接更新队列头部节点指针
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    // 为了让他垃圾回收,不占用队列的空间内存
                    p.next = null; // help GC
                    failed = false;
                    // 3.1返回不中断,结束加锁方法
                    return interrupted;
                }
                // 4、判断原尾巴节点的状态,根据状态的不同,进行相应的处理,
                // 如阻塞当前节点、原尾巴节点移除队列。
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }


private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

做了如下几件事情
1、自旋
2、获取原队列尾巴节点
3、如果原尾节点是队列头部节点,并且可尝试获取锁成功,则直接更新队列头部节点指针
3.1、拿到锁了,返回不中断,结束自旋,并且结束加锁方法
4、判断原尾巴节点的状态,根据状态的不同,进行相应的处理,如阻塞当前节点、原尾巴节点移除队列。
判断原尾巴节点的状态是否为SIGNAL(等待),如果他处于等待状态,则调用parkAndCheckInterrupt())对当前线程进行堵塞。


2.7、shouldParkAfterFailedAcquire-堵塞线程,处理同步队列

主要是堵塞线程,处理同步队列的元素位置等。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * 若前驱结点的状态是SIGNAL,意味着当前结点可以被安全地park
             */
            return true;
        if (ws > 0) {
            /*
             * 前驱节点状态如果被取消状态,将被移除出队列
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * 当前驱节点waitStatus为 0 or PROPAGATE状态时
             * 将其设置为SIGNAL状态,然后当前结点才可以可以被安全地park
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

主要做了以下几件事情:
1、判断原尾巴节点是不是等待,是的话,则直接进行堵塞。
2、如果原尾巴节点被中断,则移除队列,并且调整当前节点的队列元素位置。
3、自旋,cas的进行更新原尾巴节点的状态设置为等待。


三、公平锁加锁源码的流程图

在这里插入图片描述

四、源码分析图-模拟3个线程加锁、解脱流程图文形式

4.1、线程0进来加锁逻辑

1、线程0进来由于没有线程持有锁,直接加锁成功,并且初始化队列。
在这里插入图片描述

4.2、线程1加锁逻辑

线程1进来尝试加锁,加锁失败
开始入队,尾节点为线程1,并且节点线程为线程1,
入队成功后,进行阻塞,
这里需要注意,堵塞当前节点的的前一节点状态是-1,持有锁线程为表示代表可以唤醒
在这里插入图片描述

4.3、线程2加锁逻辑

线程2加锁,加锁失败,先入队,链表尾插法,插到队尾,设置节点线程为线程2
这里需要注意,和线程1插入不同的是,会把线程1的状态也设置为-1。在这里插入图片描述

4.4、线程0释放锁

直接解锁,不需要CAS,直接设置为0,期间会判断下有没有重入锁,
然后唤醒队列前做的事情,
头部节点的状态设置为0,唤醒最靠前头部节点的节点,然后调用LockSupport.unpark(s.thread)真正唤醒。

在这里插入图片描述

4.5、线程1,加锁成功

线程1被唤醒后,进来尝试去加锁,然后加锁成功,
处理把头部节点设置为线程节点,并且把原本指向头部节点的指针去掉,持有线程也置空,
原头部节点下一节点指针也置空,最终原头部节点置空,好让垃圾回收
在这里插入图片描述


总结

AQS核心实现原理:
自旋/CAS/队列(双向链表FIFO)

传送门:AQS应用之ReentrantLock解锁分析下(源码级别、流程图)

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

未闻花名丶丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值