JUC之AQS学习

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

前言

AbstractQueuedSynchronizer简称AQS,它是实现同步器的基本组件,比如常用的ReentrantLockSemaphoreCountDownLatch等。

AQS定义了一套多线程访问共享资源同步模板解决了实现同步器时涉及的大量细节问题,极大地减少了实现工作。

AQS基于FIFO(先入先出)的队列实现,将每一个线程封装成一个Node结点,并且内部维护一个state变量,通过原子更新这个状态变量来实现加锁解锁操作。

有一些基本的方法我们需要提前了解一下:

状态

  • getState():返回同步状态
  • setState(int newState):设置同步状态
  • compareAndSetState(int expect, int update):使用C A S设置同步状态
  • isHeldExclusively():当前线程是否持有资源

独占资源(不响应线程中断)

  • tryAcquire(int arg):独占式获取资源,子类实现
  • acquire(int arg):独占式获取资源模板
  • tryRelease(int arg):独占式释放资源,子类实现
  • release(int arg):独占式释放资源模板

共享资源(不响应线程中断)

  • tryAcquireShared(int arg):共享式获取资源,返回值大于等于0则表示获取成功,否则获取失败,子类实现
  • acquireShared(int arg):共享式获取资源模板
  • tryReleaseShared(int arg):共享式释放资源,子类实现
  • releaseShared(int arg):共享式释放资源模板

内部类Node

NodeAQS的内部类,每个线程都会封装成Node结点,用于组成CLH队列、等待队列,结点中保存着代表的线程、前驱结点、后继节点以及代表的线程的状态

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;
        // 标记线程等待在一个条件上
        static final int CONDITION = -2;
        // 标记后面的共享锁需要无条件地传播(共享锁需要连续唤醒读的线程)
        static final int PROPAGATE = -3;

    
        // 当前节点保存的线程对应的状态(可选的状态:0,CANCELLED,SIGNAL,CONDITION,PROPAGATE)
        // waitStatus == 0  表示当前线程是默认状态
        // waitStatus > 0   表示当前线程是取消状态
        // waitStatus == -1 表示当前节点是头结点的话需要唤醒后继节点
        volatile int waitStatus;
        // 当前节点的前驱结点
        volatile Node prev;
        // 当前节点的后继节点
        volatile Node next;
        // 当前节点表示的线程
        volatile Thread thread;
        // 下一个等待在条件上的节点
        Node nextWaiter;
        // 是否是共享模式
        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
        }
        // 结点的构造方法,当前线程,结点是共享还是独占式
        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;
        }
    }
复制代码

属性

我们知道AQS实现是基于一个FIFO队列实现的,下面是AQS的成员变量,包括了队列的首尾节点,还有重要的状态变量state

状态变量state用来控制加锁解锁,队列用来放置等待的线程

// 头结点,头结点中存储的线程是持有锁的线程(注意:阻塞队列不包含头结点,是从头结点的后继节点到尾节点,这个区间才是阻塞区间)
private transient volatile Node head;
// 尾结点
private transient volatile Node tail;
// 控制加锁解锁的状态变量
// state > 0  持有锁
// state == 0 没有任何线程持有锁
private volatile int state;

既然说到了state,那么就介绍一下与state有关的几个方法
// 获取当前state的值
protected final int getState() {
        return state;
    }
// 设置state的值
protected final void setState(int newState) {
    state = newState;
}
// 设置state的值,利用CAS操作执行
protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
复制代码

成员方法

既为了可以学习AQS的成员方法内部的实现,也为了能够贯通下来,我们就从加锁和解锁的层面顺下来(毕竟我们常用的就是lock操作和unlock操作)

lock操作

位于ReentrantLock(独占式锁)的中,加锁分为两种,一种是非公平锁,一种是公平锁
    非公平锁:
    final void lock() {
                if (compareAndSetState(0, 1))
                    setExclusiveOwnerThread(Thread.currentThread());
                else
                    acquire(1);
            }
    公平锁:
    final void lock() {
                acquire(1);
            }
// 我们可以看到,非公平锁只是比公平锁多了一步CAS加锁过程,我们看一下acquire()方法
复制代码

acquire方法

    
    // AQS中的acquire方法
    public final void acquire(int arg) {
            // 首先尝试获取锁,获取锁成功就直接返回,失败就继续向下执行
            if (!tryAcquire(arg) &&
                // addWaiter(Node.EXCLUSIVE), arg)将当前线程封装成Node结点入队,设置结点为独占式,
                // 入队后调用acquireQueued(该方法包括挂起当前线程,
                // 以及唤醒后相关的逻辑,令当前线程不断去获取资源,直到成功才停止)
                // acquireQueued返回boolean类型,true:表示被中断唤醒过,false:表示没被中断唤醒过
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
复制代码

tryAcquire方法

tryAcquire(arg)方法,同样分为公平锁和非公平锁
    非公平锁:
    protected final boolean tryAcquire(int acquires) {
                return nonfairTryAcquire(acquires);
            }
    final boolean nonfairTryAcquire(int acquires) {
                // 获取当前线程
                final Thread current = Thread.currentThread();
                // 获取state值
                int c = getState();
                // state为0,尝试加锁
                if (c == 0) {
                    if (compareAndSetState(0, acquires)) {
                        // CAS成功,设置持锁线程
                        setExclusiveOwnerThread(current);
                        // 返回true表示获取锁成功
                        return true;
                    }
                }
                // 如果当前线程就是持锁线程,就state就加1(重入锁)
                else if (current == getExclusiveOwnerThread()) {
                    int nextc = c + acquires;
                    if (nextc < 0) // overflow
                        throw new Error("Maximum lock count exceeded");
                    // 设置state的值
                    setState(nextc);
                    // 返回true
                    return true;
                }
                // 如果上面都没有执行成功,返回false
                return false;
            }
    公平锁:
    protected final boolean tryAcquire(int acquires) {
                // 获取当前线程
                final Thread current = Thread.currentThread();
                // 获取state的值
                int c = getState();
                // state == 0
                if (c == 0) {
                    // hasQueuedPredecessors()判断当前是否有等待者线程
                    // true  -> 当前有等待者线程,当前线程需要入队,tryAcquire直接返回false代表没有竞争到锁,方法结束
                    // false -> 当前没有等待者线程,当前线程可以尝试竞争锁
                    if (!hasQueuedPredecessors() &&
                        // 竞争锁
                        compareAndSetState(0, acquires)) {
                        // 竞争锁成功设置持锁线程
                        setExclusiveOwnerThread(current);
                        // 返回true代表竞争锁成功
                        return true;
                    }
                }
                // 如果持锁线程就是当前线程,重入
                else if (current == getExclusiveOwnerThread()) {
                    // 得到state应该设置的值
                    int nextc = c + acquires;
                    if (nextc < 0)
                        throw new Error("Maximum lock count exceeded");
                    // 设置state的值
                    setState(nextc);
                    return true;
                }
                // 返回false代表加锁失败的情况有:
                // 1. 持锁线程后有等待者线程
                // 2. 当前线程不是持锁线程
                return false;
            }

    // 当tryAcquire执行失败(竞争锁失败)就执行addWaiter方法
复制代码

addWaiter

  // addWaiter将当前线程添加到阻塞队列,并返回线程包装成的Node结点
    private Node addWaiter(Node mode) {
            // 将当前线程构造成Node结点
            Node node = new Node(Thread.currentThread(), mode);
            // 获取尾结点
            Node pred = tail;
            // 尾结点不等于null,说明队列不为空
            if (pred != null) {
                // 令当前节点的前驱结点等于原来尾结点
                node.prev = pred;
                // 自旋将当前节点设置为新的尾结点,成功就返回包装成的Node结点
                if (compareAndSetTail(pred, node)) {
                    pred.next = node;
                    return node;
                }
            }
            // 当添加到队列失败,就执行enq方法,执行到这里有两种情况:
            // 1. 当前队列为空
            // 2. 当前队列不为空,但是CAS设置尾结点失败,就执行enq自选入队
            enq(node);
            return node;
        }
复制代码

enq方法

    // 接下来我们看看enq方法
    private Node enq(final Node node) {
            for (;;) {
                // 获取尾结点
                Node t = tail;
                // 尾结点为空说明队列为空
                if (t == null) { // Must initialize
                    // 给当前持锁线程设置一个head结点并设置尾结点为head,之后自旋入队
                    if (compareAndSetHead(new Node()))
                        tail = head;
                } else {
                    // 设置当前线程的前驱结点为原来尾结点
                    node.prev = t;
                    // 自旋设置尾结点
                    if (compareAndSetTail(t, node)) {
                        // 设置当前结点为成功,将原来线程的后继节点指向它,之后返回当前节点
                        t.next = node;
                        return t;
                    }
                }
            }
        }
复制代码

acquireQueued



    // 接下里我们来看一下acquireQueued方法
    // 位于AQS,真正去竞争资源的方法
    // 参数 final Node node:封装当前线程的Node,且当前线程已经入对成功
    // 参数 int arg:更新state的值需要用到
    // 返回true:表明当前线程挂起中当前线程被中断唤醒过;返回false:表明当前线程挂起中当前线程没有被中断唤醒过
    final boolean acquireQueued(final Node node, int arg) {
            // 标记当前线程抢占锁是否是失败的
            // 默认为true,代表抢占锁失败
            boolean failed = true;
            try {
                // 默认当前线程没有被中断
                boolean interrupted = false;
                for (;;) {
                    // 获取当前节点的前驱结点
                    final Node p = node.predecessor();
                    // 前驱节点是头结点则当前线程尝试获取资源,head.next任何时候都有权利争夺锁
                    // 只有当前节点的前驱结点是头结点时才会执行往下执行tryAcquire方法
                    // tryAcquire(arg)返回true说明当前线程抢到锁了,那要重置头结点了
                    // tryAcquire(arg)返回false,说明争抢锁失败需要继续被挂起
                    if (p == head && tryAcquire(arg)) {
                        // 设置新的头结点
                        setHead(node);
                        // 设置原来头结点的后继为null,帮助垃圾回收
                        p.next = null; // help GC
                        // 当前线程获取锁的过程中没有发生异常
                        failed = false;
                        return interrupted;
                    }
                    // 判断当前线程获取锁失败后是否需要被挂起
                    // true:需要被挂起,执行parkAndCheckInterrupt方法
                    // false:不需要被挂起,自旋继续
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        // parkAndCheckInterrupt():挂起当前线程,这里的挂起指的就是中断,并标记中断标记为true,并在当前线程被唤醒后返回中断标记
                        // 唤醒该线程的方式:
                        // 1. 正常唤醒:调用unpark()方法,唤醒该线程
                        // 2. 其他线程给当前线程一个中断挂起信号
                        parkAndCheckInterrupt())
                        interrupted = true;
                }
            } finally {
                // failed == true,代表当前线程抢占锁失败,执行出队逻辑
                if (failed)
                    // node结点取消线程资源竞争
                    cancelAcquire(node);
            }
        }
复制代码

shouldParkAfterFailedAcquire

接下来我们来看看shouldParkAfterFailedAcquire方法
    // 位于AQS中,判断当前线程获取锁资源失败后是否需要被挂起
    // 返回true:代表需要被挂起;返回false:代表不需要被挂起
    // 参数pred:代表当前线程封装成的结点的前驱结点
    // 参数node:代表当前线程封装成的结点
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
            // 获取当前线程结点的状态waitStatus
            // waitStatus == 0,默认状态
            // waitStatus > 0,CANCELLED==1代表当前节点是取消状态
            // waitStatus == -1,SIGNAL代表当前结点释放锁后需要唤醒它的后继节点
            int ws = pred.waitStatus;
            // 如果ws==Node.SIGNAL,说明当前节点可以唤醒它的后继节点,在acquireQueued方法中调用parkAndCheckInterrupt去park当前节点
            if (ws == Node.SIGNAL)
                ///
                return true;
            // ws > 0,代表ws == 1,当前节点是取消状态
            if (ws > 0) {
                do {
                    // 找到第一个waitStatus状态小于等于0的结点
                    node.prev = pred = pred.prev;
                } while (pred.waitStatus > 0);
                // 将前驱结点的后继节点设为当前节点,隐含着一种操作:结点的状态为CANCELLED(1)的结点会被出队
                pred.next = node;
            } else {
                // 当前节点的前驱结点的默认状态是0,即默认这种情况
                // 将当前结点的前驱结点的状态默认设置为SIGNAL,表示该节点释放锁后会唤醒它的第一个后继结点
                compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
            }
            return false;
        }
复制代码

parkAndCheckInterrupt方法

   // 位于AQS中,挂起当前线程
    parkAndCheckInterrupt方法,挂起当前线程结点
    private final boolean parkAndCheckInterrupt() {
            LockSupport.park(this);
            return Thread.interrupted();
        }
复制代码

unlock()操作

    // unlock位于ReentrantLock中
    public void unlock() {
            sync.release(1);
        }
   
复制代码

release

 // 调用AQS的release方法,释放锁模板
    public final boolean release(int arg) {
            // 执行tryRelease方法释放锁
            // 返回true:释放锁成功
            // 返回false:释放锁失败
            if (tryRelease(arg)) {
                // 获取头结点
                // head什么时候会被创建出来?
                // 持锁线程为释放锁,且有其他线程获取锁,此时其他线程无法获取锁
                // 且此时队列为空,此时获取锁线程会为持锁线程创建一个头结点,并将获取锁线程插入到头结点的后面(head的后继节点)
                Node h = head;
                // 头结点不为空,且waitStatus不等于0说明头结点后面一定插入过结点
                if (h != null && h.waitStatus != 0)
                    // 唤醒后继结点
                    unparkSuccessor(h);
                // 返回释放资源成功
                return true;
            }
            return false;
        }
   
复制代码

tryRelease

 // 这个是Sync内部类中的方法
    // 返回true:当前线程已经完全释放锁
    // 返回false:当前线程未完全释放锁
    protected final boolean tryRelease(int releases) {
                // 获取state应该变化为的值
                int c = getState() - releases;
                // 当前线程和持锁线程不是同一个线程,抛出异常
                if (Thread.currentThread() != getExclusiveOwnerThread())
                    throw new IllegalMonitorStateException();
                // free代表释放锁是否完全成功,默认不成功
                boolean free = false;
                if (c == 0) {
                    // state为0,说明释放锁成功,设置free为true
                    free = true;
                    // 设置持锁线程为null
                    setExclusiveOwnerThread(null);
                }
                // 设置state的值
                setState(c);
                return free;
            }
   
复制代码

unparkSuccessor

 // 唤醒后继结点
    private void unparkSuccessor(Node node) {
            // 获取当前节点的waitStatus
            int ws = node.waitStatus;
            // 当前节点的waitStatus < 0,设置waitStatus为0,改成0的原因因为当前节点已经完成唤醒后继结点的任务了
            if (ws < 0)
                compareAndSetWaitStatus(node, ws, 0);
            // s是当前节点的第一个后继节点
            Node s = node.next;
            // 当前节点的后继节点为null或当前节点的后继节点是取消状态
            // 什么情况下当前节点的第一个后继节点为null
            // 1. 当前节点是尾结点
            // 2. 当前节点入队未完成时
            // 只有在s != null才会执行到s.waitStatus,当s.waitStatus>0时取消节点
            if (s == null || s.waitStatus > 0) {
                s = null;
                // 寻找第一个离node最近的可以被唤醒的节点,该节点可能是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);
        }
复制代码

lockInterruptibly方法

分析一下可中断的获取锁方法lockInterruptibly
// 该方法在ReentrantLock中
public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

复制代码

acquireInterruptibly

// 该方法在AQS中,竞争资源的方法,可以被中断
public final void acquireInterruptibly(int arg) throws InterruptedException {
        // 如果当前线程的中断标记已经为true了就直接抛出中断异常
        if (Thread.interrupted())
            throw new InterruptedException();
        /// 尝试获取锁,成功返回,失败执行doAcquireInterruptibly方法
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

复制代码

doAcquireInterruptibly

// doAcquireInterruptibly方法
private void doAcquireInterruptibly(int arg)throws InterruptedException {
        // 因为获取锁失败了所以将当前线程形成的结点加入队列
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                // 获取当前节点的前驱结点
                final Node p = node.predecessor();
                // 如果当前节点的前驱结点是头结点,那么当前节点执行tryAcquire获取锁
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                // 判断是够应该挂起后继结点
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                // 取消指定节点的竞争
                cancelAcquire(node);
        }
    }

复制代码

cancelAcquire

cancelAcquire方法
private void cancelAcquire(Node node) {
        // 当前节点是空节点,直接结束方法
        if (node == null)
            return;
        // 设置node的thread为null
        node.thread = null;
        // 结点的前驱结点
        Node pred = node.prev;
        // 获取当前未取消排队的node的前驱
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
        // 获取当前节点的后继节点
        Node predNext = pred.next;
        // 标记当前节点为取消状态
        node.waitStatus = Node.CANCELLED;
        // 当前节点为尾结点那就设置node未取消排队的前驱结点为尾结点
        if (node == tail && compareAndSetTail(node, pred)) {
            // 并设置新的尾结点的后继为null
            compareAndSetNext(pred, predNext, null);
        } else {
            int ws;
            // 当前节点的未取消的前驱结点不是头结点
            if (pred != head &&
                // 当前节点的前驱结点的状态为唤醒它的后继节点
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                // 前驱结点状态小于等于0设置状态为唤醒它的后继节点
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                // 完成真正的出队
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }
复制代码

总结

(1)AQS是Java中几乎所有锁和同步器的一个基础框架

(2)AQS中维护了一个队列,这个队列使用双链表实现,用于保存等待锁排队的线程

(3)AQS中维护了一个状态变量,控制这个状态变量就可以实现加锁解锁操作了;

(4)基于AQS可以自己动手写一个锁,只需要实现AQS的几个方法即可。

猜你喜欢

转载自juejin.im/post/7085105876800897054