一篇文章带你了解AQS独占锁实现

1 AQS概述

  Java中的大部分同步类(ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore、CyclicBarrier等)都是基于AQS(AbstractQueuedSynchronizer)实现的。AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。AQS内部的同步队列为CLH变体的虚拟双向队列FIFO,依靠单个原子int值来表示状态,通过占用和释放方法改变状态值

  一般来说,自定义同步器要么是独占方式,要么是共享方式

  • 独占锁:实现tryAcquire-tryRelease,如ReentrantLock
  • 共享锁:实现tryAcquireShared-tryReleaseShared,如CountDownLatch
  • 独占锁&共享锁:AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock

2 内部结构

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    static final class Node {
        // 当前节点在队列中的状态:1:取消;0:初始化;-1:释放锁后需要唤醒后继节点;-2:condition等待被唤醒;-3:当前线程处在SHARED情况下,该字段才会使用,共享锁状态获取将会无条件传播下去
        volatile int waitStatus;
        // 前驱节点
        volatile Node prev;
        // 后继节点
        volatile Node next;
        // 当前节点的线程
        volatile Thread thread;
        // condition队列链接到下一个节点(这个是单向队列),或共享模式下标识下一个节点为Share(传播性),独占模式这个值为null
        Node nextWaiter;
    }

    // AQS头节点引用
    private transient volatile Node head;
    // AQS尾节点引用
    private transient volatile Node tail;
    // AQS状态:独占模式下state=0表示可获取状态,state>0表示锁被占用状态;共享模式下state>0表示后续还可以获取到锁,state=0表示当前可以获取到锁后续无法再获取到锁,state<0表示共享模式下已经无法获取到锁
    private volatile int state;
    
    /** 通过volatile保证了state的可见性,通过CAS保证了state操作的原子性 */
    protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
}

3 从ReentrantLock看AQS独占锁获取实现

1)调试入口
public static void main(String[] args) throws InterruptedException {

    ReentrantLock reentrantLock = new ReentrantLock();
    Condition condition = reentrantLock.newCondition();

    new Thread(() -> {
        reentrantLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "等待唤醒");
            condition.await();
            System.out.println(Thread.currentThread().getName() + "被唤醒");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();
        }
    }, "t1").start();

    TimeUnit.SECONDS.sleep(1);

    new Thread(() -> {
        reentrantLock.lock();
        try {
            condition.signal();
            System.out.println(Thread.currentThread().getName() + "唤醒别的线程");
        } finally {
            reentrantLock.unlock();
        }
    }, "t2").start();
}
2)lock()

  ReentrantLock内部聚合了一个AbstractQueuedSynchronizer对象sync,通过ReentrantLock的构造方法可以指定当前lock为公平锁还是非公平锁

final void lock() {
    //非公平锁的lock方法比公平锁的lock方法多了一个CAS操作
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
3) acquire(int arg)

  ReentrantLock内部调用AQS模板提供的acquire()方法,此方法也是AQS模板中Lock()方法的实现,该方法里核心调用了三个方法:tryAcquire、addWaiter,acquireQueued

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

  AbstractQueuedSynchronizer#tryAcquire是一个需要子类实现的方法,AQS模板中使用此方法作为tryLock()的实现

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

  ReentrantLock.FairSync#tryAcquire是公平锁的实现,比非公平锁多了hasQueuedPredecessors方法,此方法用来判断AQS队列中是否已经有等待的线程(头节点的下一个节点是否存在),如果存在则公平模式下无需竞争直接返回false调用addWaiter进入同步队列

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //获取当前同步状态值,ReentrantLock使用state==0表示可获取锁的状态,state==1表示当前锁被占用状态
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            //当前获取锁成功将state置为1,设置AQS锁独占线程为当前线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //若当前线程就是AQS中持有锁的线程,这是ReentrantLock支持可重入的实现,使state+1
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

  hasQueuedPredecessors:返回当前AQS中同步队列是否已经有等待时间最长的节点,即头节点下一个等待被唤醒的节点,若存在返回true反则false

public final boolean hasQueuedPredecessors() {
    Node t = tail; 
    Node h = head;
    Node s;
    //h!=t:空队列/刚初始化完成的时候为false
    //h.next == null:h!=t成立得到队列长度>1,h.next == null肯定不是null则条件为false;但是有一种情况,enq()方法中初始化完成后有一个虚拟头节点,node节点进先设置前驱指针再CAS成功替换了tail,还没来得及设置头节点的后继节点,这时候h!=t且h.next==null成立,即队列中已经有等待的节点了
    //s.thread != Thread.currentThread():如果队列中已经有很多节点,判断等待时间最长的那个节点线程是否为当前线程
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
5)addWaiter(Node mode)

  AbstractQueuedSynchronizer#addWaiter方法是AQS模板的内部私有方法,通过tryAcquire方法获取锁失败的线程都会通过addWaiter方法进入AQS同步队列中,此方法包括了AQS同步队列的创建及初始化(enq方法)、线程对应节点的创建及入队、AQS同步队列头尾指针的维护

  在addWaiter和enq中提了两个很重要、很重要、很重要的问题,看完流程后给出答案

/**
根据给定的模式(独占/共享)为当前线程创建对应的节点并加入到AQS同步队列中
*/
private Node addWaiter(Node mode) {
    //独占模式下mode=null
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    //AQS的尾节点不为空,先设置node的前驱节点,再CAS替换AQS尾节点成功后再设置之前的尾节点指向node
    //这里有一个很重要的问题:为什么要先设置node节点的前驱节点,再CAS替换尾节点,最后才设置pred的后继节点?这里的顺序能变换吗?
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //如果队列未初始化会通过enq方法进行队列的初始化并完成入队
    enq(node);
    return node;
}
/**
enq方法node节点的入队操作和addWaiter的方式一样,只是多了队列的初始化,这里也有一个很重要的问题:为什么要设置一个空的头节点,而不是把当前的node节点作为头节点?
*/
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
6)acquireQueued(final Node node, int arg)

  通过上面的addWaiter方法使当前线程入队成功后,就会进入acquireQueued方法,这个方法贼难理解,也是贼核心的一个方法!首先看这个方法先说明几个问题:

  • 这个方法是怎样修改节点状态的?
  • 是怎样实现可能存在的频繁阻塞和唤醒的?
  • 获取锁成功后怎样出队的?
  • acquire方法使用的注释说明了是不响应中断的,怎样做到的?
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        // 局部变量记录中断标志位,因为acquire注释中说明了此过程是不响应中断的,所以需要记录这个状态过程中的中断标志位,若获取锁过程中当前线程的中断标志位变为true则需要在acquire执行完成前通过这个局部变量返回值最后设置回来,总结:interrupted这个局部变量实现了acquire方法的不响应中断
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            // 判断node节点的前驱节点是否为头节点,且其实现类的tryAcquire方法是否获取锁成功,若均为true则当前node节点获取到AQS的锁(在tryAcquire方法中成功修改了state值和AQS中独占线程为node的线程),此时node节点需要出队并重新设置虚拟头节点(head = node;node.thread = null;node.prev = null;)
        node.thread = null;
        node.prev = null;if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 获取锁失败,判断是否需要阻塞(挂起)当前线程,需要挂起的话会把前驱节点p的waitStatus(ws)设置为-1而不是把当前节点node的ws设置为-1,也就是上面说内部结构的时候说的waitStatus=-1时表示释放锁后需要唤醒后继节点,即node节点自行挂起后需要p(ws=-1)节点释放锁时唤醒自己。shouldParkAfterFailedAcquire这个方法没啥好看的,就是node需要找到一个稳定的前驱节点p(ws<=0)为止,然后将其状态设置为-1(若已经为-1则不用设置直接返回true)
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

  parkAndCheckInterrupt方法是通过LockSupport.park将当前线程挂起,阅读该方法注释可知程序会在下面三种情况下向下执行

  • 被unpark
  • 被interrupt
  • 其它不合逻辑的返回

  所以当被interrupt=true导致被唤醒,因为acquire方法是不响应中断的,所以返回中断标志为true赋值给外面的interrupted局部变量,并清除中断标志位(置为false),再次进入获取锁/挂起的状态!

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

4 从ReentrantLock看AQS独占锁释放实现

1)unlock()

  能够进入到unLock方法,说明了两个问题:1、当前线程已经完成了业务操作/异常终止或其它原因进入finally;2、当前线程是AQS中获取到锁的线程,最后要进行释放锁操作

public void unlock() {
    sync.release(1);
}
2)release(int arg)
public final boolean release(int arg) {
    // tryRelease方法是AQS模板要求子类实现的方法,若成功释放锁则返回true反则返回false
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            // 当前线程成功释放锁后,因为之前说了ws=-1需要唤醒后续节点线程,unparkSuccessor方法就是唤醒后继节点的方法
            unparkSuccessor(h);
        return true;
    }
    return false;
}
3)tryRelease(int arg)

  tryRelease和tryAcquire一样都是AQS模板中要求子类自行实现的,只是tryAcquire是用来获取锁修改AQS的state状态为占用,设置AQS的独占线程为当前线程;而tryRelease刚好相反,用来释放锁修改AQS的state状态为空闲,设置AQS的独占线程为null

protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 当前进入的线程不是AQS中独占线程的拥有者就会抛出IllegalMonitorStateException,这里很有意思:1、不是AQS的独占线程拥有者当然会抛出这个异常,2、还有一个抛出这个异常的是Condition对象调用await方法挂起的时候也会进入这里释放锁,这就从Java代码上看到了synchronized的两个问题:第一个问题是使用wait和notify方法必须在同步代码块中,等同与Condition的await方法和signal方法必须在lock和unlock中,不然都会抛出IllegalMonitorStateException,第二个问题是他们都是使用相同的同步监视器如synchronized(obj),等同于一个ReentraLock.lock(),所以我们看不懂synchronized的底层实现可以看ReentrantLock和Condition的底层实现啊!
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 如果AQS的状态值为0则释放锁:把AQS的state设置为0,独占线程拥有者设置为null,这里也有一个很有意思的,就是针对可重入锁这个c!=0不会释放锁,还需要再次进入此方法释放锁直至变为0为止才释放锁
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}
4)unparkSuccessor(Node node)

  tryRelease方法成功释放锁后,这时候就需要通过unparkSuccessor方法来唤醒后继节点,因为之前后继节点挂起的时候把当前节点的ws设置为-1了。这个方法很简单,就是找到node节点的下一个稳定节点(ws<=0)然后通过unpark方法唤醒它,但是这里也是有一个最有意思的问题找这个node节点的下一个稳定节点为什么是从后往前找而不是从前往后找???

上面说过一个问题:addWaiter方法和enq方法为什么要先设置node节点的前驱节点,再CAS替换尾节点,最后才设置pred的后继节点?下面给总结!

private void unparkSuccessor(Node node) {
    
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    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);
}

5 问题总结

1)为什么需要虚拟头节点?

  看完AQS获取锁的流程都知道,队列中的节点获取到锁操作的是head节点出队,有新的线程节点入队操作的是tail节点,这样就保证了同步队列出队和入队分别操作的是不同的节点;如果没有这个虚拟头节点那么在同步队列中只剩下一个node节点的时候head=tail,这个node获取到锁既要出队,但是新进来的节点又要入队前驱指针指向node并修改tail为自己,都要操作这个node节点但是没有锁是无法保证并发安全的!

  其实上面说法都是在当前AQS实现的基础去反推这个结论的,我觉得有点没有意义!主要还是作者在设计AQS的时候,节点Node的ws状态有1、0、-1、-2、-3,而-1状态的节点表示需要唤醒下一个节点,当node节点挂起的时候需要设置其前驱节点的ws=-1,所以会有一个虚拟节点充当唤醒队列中第一个节点的头节点来唤醒等待时间最长的线程节点!

  因此我更偏向与AQS的设计而产生了这个头节点,当然也是为了解决上面说的只有一个节点的时候并发安全问题

2)节点入队操作顺序能改?

  addWaiter和enq这两个方法是怎样保证了无锁的情况下节点入队的并发安全的?就是通过这个顺序保证的,试想一下再高并发环境下同时间进入了很多个node节点,这时候只是都把前驱指针指向了pred,但是这么多个node节点只有一个CAS替换tail成功,这个成功的节点后续再把pred的后继节点指向自己,那么失败的节点在下一次循环的时候就把拿到新的pred就是上一个替换成功的节点,再重复这个操作,就能保证了节点入队的安全,这也说明了为什么AQS同步队列使用双向而不是单向!

  如图t2、t3、t4线程节点同时入队抢占设置tail,假设只有t2成功这时候把t1的next指向t2节点,失败的t3、t4下一次循环就会替换了prev都指向t2,假如t4成功然后到t3,最后t2、t3、t4都成功入队了!

  • 如果顺序改为先替换前驱节点再替换前驱节点的后继节点,最后CAS成功的节点再修改tail,这时候就会存在pred.next指向的不是tail而是一个错误的节点
  • 如果先CAS再设置前后节点呢?如果node设置tail成功了,但是这时候前后节点还没连接上,同步队列就会出现短时间的指针空指向,这个问题会导致什么?上面说的hasQueuedPredecessors方法判断h.next就会不准确,还有unparkSuccessor方法唤醒头节点的下一个稳点节点(ws<=0)包括从后往前找稳定节点也不准确!
    在这里插入图片描述
3)唤醒节点为什么从后往前找?

  上一个问题说了成功替换tail节点的node,是先有前驱节点,这时候后继节点可能存在短暂的指向null,所以从后往前找是正确的,不能从前往后!!!

4)acquire不响应中断怎样实现?

  上面acquireQueued方法添加的注释中说了通过interrupted局部变量实现的。既然acquire方法是不影响中断的,查看AQS的响应中断方法是acquireInterruptibly,对应ReentrantLock的API就是lockInterruptibly方法

  其实acquire表示独占锁,相对的acquireShared和releaseShared表示共享下的获取和释放锁,方法加上Interruptibly就表示响应中断,加上时间就表示了可设置超时响应…

5)AQS公平锁和非公平锁实现原理?

  AQS队列是FIFO的,所以不管是公平锁还是非公平锁只要是进入了队列的线程都是先进先出的,但是公平锁的tryAcquire方法比非公平锁的tryAcuire方法多了一个hasQueuedPredecessors方法,用来判断队列中是否有已经在排队的其它线程,如果返回true则需要排队,而非公平锁的话没有此方法。

  每次线程进来的时候都会先调用一次tryAcquire方法尝试获取锁,只有此方法返回false的时候才会把当前线程加入同步队列进行阻塞,若在高并发的情况下刚好队列中一个节点出队了把AQS的state和exclusiveOwnerThread都置为无锁状态值了,然后准备唤醒下一个节点前刚好进入一个新的线程,若是非公平锁就会在第一次调用tryAcquire方法获取锁成功,若是公平锁就需要判断前面若有等待的线程会先入队。

  所以非公平锁能提高吞吐量!

6)AQS是什么?

  AQS是实现同步器的一个模板,提供了一个原子状态值state来管理队列中的锁状态,state值通过volatile保证线程间的可见性,通过CAS保证了线程竞争修改的安全性。(独占模式非共享模式)需要各个具体实现的同步器去实现tryAcquire和tryRelease方法去修改state值状态,并返回true/false来告知AQS获取锁/释放锁的结果,每个线程至少调用一次tryAcquire/tryRelease方法,当第一次调用返回true则表示获取锁成功无需进入同步队列,反之获取锁失败需要进入同步队列且可能会被频繁的阻塞和唤醒执行调用tryAcquire获取锁成功出队,和tryRelease释放锁成功为止!其出入队流程、释放锁流程如上面源码分析!

7)ReentrantLock和synchronized
ReentrantLocksynchronized
用法是一个类,提供了获取锁、释放锁等方法直接使用关键字
锁实现机制AQSObjectMonitor(监视器)
灵活性支持响应中断、超时、尝试获取锁不灵活
释放锁显式调用unlock方法出了作用域自动释放
支持锁类型公平锁、非公平锁非公平锁
可重入行可重入可重入

6 图解

1)队列结构

  这个同步队列结构图很抽象!都是节点入队和出队的最终结果,其过程包括节点指针变化和AQS内部状态变化的动态过程没描述出来!
在这里插入图片描述

2)流程图

  TODO:上面源码分析没问题,画流程图不太擅长,后面再补上!!!

7 MySynchronized

  既然已经了解了AQS独占模式下的同步锁实现,那么我们能不能实现一个独占同步器呢?其实就是重写tryAcquire和tryRelease方法即可!下面同步器只是一个简单实现没做可重入、公平非公平等处理!

package aqs.self;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;

/**
 * @author linxh
 * @date 2023/03/30
 */
public class MySynchronized {

    private final Sync sync;

    public MySynchronized() {
        sync = new Sync();
    }

    public void lock() {
        sync.acquire(1996);
    }

    public void unlock() {
        sync.release(1996);
    }

    private static class Sync extends AbstractQueuedSynchronizer {

        @Override
        protected boolean tryAcquire(int arg) {
            return compareAndSetState(0, arg);
        }

        @Override
        protected boolean tryRelease(int arg) {
            setState(0);
            return getState() == 0;
        }
    }

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        MySynchronized mySynchronized = new MySynchronized();
        int threadNum = 100;
        CountDownLatch countDownLatch = new CountDownLatch(threadNum);
        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1_000_000; j++) {
                    mySynchronized.lock();
                    count++;
                    mySynchronized.unlock();
                }
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        System.out.println(count);
    }
}

8 最后

  后面会更新Condition,AQS共享锁,共享锁和独占锁共存的ReentrantReadWriteLock,ReentrantReadWriteLock是最有意思:doug lea使用state变量为int值的特性(占4个字节),使用高16位标位表示共享锁,使用低16位表示独占锁!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值