Java多线程(16)——JUC——locks系列(3)——AQS

目录

0.locks包简介

1.AQS简介

1.1 AQS是什么

1.2 AQS框架实现的一些点

1.3 AQS的层次结构

2.AQS原理及源码详解

2.1 状态

(1)state实现独占锁的图解

(2)state实现可重入锁的图解

2.2 节点Node

2.3 同步队列

2.4 加锁函数acquire

(1)tryAcquire(int)

(2)addWaiter(Node)

(3)acquireQueued(Node, int)

2.5 释放锁的函数

3.使用AQS实现自己的锁的案例


0.locks包简介

locks包下类如下(jdk1.8)

1.AQS简介

1.1 AQS是什么

  • AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。
  • JUC当中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列,条件队列,独占获取,共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer,简称AQS
  • AQS是一个用来构建锁和同步器的抽象同步框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是 基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。
  • 优势:
    • 基于AQS来构建同步器能带来许多好处。它不仅能极大地减少实现工作,而且也不必处理在多个位置上发生的竞争问题。
    • 在基于AQS构建的同步器中,只可能在一个时刻发生阻塞,从而降低上下文切换的开销,并提高吞吐量

1.2 AQS框架实现的一些点

  • AQS内部维护属性volatile int state(32位)
    • state表示资源的可用状态
    • state的三种访问方式
      • getState(),setState(),compareAndSetState()
  • AQS定义两种资源共享方式
    • Exclusive:独占,只有一个线程能执行,如ReentrantLock
    • Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch
  • AQS定义两种队列
    • 同步等待队列
    • 条件等待队列

1.3 AQS的层次结构

可以发现AQS是继承自AbstractOwnableSynchronizer这个类,我们来看看这个类的完整代码如下:

    public abstract class AbstractOwnableSynchronizer
            implements java.io.Serializable {

        /** Use serial ID even though all fields transient. */
        private static final long serialVersionUID = 3737899427754241961L;

        /**
         * 默认构造函数
         */
        protected AbstractOwnableSynchronizer() { }

        /**
         * 独占模式下拥有锁的线程
         */
        private transient Thread exclusiveOwnerThread;

        /**
         * 设置拥有锁的线程
         */
        protected final void setExclusiveOwnerThread(Thread thread) {
            exclusiveOwnerThread = thread;
        }

        /**
         * 获取拥有锁的线程
         */
        protected final Thread getExclusiveOwnerThread() {
            return exclusiveOwnerThread;
        }
    }
  • 可以发现该类只定义了独占锁的拥有线程,并提供了它的设置和获取方法

2.AQS原理及源码详解

2.1 状态

    //锁状态:1表示有线程占用锁    0表示没有任何线程占用锁
    private volatile int state;

    /**
     * 获取state的值
     */
    protected final int getState() {
        return state;
    }

    /**
     * 设置state的值
     */
    protected final void setState(int newState) {
        state = newState;
    }

    /**
     * 通过CAS原子地修改state的值
     */
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

我们都知道synchronized底层是使用的内核态的mutex锁,这样加锁和释放锁时就要在内核态和用户态切换,这样就使得synchronized效率比较低,而state是用来用户态实现独占锁的

  • 在state为0的时候,表示锁未被任何线程占用
  • 在state为1的时候,表示锁被某一个线程锁占用
  • 在state>1的时候,表示占用锁的线程多次加锁的这个次数即重入次数

以下通过图解来理解使用state实现独占锁和重入锁的过程:

(1)state实现独占锁的图解

  • 1.如果之前没人加过锁,那么state的值肯定是0,此时线程1就可以加锁成功。一旦线程1加锁成功了之后,就可以设置当前加锁线程是自己

  • 2.如果线程1加锁了之后,线程2跑过来加锁,state的值不是0,所以CAS操作将state从0变为1的过程会失败,因为state的值当前为1,说明已经有线程已经加锁了,接着线程2会看一下,是不是自己之前加的锁,当然不是了,“加锁线程”这个变量明确记录了是线程1占用了这个锁,所以线程2此时就是加锁失败。

  • 3.接着,线程2会将自己放入AQS中的同步等待队列,因为自己尝试加锁失败了,此时就要将自己放入队列中来等待,等待线程1释放锁之后唤醒它,自己就可以重新尝试加锁了

  • 4.接着,线程1在执行完自己的业务逻辑代码之后,就会释放锁,他释放锁的过程非常的简单,就是将AQS内的state变量的值递减1,如果state值为0,则彻底释放锁,会将“加锁线程”变量也设置为null

  • 5.接下来,会从同步队列的队头唤醒线程2重新尝试加锁。线程2现在就重新尝试加锁,这时还是用CAS操作将state从0变为1,此时就会成功,成功之后代表加锁成功,就会将state设置为1。此外,还要把“加锁线程”设置为线程2自己,同时线程2自己就从同步队列中出队了

(2)state实现可重入锁的图解

那么如果是线程1加锁以后,又再次去加锁会怎么样呢?以下是重入锁的实现逻辑:

所以state>1表示的是重入锁的数量,这样在释放的时候,如下:

  • 1.在第一次调用unlock释放锁的时候,将state先减1,然后判断state是否变为0,可以看到暂时还为1并没有变为0,就什么都不做,代表线程1只是释放了内部的一层重入锁

  • 2.在第二次调用unlock释放锁的时候,将state先减1,然后判断state是否变为0,变为0,就将加锁线程设置为null,代表线程1彻底释放锁

2.2 节点Node

    static final class Node {
        //共享模式的标记
        static final Node SHARED = new Node();
        //独占模式的标记
        static final Node EXCLUSIVE = null;

        //表示线程已被取消(等待超时或者被中断)
        static final int CANCELLED =  1;
        //表示后继线程需要被唤醒(unpaking)
        static final int SIGNAL    = -1;
        //表示当前节点不在同步队列中,它在条件队列中,结点线程等待在condition上,当被signal后,会从等待队列转移到同步到队列中
        static final int CONDITION = -2;
        //表示下一次共享式同步状态会被无条件地传播下去
        static final int PROPAGATE = -3;

        //等待状态,初始为0
        volatile int waitStatus;

        //前置节点
        volatile Node prev;

        //后置节点
        volatile Node next;

        //记录当前节点关联的线程(节点值)
        volatile Thread thread;

        /**
         * 对于同步阻塞队列
         *      为null表示EXCLUSIVE,即在独占模式下
         *      为SHARED表示在共享模式下
         *
         * 对于条件队列(单项链表)
         *      表示下一个在condition上等待的线程节点
         */
        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
        }

        //此构造函数在addWaiter方法中被使用
        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        //此构造函数被在ConditionObject内部类中使用
        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

Node其实既是AQS的同步阻塞队列中的节点,也是条件队列中的节点

  • 作为同步阻塞队列中的节点是一个双链表节点:
  • 作为条件队列中的节点:(以下仅仅是表示出了节点,上面的其他属性还在节点中)

对nextWaiter的特别说明:

其实nextWaiter在同步阻塞队列中和条件队列中充当了不同的角色

  • 在同步阻塞队列中
    • nextWaiter表示该节点是独占模式还是共享模式
  • 在条件队列中
    • nextWaiter表示指向下一个等待者的指针

节点中除过包含指向前后节点的指针(prev和next),还包括需要同步的线程本身(thread)和其等待状态(waitStatus)

  • 变量waitStatus则表示当前Node结点的等待状态,共有以下5种取值
    • CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。

    • SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。

    • CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

    • PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。

    • 0:新结点入队时的默认状态。

注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。

2.3 同步队列

AQS核心思想是:

  • 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态(state=1)。
  • 如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到同步队列中。

同步队列是一个带有头结点(头结点的值thread=null)的双向链表

 

 

 

2.4 加锁函数acquire

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

函数流程:

  • 1.tryAcquire()尝试直接去获取资源,如果成功则直接返回(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加锁一次,而CLH队列中可能还有别的线程在等待);
  • 2.如果tryAcquire加锁不成功,addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
  • 3.acquireQueued()使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  • 4.如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

(1)tryAcquire(int)

  • 此方法尝试去获取独占资源。如果获取成功,则直接返回true,否则直接返回false。
  • AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现,AQS这里只定义了一个接口,具体资源的获取交由自定义同步器去实现
  • 这里之所以没有定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。

(2)addWaiter(Node)

    /**
     * 此方法用于将当前线程加入到等待队列的队尾,并返回当前线程所在的结点。
     * @param mode  共享或者独占模式
     * @return
     */
    private Node addWaiter(Node mode) {
        //创建一个当前线程的Node节点,并设置它的模式为传入的参数mode
        Node node = new Node(Thread.currentThread(), mode);
        //取得尾节点
        Node pred = tail;
        //尾结点不为空,即同步阻塞队列不为空,就将上述新建的节点node插入到队列的尾部
        if (pred != null) {
            node.prev = pred;
            //原子地设置尾节点为node
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //初始化队列,并将节点加入到队列尾部
        enq(node);
        return node;
    }

可以看到addWaiter又使用了enq(Node)

    /**
     * 此方法在是在同步阻塞队列没有被初始化的时候(tail=null),先初始化队列
     *          如果已经被初始化过,就将节点加入到队列尾部
     *
     * 可以看到队列被初始化是创建一个新的空节点作为头结点,即同步阻塞队列是带头结点的双向链表,在这里就可以看到
     * @param node
     * @return
     */
    private Node enq(final Node node) {
        //CAS"自旋",直到成功加入队尾
        for (;;) {
            Node t = tail;
            //队列没有被初始化,初始化队列 head=tail=new Node()
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            //队列已经被初始化,将节点加入到队列尾部
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

    /**
     * 通过Unsafe的CAS来修改头结点
     */
    private final boolean compareAndSetHead(Node update) {
        return unsafe.compareAndSwapObject(this, headOffset, null, update);
    }

    /**
     * 通过Unsafe的CAS来修改尾结点
     */
    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }

同步阻塞队列初始化:

  • 可以看到同步阻塞队列在初始化的时候,是将head和tail都指向一个空节点(即不存储任何线程的节点),所以同步阻塞队列是一个带头结点的双向链表

for(;;)说明:

  • 这里其实是使用了一种经典用法——volatile+CAS+自旋
  • head和tail都是volatile变量,保证了可见性和有序性,然后通过CAS保证了原子性,通过自旋来阻塞到直到修改成功为止
  • 这种用法也可在Unsafe类的getAndAddInt方法或者AtomicInteger类的getAndUpdate方法中找到

(3)acquireQueued(Node, int)

通过tryAcquire()和addWaiter(),该线程获取资源失败,已经被放入等待队列尾部了

 

 

  • acquireQueued内部也是一个死循环,只有前驱结点是头结点的结点,也就是老二结点,才有机会去tryAcquire;
    • 若tryAcquire成功,表示获取同步状态成功,将此结点设置为头结点;
    • 若是非老二结点,或者tryAcquire失败,则进入shouldParkAfterFailedAcquire去判断判断当前线程是否应该阻塞,
      • 若可以,调用parkAndCheckInterrupt阻塞当前线程,直到被中断或者被前驱结点唤醒。
      • 若还不能休息,继续循环。

 

 

 

2.5 释放锁的函数

 

 

3.使用AQS实现自己的锁的案例

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

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值