【J.U.C-Locks】锁框架——抽象队列同步器AQS实现原理

AbstractQueuedSynchronized:抽象的队列同步器。
既然是抽象,也就是说很多的同步器都是实现了这一抽象的接口,实现了其中定义的获取与释放锁的方法。
它的核心思想在于一个FIFO的双向链表队列+volatile修饰的state同步状态,多个线程并发争夺state资源。

资源分为共享和独占两种方式:
	实现了共享形式的同步器如ReadWriteLock
	实现了独占形式的同步器如ReentrantLock

这些同步器只需要定义如何去获取与释放state资源,至于对这个队列的维护,是在AQS框架实现的。
资源如果获取成功则结束,获取失败就加入到同步队列的队尾;
此时线程在同步队列中,循环去尝试获取资源,因为是FIFO的队列,所以首先需要前置节点是头结点,然后获取资源成功才算成功;
当前线程资源释放成功后,则唤醒后面一个节点。

抽象队列同步器AQS

java中的大部分同步类:Lock、Semaphore、ReentrantLock、CountDownLatch等都是基于AbstractQueuedSynchronizer实现的。AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。

AQS的核心思想:

  • 如果被请求的共享资源空闲,那么将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;
  • 如果共享资源被占用,就需要一定的阻塞等待、唤醒机制来保证锁分配。这个机制是使用CLH队列,一种双向链表来实现的。
  • AQS通过队列来辅助实现线程同步,线程并发争夺state资源,争夺失败则进入等待队列并阻塞;在state资源被释放之后,再从队列头唤醒被阻塞的线程节点
    在这里插入图片描述
  1. AQS使用一个Volatile的int类型的成员变量来表示 同步状态state
  2. 通过内置的 FIFO双向队列 来完成资源获取的排队工作
  3. 通过 CAS 完成对State值的修改

1.AQS类属性

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
    implements java.io.Serializable {    
    // 版本号
    private static final long serialVersionUID = 7373984972572414691L;    
    // 头结点
    private transient volatile Node head;    
    // 尾结点
    private transient volatile Node tail;    
    // 状态
    private volatile int state;    
    // 自旋时间
    static final long spinForTimeoutThreshold = 1000L;
    
    // Unsafe类实例
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    // state内存偏移地址
    private static final long stateOffset;
    // head内存偏移地址
    private static final long headOffset;
    // state内存偏移地址
    private static final long tailOffset;
    // tail内存偏移地址
    private static final long waitStatusOffset;
    // next内存偏移地址
    private static final long nextOffset;
    // 静态初始化块
    static {
        try {
            stateOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
            headOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("head"));
            tailOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
            waitStatusOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("waitStatus"));
            nextOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("next"));

        } catch (Exception ex) { throw new Error(ex); }
    }
}

这个类是一个abstract抽象类,其实现类有:
在这里插入图片描述

2.AQS数据结构

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;
  
        volatile int waitStatus;//	当前节点在队列中的状态
        volatile Node prev;//前驱指针
        volatile Node next;//后继指针
        volatile Thread thread;//表示处于该节点的线程
        Node nextWaiter;//指向下一个处于CONDITION状态的节点
        
        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;
        }
    }

waitStatus即当前节点的状态包括:

名称意义
0Node初始化的默认值
CANCELLED1线程获取锁的请求已取消
CONDITION-2节点在等待队列中,节点线程等待唤醒
PROPAGATE-3当前线程处在SHARED情况下,该字段才会使用
SIGNAL-1线程已经准备好了,就等资源释放了

3.AQS同步状态

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

AQS资源共享方式有两种:

  • 独占Exclusive:只能有一个线程获取锁,如ReentrantLock;
  • 共享Share:可以有多个线程同时获取锁,如CountDownLatch、ReadWriteLock

自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式 即可;
至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在上层已经帮我们实现好了。

自定义同步器需要继承同步器,并实现指定的方法:

tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。

tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。

tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。

tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。

一般来说,自定义同步器要么是独占方式,要么是共享方式,它们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。

AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

【实现原理】独占模式同步状态获取——Acquire

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
  1. tryAcquire首先尝试获取锁(这个方法是在自定义同步器中实现的)
  2. 如果成功获取,则方法结束
  3. 获取锁不成功,则与自定义同步器的逻辑无关,进入到AQS框架的后续逻辑:

1.线程加入同步队列addWaiter

  • 加入时机:AQS尝试通过tryAcquire获取锁失败时
    此时就会 将此线程加入到等待队列中
private Node addWaiter(Node mode) {
        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;
            }
        }
        enq(node);
        return node;
    }
  1. 新创建一个节点Node保存当前线程,并设置为 独占模式

  2. 如果等待队列的尾结点tail不为空,表示已经被初始化, 快速加入队尾

    • 将Node连接到尾结点之后;这里可能有其他线程同时插入的情况,所以 基于CAS 设置尾结点;
    • 当前线程Node已入队,返回;
  3. 如果队列为空,还没有被初始化,或者是Node节点CAS入队失败:

    • 表示快速加入队尾失败;
    • 使用enq 自旋方式 入队:
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;
                }
            }
        }
    }
  1. 如果队尾tail为空,表示队列为空的情况:
    • 初始化一个新Node作为队列头head;
    • 设置tail指向head
    • 继续下一次for循环
    • 此时tail不为空,进入到else分支:
  2. tail不为空,进入到else分支:与上述addWaiter中的CAS操作相同,设置node与之前队尾的双向链接。

也就是说,线程加入同步队列,如果第一次CAS双向链接不成功,那么通过 死循环+CAS实现自旋 不断尝试,直到成功。

  • 对于双向链表的操作过程如图:
    在这里插入图片描述

2.线程获取锁出队acquireQueued

  • 此时的状态为:当前线程获取锁失败,被加入到等待队列中。
    在Node插入到队列中后,并不会直接去挂起线程,因为在入队过程中,可能其他的线程已经执行完成了。所以会先通过acquireQueued,使放入队列中的线程 不断去获取锁 ,直到获取成功或不再需要获取(中断)。
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
  1. 获取当前节点的前一个节点;
  2. 如果前一个节点为头结点,即 当前线程的前驱节点是头结点时,去尝试获取锁 ,如果获取成功,则返回;
  3. 如果没有获取到锁,则判断线程是否应该挂起:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * 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;
    }
  • 如果当前线程的前一个节点waitStatus为:
  • SIGNAL(-1):返回true,表示前置结点处于唤醒状态, 挂起当前线程 ,并等待被唤醒;
  • 状态> 0 :返回false,表示前置节点处于取消状态,那么不断向前找,直到将当前Node的prex指向一个status<0的节点,并且把取消的节点从队列中删除;
  • 状态 = 0、-2、-3:返回false,将prex节点的状态设为SIGNAL,目的是,下一次for循环进入此方法的时候,将会返回true;

这里的分支如果返回为true,则挂起当前线程:

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
  • 执行park操作挂起当前线程;
  • 返回线程是否已经被中断;
  1. 方法的最终,如果等待过程中当前线程没有成功获取锁,那么取消当前线程的排队:
 private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        node.thread = null;

        // Skip cancelled predecessors
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // predNext is the apparent node to unsplice. CASes below will
        // fail if not, in which case, we lost race vs another cancel
        // or signal, so no further action is necessary.
        Node predNext = pred.next;

        // Can use unconditional write instead of CAS here.
        // After this atomic step, other Nodes can skip past us.
        // Before, we are free of interference from other threads.
        node.waitStatus = Node.CANCELLED;

        // If we are the tail, remove ourselves.
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (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. 解除Node与Thread的绑定关系,跳过状态为取消的节点;
  2. 设置状态为 取消状态
  3. 如果当前节点是tail:将此节点前驱节点的next置为null;
  4. 如果当前节点是head的后置节点,直接unpark唤醒后继节点;
  5. 如果当前节点不是第一个有效节点,也不是tail:将node的前置节点的next指向后置节点;
    也就是说不论哪种情况,目的都是把当前节点置为取消状态,并将node从等待队列移除。

对于整个获取锁的过程如图:
在这里插入图片描述

【实现原理】独占模式同步状态释放——Release

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. 如果释放失败,返回false;
  3. 如果释放成功,唤醒后续节点;

这里的判断条件if (h != null && h.waitStatus != 0):

  • h = null:初始情况下head为空,如果第一个节点还没入队,就会出现head = null的情况;
  • h != null && waitStatus == 0 表明后继节点对应的线程仍在运行中,不需要唤醒;
  • h != null && waitStatus < 0 表明后继节点可能被阻塞了,需要唤醒;

1.唤醒节点线程unparkSuccessor

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)
            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;
        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. 获取当前节点的状态,如果<0,则强制设为0;
  2. 从后往前找,等待队列中最前边的未放弃状态的线程,并唤醒;

可以看出释放锁就是将头结点的后继节点唤醒,如果后继节点不符合唤醒条件,则 从后往前找 ,直到找到符合唤醒条件的节点。

对于整个释放锁的过程如图:
在这里插入图片描述

【实现原理】共享模式同步状态获取——AcquireShared

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

与独占模式类似:

  1. tryAcquireShared首先尝试获取锁(这个方法是在自定义同步器中实现的)
  2. 如果获取成功,则方法结束
  3. 获取锁不成功,进入到后续逻辑doAcquireShared:
private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

1. 线程加入同步队列addWaiter

与独占模式一样

2. 线程获取锁出队

与独占模式不同的是

  • 独占模式达到出队条件(当前为头结点且获取到锁)之后,进行setHead:
    直接出队,结束
private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }
  • 而共享模式进行setHeadAndPropagate:
private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

将当前节点出队后,继续唤醒同步队列中后续的共享节点,doReleaseShared:

3. 传递唤醒后续共享节点doReleaseShared

private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

  • 可知,当同步队列的头结点被唤醒后,会循环唤醒下一个节点对应的线程,以实现 共享状态的“向后传播”

【实现原理】共享模式同步状态释放——ReleaseShared

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

与独占模式类似:

  1. tryReleaseShared首先尝试释放锁(这个方法是在自定义同步器中实现的)
  2. 如果释放失败,返回false;
  3. 如果释放成功,唤醒后续节点;

1.传递唤醒后续共享节点doReleaseShared

不同的是:

  • 独占模式中唤醒后续节点unparkSuccessor,只会唤醒一个符合条件的节点;
  • 共享模式进入到doReleaseShared,由上文知,是唤醒后续全部共享等待节点。

附1. LockSupport工具类

LockSupport是J.U.C包中的一个工具类,是用来创建锁和其他同步类的基本线程阻塞原语,它定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能;

  • Java锁和同步器框架的核心原理AQS,就是基于LockSupport.park()和LockSupport.unpark()来实现线程阻塞和唤醒的;

LockSupport与每个使用它的线程都会 关联一个许可证(permit) ,这个permit相当于一个开关:

  • 0代表关闭,1代表打开
  • 调用一次unpark,permit将会+1,变为1;(重复调用多次unpark, permit不会累加 ,也为1)
  • 调用一次park,permit将会-1,变为0;
  • 再次调用park,将会变为block;(线程将阻塞,知道permit变为1)

park、unpark方法

LockSupport定义了一组park开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法来唤醒一个被阻塞的线程。

  • 阻塞当前线程,如果调用unpark(Thread thread)方法或者当前线程被中断,才能从park方法返回:
public static void park()
  • 阻塞当前线程,最长不超过nanos纳秒,返回条件在park的基础上增加了超时返回:
public static void park(long nanos); 
  • 阻塞当前线程,直到deadline时间:
public static void park(long deadline); 
  • 唤醒处于阻塞状态的线程thread:
public static void unpark(Thread thread); 

使用Demo:

public static void main(String[] args) throws InterruptedException{
        Thread thread = new Thread(){
            public void run(){
                //阻塞当前线程
                LockSupport.park();
            }
        };
        thread.start();
        Thread.sleep(3000);
        //唤醒指定线程
        LockSupport.unpark(thread);
    }

LockSupport特点

  1. park、unpark与wait、notify提供类似的功能,但是park不必获取同步锁就可以执行;
  2. 顺序性:先调用unpark再调用park,仍能够正确实现同步,而不会像wait/notify一样顺序不当引起阻塞
  3. 中断响应:当线程被interrupt时作用与unpark一样,都可以使被park的线程继续向下执行;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值