AQS-AbstractQueuedSynchronizer同步器原理

目录

2、独占式同步状态获取与释放

2.1、独占锁的获取

2.2、独占锁的释放

3、共享式同步状态获取与释放

3.1、获取锁

3.2、释放锁

4、独占锁和共享锁在实现上的区别 

5、独占式超时获取同步状态?

6、一些锁

6.1、重入锁

6.1.1、ReentrantLock重入锁

6.1.2、非公平锁

6.1.3、公平锁

6.2、读写锁ReentrantReadWriteLock

6.2.1、写锁的获取与释放

6.3.1、读锁的获取与释放

7、CAS与AQS


1、什么是AQS


AQS是AbstractQueuedSynchronizer的简称,它是一个Java提供的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。AQS的主要作用是为Java中的并发同步组件提供统一的底层支持,例如ReentrantLock,CountdowLatch,ThreadPoolExecutor,ReentrantReadWriteLockd等就是基于AQS实现的,用法是通过继承AQS实现其模版方法,然后将子类作为同步组件的内部类。

       同步器的主要使用方式是继承。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态。

       同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。

  • ·getState():获取当前同步状态。
  • ·setState(int newState):设置当前同步状态。
  • ·compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。

       同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中的等待线程情况。自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义。

       下面看下 同步器可被重写的方法如下(如,ReentrantLock就重写了下面的某些方法) 

 同步器可重写的方法如图:

下面我们就举个例子,看独占锁(独占锁就是在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁)是如何利用AQS,和重写他的方法来达到独占锁的效果的。

public class Mutex implements Lock {
    // 静态内部类,自定义同步器
    private static class Sync extends AbstractQueuedSynchronizer {
        // 是否处于占用状态
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
        // 当状态为0的时候获取锁
        public boolean tryAcquire(int acquires) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        // 释放锁,将状态设置为0
        protected boolean tryRelease(int releases) {
            if (getState() == 0) throw new
                    IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        // 返回一个Condition,每个condition都包含了一个condition队列
        Condition newCondition() { return new ConditionObject(); }
    }
    // 仅需要将操作代理到Sync上即可
    private final Sync sync = new Sync();
    public void lock() { sync.acquire(1); }
    public boolean tryLock() { return sync.tryAcquire(1); }
    public void unlock() { sync.release(1); }
    public Condition newCondition() { return sync.newCondition(); }
    public boolean isLocked() { return sync.isHeldExclusively(); }
    public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
        
}


Mutex中定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。在tryAcquire(int acquires)方法中,如果经过CAS设置成功(同步状态设置为1),则代表获取了同步状态,而在tryRelease(int releases)方法中只是将同步状态重置为0。用户使用Mutex时并不会直接和内部同步器的实现打交道,而是调用Mutex提供的方法,在Mutex的实现中,以获
取锁的lock()方法为例,只需要在方法实现中调用同步器的模板方法acquire(int args)即可。
      下面我们队队列同步器AQS的实现进行分析。主要包括:同步队列、独占式同步状态获取与释放、共享式同步状态获取与释放以及超时获取同步状态等同步器的核心数据结构与模板方法。

同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点,具体的定义和状态大家可以自行去看下源码这里略过。同步队列的基本结构如图所示:
 

同步器包含了两个节点类型的引用,一个指向头节点,而另一个指向尾节点。试想一下,当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Nodeupdate),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。

        同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。

        下面分别看下独占式同步状态获取与释放和共享式同步状态获取与释放是如何实现的。

2、独占式同步状态获取与释放

 通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出,该方法代码如下:

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

上述代码主要完成了同步状态获取、节点构造、加入同步队列以及在同步队列中自旋等待的相关工作,其主要逻辑是:首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。

    我们看下同步器的addWaiter和enq的实现代码如下:

顾客1、顾客2、顾客3

    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) {
            // 尾节点不为null,把新的节点的前节点指向以前的尾节点(就是pred)
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                // compareAndSetTail(pred, node) 这个是把node节点设置成尾节点,从而pred就不是尾节点了,node变成了尾节点(tail)
                // 把pred的next指向node
                pred.next = node;
                return node;
            }
        }
        // 没有尾节点,没有队列,把node加入队列
        enq(node);
        return node;
    }

    private Node enq(final Node node) {
        for (; ; ) {
            // 循环,自旋
            Node t = tail;
            if (t == null) { // Must initialize
                // 尾节点尾null 初始化一个尾节点,new一个,head和tail同一个
                // new Node()虚节点,占位,哨兵节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // 尾节点不为null,把新的节点的前节点指向以前的尾节点(就是t)
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    // compareAndSetTail(t, node) 这个是把node节点设置成尾节点,从而t就不是尾节点了,node变成了尾节点(tail)
                    // 把t的next指向node
                    t.next = node;
                    return t;
                }
            }
        }
    }

上述代码通过使用compareAndSetTail(Node expect,Node update)方法来确保节点能够被线程安全添加

    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }

在enq(final Node node)方法中,同步器通过“死循环”来保证节点的正确添加,在“死循环”中只有通过CAS将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线程不断地尝试设置。

节点进入同步队列之后,就进入了一个自旋的过程(acquireQueued方法),每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程),如代码所示

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (; ; ) { // 死循环,自旋
                // 获取下一个节点
                final Node p = node.predecessor();
                // 1、如果是头节点,并且获取锁成功
                if (p == head && tryAcquire(arg)) {
                    // 设置为头节点
                    // 把以前的头节点改成node的节点,原来的头节点就可以回收了
                    setHead(node);
                    // 没有引用,有助于GC收集
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // shouldParkAfterFailedAcquire(p, node)
                // parkAndCheckInterrupt() 把线程阻塞,正在排队的等待中
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                // 取消的node
                cancelAcquire(node);
        }
    }

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 获取前驱节点的状态
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
        /**
         * 如果是signal状态,即等待被占用的资源释放,直接返回true
         * 准备调用parkAndCheckInterrupt方法,用于将当前的线程挂起
         */
            return true;
            // 大于0说明是cancelled状态(取消)
        if (ws > 0) {
            /**
             * 循环判断前驱节点的前驱节点是否也为cancelled状态,忽略该状态的节点,重新连接队列
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 把前驱节点的前驱节点状态设置为signal,用于后续唤醒操作
            // 程序第一次执行到这返回false,还会进行外层第二次循环
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

    private final boolean parkAndCheckInterrupt() {
        // 线程挂起,程序不会继续向下执行
        LockSupport.park(this);
        // 根据park方法的api描述,程序在下述三种情况会向下执行
        // 1、被unpark
        // 2、被中断(interrupt)
        // 3、其他不符合逻辑的返回才会继续向下执行
        // 因上述三种情况程序执行至此,返回当前线程的中断状态,并清空中断状态
        // 如果由于被中断,该方法会返回true
        return Thread.interrupted();
    }

在acquireQueued(final Node node,int arg)方法中,当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态。

独占式同步状态获取流程,也就是acquire(int arg)方法调用流程,如图所示:

下面看下如何释放释放同步状态的:

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            // 头节点
            Node h = head;
            // 头节点不为0,并且状态不为0
            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)
            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;
            // s.waitStatus > 0 取消
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            // unpark 回到方法的 java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued
            // 的parkAndCheckInterrupt()方法的LockSupport.park(this)航代码,向下执行
        /**
         * final Node p = node.predecessor();
         *                 if (p == head && tryAcquire(arg)) {
         *                     setHead(node);
         *                     p.next = null; // help GC
         *                     failed = false;
         *                     return interrupted;
         *                 }
         */
            LockSupport.unpark(s.thread);
    }

该方法执行时,会唤醒头节点的后继节点线程,unparkSuccessor(Node node)方法使用LockSupport(这里不做介绍)来唤醒处于等待状态的线程。

2.1、独占锁的获取

  • 调用入口方法acquire(arg)
  • 调用模版方法tryAcquire(arg)尝试获取锁,若成功则返回,若失败则走下一步
  • 将当前线程构造成一个Node节点,并利用CAS将其加入到同步队列到尾部,然后该节点对应到线程进入自旋状态
  • 自旋时,首先判断其前驱节点释放为头节点&是否成功获取同步状态,两个条件都成立,则将当前线程的节点设置为头节点,如果不是,则利用LockSupport.park(this)将当前线程挂起 ,等待被前驱节点唤醒

2.2、独占锁的释放

  • 调用入口方法release(arg)
  • 调用模版方法tryRelease(arg)释放同步状态
  • 获取当前节点的下一个节点
  • 利用LockSupport.unpark(currentNode.next.thread)唤醒后继节点(接获取的第四步)

       适当做个总结:在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。

3、共享式同步状态获取与释放

共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。

通过调用同步器的acquireShared(int arg)方法可以共享式地获取同步状态,该方法代码如下:

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0) // 获取状态
            doAcquireShared(arg);
 }
// 不停的循环获得共享锁
 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);
        }
    }

 在acquireShared(int arg)方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状态,tryAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是tryAcquireShared(int arg)方法返回值大于等于0。可以看到,在doAcquireShared(int arg)方法的自旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出.

     与独占式一样,共享式获取也需要释放同步状态,通过调用releaseShared(int arg)方法可以释放同步状态,该方法代码:

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

该方法在释放同步状态之后,将会唤醒后续处于等待状态的节点。对于能够支持多个线程同时访问的并发组件(比如Semaphore),它和独占式主要区别在于tryReleaseShared(int arg)方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS来保证的,因为释放同步状态的操作会同时来自多个线程

3.1、获取锁

  • 调用acquireShared(arg)入口方法
  • 进入tryAcquireShared(arg)模版方法获取同步状态,如果返返回值>=0,则说明同步状态(state)有剩余,获取锁成功直接返回
  • 如果tryAcquireShared(arg)返回值<0,说明获取同步状态失败,向队列尾部添加一个共享类型的Node节点,随即该节点进入自旋状态
  • 自旋时,首先检查前驱节点释放为头节点&tryAcquireShared()是否>=0(即成功获取同步状态)
  • 如果是,则说明当前节点可执行,同时把当前节点设置为头节点,并且唤醒所有后继节点
  • 如果否,则利用LockSupport.unpark(this)挂起当前线程,等待被前驱节点唤醒

3.2、释放锁

  • 调用releaseShared(arg)模版方法释放同步状态
  • 如果释放成,则遍历整个队列,利用LockSupport.unpark(nextNode.thread)唤醒所有后继节点

4、独占锁和共享锁在实现上的区别 

独占锁的同步状态值为1,即同一时刻只能有一个线程成功获取同步状态
共享锁的同步状态>1,取值由上层同步组件确定
独占锁队列中头节点运行完成后释放它的直接后继节点
共享锁队列中头节点运行完成后释放它后面的所有节点
共享锁中会出现多个线程(即同步队列中的节点)同时成功获取同步状态的情况
 

5、独占式超时获取同步状态?

       通过调用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超时获取同步状态,即在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则,返回false。该方法提供了传统Java同步操作(比如synchronized关键字)所不具备的特性。

       超时获取同步状态过程可以被视作响应中断获取同步状态过程的“增强版”,doAcquireNanos(int arg,long nanosTimeout)方法在支持响应中断的基础上,增加了超时获取的特性。针对超时获取,主要需要计算出需要睡眠的时间间隔nanosTimeout,为了防止过早通知,nanosTimeout计算公式为:nanosTimeout-=now-lastTime,其中now为当前唤醒时间,lastTime为上次唤醒时间,如果nanosTimeout大于0则表示超时时间未到,需要继续睡眠nanosTimeout纳秒,反之,表示已经超时.具体代码这里就不贴出来了。大致的流程图如下:

      

        独占式超时获取同步状态doAcquireNanos(int arg,long nanosTimeout)和独占式获取同步状态acquire(int args)在流程上非常相似,其主要区别在于未获取到同步状态时的处理逻辑。acquire(int args)在未获取到同步状态时,将会使当前线程一直处于等待状态,而doAcquireNanos(int arg,long nanosTimeout)会使当前线程等待nanosTimeout纳秒,如果当前线程在nanosTimeout纳秒内没有获取到同步状态,将会从等待逻辑中自动返回。
 

6、一些锁

6.1、重入锁

重入锁指的是当前线成功获取锁后,如果再次访问该临界区,则不会对自己产生互斥行为。Java中对ReentrantLock和synchronized都是可重入锁,synchronized由jvm实现可重入即使,ReentrantLock都可重入性基于AQS实现

同时,ReentrantLock还提供公平锁和非公平锁两种模式

6.1.1、ReentrantLock重入锁

重入锁的基本原理是判断上次获取锁的线程是否为当前线程,如果是则可再次进入临界区,如果不是,则阻塞。

由于ReentrantLock是基于AQS实现的,底层通过操作同步状态来获取锁,下面看一下非公平锁的实现逻辑:
java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire

 final boolean nonfairTryAcquire(int acquires) {
     //获取当前线程
            final Thread current = Thread.currentThread();
            //通过AQS获取同步状态
            int c = getState();
            //同步状态为0,说明临界区处于无锁状态,
            if (c == 0) {
	            //修改同步状态,即加锁
                if (compareAndSetState(0, acquires)) {
	                //将当前线程设置为锁的owner
                    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;
 }

重入锁的最主要逻辑就锁判断上次获取锁的线程是否为当前线程

6.1.2、非公平锁

非公平锁是指当锁状态为可用时,不管在当前锁上是否有其他线程在等待,新近线程都有机会抢占锁。

上述代码即为非公平锁和核心实现,可以看到只要同步状态为0,任何调用lock的线程都有可能获取到锁,而不是按照锁请求的FIFO原则来进行的。

6.1.3、公平锁

公平锁是指当多个线程尝试获取锁时,成功获取锁的顺序与请求获取锁的顺序相同,下面看一个ReentrantLock的实现:

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
	            //此处为公平锁的核心,即判断同步队列中当前节点是否有前驱节点
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            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.
        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());
    }

从上面的代码中可以看出,公平锁与非公平锁的区别仅在于是否判断当前节点是否存在前驱节点!hasQueuedPredecessors() ,由AQS可知,如果当前线程获取锁失败就会被加入到AQS同步队列中,那么,如果同步队列中的节点存在前驱节点,也就表明存在线程比当前节点线程更早的获取锁,故只有等待前面的线程释放锁后才能获取锁。
 

6.2、读写锁ReentrantReadWriteLock

Java提供了一个基于AQS到读写锁实现ReentrantReadWriteLock,该读写锁到实现原理是:将同步变量state按照高16位和低16位进行拆分,高16位表示读锁,低16位表示写锁。

6.2.1、写锁的获取与释放

写锁是一个独占锁,所以我们看一下ReentrantReadWriteLocktryAcquire(arg)的实现:

protected final boolean tryAcquire(int acquires) {

            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

上述代码的处理流程已经非常清晰:

  • 获取同步状态,并从中分离出低16为的写锁状态
  • 如果同步状态不为0,说明存在读锁或写锁
  • 如果存在读锁(c !=0 && w == 0),则不能获取写锁(保证写对读的可见性)
  • 如果当前线程不是上次获取写锁的线程,则不能获取写锁(写锁为独占锁)
  • 如果以上判断均通过,则在低16为写锁同步状态上利用CAS进行修改(增加写锁同步状态,实现可重入)
  • 将当前线程设置为写锁的获取线程

写锁的释放过程与独占锁基本相同:

protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}

在释放的过程中,不断减少读锁同步状态,只为同步状态为0时,写锁完全释放。

6.3.1、读锁的获取与释放

读锁是一个共享锁,获取读锁的步骤如下:

  • 获取当前同步状态
  • 计算高16为读锁状态+1后的值
  • 如果大于能够获取到的读锁的最大值,则抛出异常
  • 如果存在写锁并且当前线程不是写锁的获取者,则获取读锁失败
  • 如果上述判断都通过,则利用CAS重新设置读锁的同步状态
  • 读锁的获取步骤与写锁类似,即不断的释放写锁状态,直到为0时,表示没有线程获取读锁。

 

7、CAS与AQS

CAS是一种解决并发问题的思想,也就是先比较后替换,J.U.C通过自旋执行CAS操作实现线程安全的状态更新, AQS是Java并发包的一个底层框架,用某书里的话说,Lock面向用户,AQS面向Lock,也就是说AQS为各种Lock提供了底层的支持,AQS的最核心原理之一就是利用CAS更新同步状态

参考:

https://blog.csdn.net/ym123456677/article/details/80381354

https://blog.csdn.net/zhangdong2012/article/details/79983404

https://www.jianshu.com/p/cc308d82cc71(有空看一下,2019.08.27)

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
AQSAbstractQueuedSynchronizer)是Java中实现同步器的框架,它提供了一种基于FIFO队列的阻塞和唤醒机制。AQS的阻塞队列原理是通过CLH(Craig, Landin, and Hagersten)队列来实现的。 CLH队列是一种虚拟的双向链表,它仅存在节点之间的关联关系,而不存在队列的实例。每个请求共享资源的线程都会被封装成一个CLH队列的节点(Node)。当线程请求共享资源时,它会被添加到CLH队列的尾部,并进入阻塞状态。 当共享资源被占用时,其他线程请求该资源的线程会被放入CLH队列的末尾,即排队等待。这种排队等待的方式可以保证请求资源的线程按照FIFO的顺序获得资源,避免了饥饿现象。当资源释放后,AQS会自动唤醒队列中的下一个线程,使其获得资源并继续执行。 需要注意的是,AQS的同步队列(Sync queue)是一个双向链表,包括头节点(head)和尾节点(tail),用于后续的调度。而条件队列(Condition queue)是一个单向链表,只有在使用Condition时才会存在,并且可能会有多个条件队列。 总结一下,AQS实现阻塞队列的原理是通过CLH队列来实现的,当共享资源被占用时,请求资源的线程会被添加到CLH队列中排队等待。当资源释放后,AQS会自动唤醒队列中的下一个线程,使其获得资源并继续执行。同步队列用于后续的调度,而条件队列只在使用Condition时才会存在。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值