zoukankan      html  css  js  c++  java
  • 并发编程学习笔记(5)----AbstractQueuedSynchronizer(AQS)原理及使用

    (一)什么是AQS?

    阅读java文档可以知道,AbstractQueuedSynchronizer是实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架,它是一个依靠单个原子 int 值来表示状态的大多数同步器的一个基础类。在jdk中他的实现的类有Semaphore,ReentrantLock,CountDownLatch,ReentrantReadWriteLock等等很多的实现。

    (二)原理

      它通过实现一个volatile int state来维护线程的状态,并使用一个双向链表来维护多个线程的等待队列。一般将子类作为一个非公共的内部帮助器类。它存在有独享和共享两种模式,在独享模式下,当锁被占用时,其他线程试图获取锁一定不会成功,共享模式下其他线程获取锁可能会成功。(读写锁读-读不互斥,读-写,写-写互斥)

    它提供了getState()setState(int) ,compareAndSetState(int, int) 三个方法来获取和修改单个原子状态的方法,在使用的是我,我们只需要重写以下几个方法即可,

    (三)源码实现

    acquire:  

      根据调用流程acquire-release来一步步分析独享模式的源码,共享模式会在后面的学习中补上,首先从acquire,锁的入口开始。

    public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
      1)tryAcquire(int)最少会执行一次尝试去获取资源,如果获取到锁,则返回true,否则放回false。
    protected boolean tryAcquire(int arg) {
            throw new UnsupportedOperationException();
        }
    tryAcquire(int)在AQS源码中直接直接抛出了一个异常,这是因为我们前面说过AQS是一个框架,使用的时候是需要我们去实现自己的核心部分的,tryAcquire(int)这里调用的实际上是在使用时我们重写的方法,通过操作state
    来进行我们自己的实现的。
      2)addWaiter(Node.EXCLUSIVE)将一个独占模式的线程加入到队列中队尾。
    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;
        }

    这里的Node包含了当前线程本身以及线程的状态及以下基本信息,这里就直接看注释,我就不一一写出来了

    /** Marker to indicate a node is waiting in shared mode */
            static final Node SHARED = new Node();
            /** Marker to indicate a node is waiting in exclusive mode */
            static final Node EXCLUSIVE = null;
    
            /** waitStatus value to indicate thread has cancelled */
            static final int CANCELLED =  1;
            /** waitStatus value to indicate successor's thread needs unparking */
            static final int SIGNAL    = -1;
            /** waitStatus value to indicate thread is waiting on condition */
            static final int CONDITION = -2;
            /**
             * waitStatus value to indicate the next acquireShared should
             * unconditionally propagate
             */
            static final int PROPAGATE = -3;
    
            /**
             * Status field, taking on only the values:
             *   SIGNAL:   值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。  
             *   CANCELLED: 值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化
    * CONDITION: 值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁
          * PROPAGATE: 值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。

      这里特别说下一下0状态表示为初始化状态。

      addWaiter()方法创建一个当前线程节点,当尾节点不为空时,将当前节点的prev指向双向链表的尾节点,并将当前节点通过cas的方式设置为尾节点,将尾节点的next指向当前节点,成功将整个链表连起来,这就是将当前线程加入到队列的过程,返回当前创建的节点。当尾节点为空时,调用enq方法。

      enq(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;
                    }
                }
            }
        }

      for()循环表示自旋cas,知道成功将节点添加到队列为止,如果此时的尾节点tail为空,则创建一个空标志性的节点作为head节点,并将尾节点t也指向它,当tail不为空时,就按照前面的正常的添加节点的方式将当前节点添加到队列尾部。添加成功,退出循环,返回当前节点。

      4)acquireQueued()使线程在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。

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

      当线程获取资源失败,并且已经进入了队列中了,那么此时的线程就需要等待了,只等待其他线程执行完成之后,再唤醒队列中的线程时,唤醒到该线程时,该线程才可以继续执行,failed标记线程是否拿到锁,interrupted标记线程是否被中断过。自旋获取锁,

      第一步,拿到当前的上一个节点,即前驱,如果上一个节点是头节点,则有资格去获取锁,可能是上一个线程释放锁之后轮到第二个线程,当有资格获取锁时,将当前节点设置成头节点,此时前面的一个节点已经出了队列了,并将前一个节点置为空,为了方便垃圾回收,返回等待过程中该线程是否被中断过。

      shouldParkAfterFailedAcquire()这个方法,我们也需要理解一下:该方法主要是用于检查状态,判读自己是否可以进入等待队列。

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

      从方法可以看到,当前节点的前一个节点的状态为通知状态时,返回true,即表示已经告诉了前节点,当释放之后叫醒当前节点,当ws大于0时,表示当前节点的线程被中断或者是等待超时,此时需要将该节点从队列中去除,即node.prev = pred = pred.prev和pred.next=node,表示将pred节点删除,替换成了pred的prev节点;else 则将pred节点设置成为通知唤醒状态。让前驱在执行完成后通知当前节点。

    parkAndCheckInterrupt()使线程真正的进入等待状态。

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

      park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt(),。需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。

    看了shouldParkAfterFailedAcquire()和parkAndCheckInterrupt(),现在让我们再回到acquireQueued(),总结下该函数的具体流程:

    结点进入队尾后,检查状态,找到安全休息点;调用park()进入waiting状态,等待unpark()或interrupt()唤醒自己;被唤醒后,看自己是不是有资格能拿到号。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程1。

    release:
    接下来看当线程释放锁,即Lock调用unlock时,源码情况:
    public final boolean release(int arg) {
            if (tryRelease(arg)) {
                Node h = head;
                if (h != null && h.waitStatus != 0)
                    unparkSuccessor(h);
                return true;
            }
            return false;
        }
    release的实现就相对简单了,首先同样是调用我们用户自己实现的tryRelease()方法,当tryReleace返回true时,判断头节点状态是否等于初始值,不等于初始值时调用unparkSuccessor(node)方法释放锁,否则表示资源已经呗释放,直接返回true。
    unparkSuccessor(node):
     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);
        }

    这个方法是用于唤醒一个线程,如果这个线程存在的话,ws小于0,表示当前线程是可以呗唤醒的,处于等待状态的线程,此时修改当前线程的状态为原始状态,如果当前线程的下一个线程为空,或者是状态大于0,表示处于CANCELLED状态,此时循环链表,找到最后一个状态小于0的线程,将找到的线程赋值给s,最后如果成功找到s,则使用unpark唤醒s节点的线程,这样就实现了release,

    总结:release()是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。

    (四) 重写tryAcquire(int) 和 tryRelease(int) 方法实现一个简易的可重入锁

    这里直接使用java文档的示例代码:

     class Mutex implements Lock, java.io.Serializable {
    
        // Our internal helper class
        private static class Sync extends AbstractQueuedSynchronizer {
          // Report whether in locked state
          protected boolean isHeldExclusively() { 
            return getState() == 1; 
          }
    
          // Acquire the lock if state is zero
          public boolean tryAcquire(int acquires) {
            assert acquires == 1; // Otherwise unused
           if (compareAndSetState(0, 1)) {
             setExclusiveOwnerThread(Thread.currentThread());
             return true;
           }
           return false;
          }
    
          // Release the lock by setting state to zero
          protected boolean tryRelease(int releases) {
            assert releases == 1; // Otherwise unused
            if (getState() == 0) throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
          }
           
          // Provide a Condition
          Condition newCondition() { return new ConditionObject(); }
    
          // Deserialize properly
          private void readObject(ObjectInputStream s) 
            throws IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
          }
        }

    // The sync object does all the hard work. We just forward to it.
        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));
        }

    由于官网的文档实现的是不可重入的锁,所以本人也自己实现了一个可重入锁,下面是代码:

    package com.ucar.work;
    
    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.AbstractQueuedSynchronizer;
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    
    /**
     * @since: 2018/9/27
     * @version: 1.0
     * Copyright: Copyright (c) 2018
     */
    public class MyLock implements Lock, java.io.Serializable {
    
        private static class Sync extends AbstractQueuedSynchronizer {
            @Override
            protected boolean isHeldExclusively() {
                return getState() == 1;
            }
    
            @Override
            public boolean tryAcquire(int acquires) {
               int stat = getState();
               Thread thread = Thread.currentThread();
               //表示线程第一次进如,直接加锁
               if (stat == 0) {
    
                   compareAndSetState(0,stat+acquires);
                   setExclusiveOwnerThread(thread);
                   return true;
                   //当前线程等于getExclusiveOwnerThread()表示线程重入。返回true表示可以获取锁
               } else if (stat != 0  && thread == getExclusiveOwnerThread()){
                   return true;
               }
               //否则返回false表示获取锁失败
               return false;
            }
    
            @Override
            protected boolean tryRelease(int releases) {
                int stat = getState();
                //stat==0表示该锁状态为释放状态,不能去释放
                if (stat == 0) {
                    setExclusiveOwnerThread(null);
                    return false;
                } else {
                    //重入的情况下减去重入次数
                    setState(getState()-releases);
                }
                //返回可释放信号
                return true;
            }
    
            Condition newCondition() { return new ConditionObject(); }
    
            private void readObject(ObjectInputStream s)
                    throws IOException, ClassNotFoundException {
                s.defaultReadObject();
                setState(0);
            }
        }
    
        private final Sync sync = new Sync();
    
        @Override
        public void lock()                { sync.acquire(1); }
        @Override
        public boolean tryLock()          { return sync.tryAcquire(1); }
        @Override
        public void unlock()              { sync.release(1); }
        @Override
        public Condition newCondition()   { return sync.newCondition(); }
        public boolean isLocked()         { return sync.isHeldExclusively(); }
        public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
        @Override
        public void lockInterruptibly() throws InterruptedException {
            sync.acquireInterruptibly(1);
        }
        @Override
        public boolean tryLock(long timeout, TimeUnit unit)
                throws InterruptedException {
            return sync.tryAcquireNanos(1, unit.toNanos(timeout));
        }
    }

    实现细节注释已经写清楚了,这里就不在叙述了。

    至于共享模式的AQS,会在以后的学习中继续记录,不足之处,忘各位大佬多多指点。

    原文  并发编程学习笔记(5)----AbstractQueuedSynchronizer(AQS)原理及使用

  • 相关阅读:
    如何开始DDD(续)
    如何开始DDD
    ThinkNet终于见面了
    [Umbraco] umbraco中如何分页
    ETL 工具下载全集 包括 Informatica Datastage Cognos( 持续更新)
    js时间对比-转化为几天前,几小时前,几分钟前
    原生JS实现返回顶部和滚动锚点
    JSONP原理及简单实现 可做简单插件使用
    CSS3 transition效果 360度旋转 旋转放大 放大 移动
    js获取url的常用方法
  • 原文地址:https://www.cnblogs.com/xiaoshen666/p/10868774.html
Copyright © 2011-2022 走看看