浅谈AQS --JAVA

一   AQS能干什么

 AQS是对列同步器AbustactQueuedSynchronizer的简称,位于juc包下。

你所看见的锁包括 ReentrantLock、ReentrantReadWriteLock,还有同步组件 CyclicBarrier等,它的内部都是通过AQS实现的。通过学习AQS,你可以写出自己的锁与同步组件。

二AQS内部实现

1.同步队列(一个FIFO双向队列)

作用:主要作用是用来存放在锁上阻塞的线程,当一个线程尝试获取锁时,如果锁已经被占用,那么当前线程就会被构造成一个Node节点假如到同步队列的尾部,

注意:同步队列实际是不存在的,在AQS源码中只定义了节点的前一个节点与后一个节点。

           waitStatus值代表的含义。

 1 static final class Node {
 2         /** waitStatus值,表示线程已被取消(等待超时或者被中断)*/
 3         static final int CANCELLED =  1;
 4         /** waitStatus值,表示后继线程需要被唤醒(unpaking)*/
 5         static final int SIGNAL    = -1;
 6         /**waitStatus值,表示结点线程等待在condition上,当被signal后,会从等待队列转移到同步到队列中 */
 7         /** waitStatus value to indicate thread is waiting on condition */
 8         static final int CONDITION = -2;
 9        /** waitStatus值,表示下一次共享式同步状态会被无条件地传播下去
10         static final int PROPAGATE = -3;
11         /** 等待状态,初始为0 */
12         volatile int waitStatus;
13         /**当前结点的前驱结点 */
14         volatile Node prev;
15         /** 当前结点的后继结点 */
16         volatile Node next;
17         /** 与当前结点关联的排队中的线程 */
18         volatile Thread thread;
19         /** ...... */
20     }

  2.AQS提供的便利

在AQS源码中,同步状态通过设定一个int类型的state来实现。同步状态可以用来判断当前线程是否是安全的。对于同步状态的修改与获取,AQS已经帮你实现好了。

getState()

setState()

compareAndSetState()

 AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

  不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。

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

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

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

tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false

实现自定义同步器时,还需要调用AQS提供的模板方法

 

3  独占式同步状态的获取与释放

      lock方法一般会直接代理到acquire上

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

 a.首先,调用使用者重写的tryAcquire方法,若返回true,意味着获取同步状态成功,后面的逻辑不再执行;若返回false,也就是获取同步状态失败,进入b步骤;

 b.此时,获取同步状态失败,构造独占式同步结点,通过addWatiter将此结点添加到同步队列的尾部(此时可能会有多个线程结点试图加入同步队列尾部,需要以线程安全的方  式添加);

 c.该结点以在队列中尝试获取同步状态,若获取不到,则阻塞结点线程,直到被前驱结点唤醒或者被中断。

addWaiter

    为获取同步状态失败的线程,构造成一个Node结点,添加到同步队列尾部

 private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);//构造结点
        //指向尾结点tail
        Node pred = tail;
        //如果尾结点不为空,CAS快速尝试在尾部添加,若CAS设置成功,返回;否则,eng。
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

先cas快速设置,若失败,进入enq方法  

  将结点添加到同步队列尾部这个操作,同时可能会有多个线程尝试添加到尾部,是非线程安全的操作。

  以上代码可以看出,使用了compareAndSetTail这个cas操作保证安全添加尾结点。

enq方法

 private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { //如果队列为空,创建结点,同时被head和tail引用
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {//cas设置尾结点,不成功就一直重试
                    t.next = node;
                    return t;
                }
            }
        }
    }

  enq内部是个死循环,通过CAS设置尾结点,不成功就一直重试。很经典的CAS自旋的用法。这是一种乐观的并发策略

最后,看下acquireQueued方法

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)) {//如果前驱结点是头结点,才tryAcquire,其他结点是没有机会tryAcquire的。
                    setHead(node);//获取同步状态成功,将当前结点设置为头结点。
                    p.next = null; // 方便GC
                    failed = false;
                    return interrupted;
                }
                // 如果没有获取到同步状态,通过shouldParkAfterFailedAcquire判断是否应该阻塞,parkAndCheckInterrupt用来阻塞线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

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

shouldParkAfterFailedAcquire

shouldParkAfterFailedAcquire用来判断当前结点线程是否能休息
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //获取前驱结点的waitStatus值 
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)//若前驱结点的状态是SIGNAL,意味着当前结点可以被安全地park
            return true;
        if (ws > 0) {
        // ws>0,只有CANCEL状态ws才大于0。若前驱结点处于CANCEL状态,也就是此结点线程已经无效,从后往前遍历,找到一个非CANCEL状态的结点,将自己设置为它的后继结点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {  
            // 若前驱结点为其他状态,将其设置为SIGNAL状态
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }    

  若shouldParkAfterFailedAcquire返回true,也就是当前结点的前驱结点为SIGNAL状态,则意味着当前结点可以放心休息,进入parking状态了。parkAncCheckInterrupt阻塞线程并处理中断。

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);//使用LockSupport使线程进入阻塞状态
        return Thread.interrupted();// 线程是否被中断过
    }

  至此,关于acquire的方法源码已经分析完毕,我们来简单总结下

    a.首先tryAcquire获取同步状态,成功则直接返回;否则,进入下一环节;

    b.线程获取同步状态失败,就构造一个结点,加入同步队列中,这个过程要保证线程安全;

    c.加入队列中的结点线程进入自旋状态,若是老二结点(即前驱结点为头结点),才有机会尝试去获取同步状态;否则,当其前驱结点的状态为SIGNAL,线程便可安心休息,进入阻塞状态,直到被中断或者被前驱结点唤醒。

释放同步状态--release()

  当前线程执行完自己的逻辑之后,需要释放同步状态,来看看release方法的逻辑

 public final boolean release(int arg) {
        if (tryRelease(arg)) {//调用使用者重写的tryRelease方法,若成功,唤醒其后继结点,失败则返回false
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);//唤醒后继结点
            return true;
        }
        return false;
    }
  unparkSuccessor:唤醒后继结点 
 1 private void unparkSuccessor(Node node) {
 2         //获取wait状态
 3         int ws = node.waitStatus;
 4         if (ws < 0)
 5             compareAndSetWaitStatus(node, ws, 0);// 将等待状态waitStatus设置为初始值0
 6         Node s = node.next;//后继结点
 7         if (s == null || s.waitStatus > 0) {//若后继结点为空,或状态为CANCEL(已失效),则从后尾部往前遍历找到一个处于正常阻塞状态的结点     进行唤醒
 8             s = null;
 9             for (Node t = tail; t != null && t != node; t = t.prev)
10                 if (t.waitStatus <= 0)
11                     s = t;
12         }
13         if (s != null)
14             LockSupport.unpark(s.thread);//使用LockSupprot唤醒结点对应的线程
15     }    

  release的同步状态相对简单,需要找到头结点的后继结点进行唤醒,若后继结点为空或处于CANCEL状态,从后向前遍历找寻一个正常的结点,唤醒其对应线程。

 

 

4  共享式

  共享式:共 享式地获取同步状态。对于独占式同步组件来讲,同一时刻只有一个线程能获取到同步状态,其他线程都得去排队等待,其待重写的尝试获取同步状态的方法 tryAcquire返回值为boolean,这很容易理解;对于共享式同步组件来讲,同一时刻可以有多个线程同时获取到同步状态,这也是“共享”的意义 所在。其待重写的尝试获取同步状态的方法tryAcquireShared返回值为int。

 protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }

  1.当返回值大于0时,表示获取同步状态成功,同时还有剩余同步状态可供其他线程获取;

  2.当返回值等于0时,表示获取同步状态成功,但没有可用同步状态了;

  3.当返回值小于0时,表示获取同步状态失败。

  获取同步状态--acquireShared  

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)//返回值小于0,获取同步状态失败,排队去;获取同步状态成功,直接返回去干自己的事儿。
            doAcquireShared(arg);
    }

  doAcquireShared

 1  private void doAcquireShared(int arg) {
 2         final Node node = addWaiter(Node.SHARED);//构造一个共享结点,添加到同步队列尾部。若队列初始为空,先添加一个无意义的傀儡结点,再将新节点添加到队列尾部。
 3         boolean failed = true;//是否获取成功
 4         try {
 5             boolean interrupted = false;//线程parking过程中是否被中断过
 6             for (;;) {//死循环
 7                 final Node p = node.predecessor();//找到前驱结点
 8                 if (p == head) {//头结点持有同步状态,只有前驱是头结点,才有机会尝试获取同步状态
 9                     int r = tryAcquireShared(arg);//尝试获取同步装填
10                     if (r >= 0) {//r>=0,获取成功
11                         setHeadAndPropagate(node, r);//获取成功就将当前结点设置为头结点,若还有可用资源,传播下去,也就是继续唤醒后继结点
12                         p.next = null; // 方便GC
13                         if (interrupted)
14                             selfInterrupt();
15                         failed = false;
16                         return;
17                     }
18                 }
19                 if (shouldParkAfterFailedAcquire(p, node) &&//是否能安心进入parking状态
20                     parkAndCheckInterrupt())//阻塞线程
21                     interrupted = true;
22             }
23         } finally {
24             if (failed)
25                 cancelAcquire(node);
26         }
27     }

  大体逻辑与独占式的acquireQueued差距不大,只不过由于是共享式,会有多个线程同时获取到线程,也可能同时释放线程,空出很多同步状态,所以当排队中的老二获取到同步状态,如果还有可用资源,会继续传播下去。

  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) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

  释放同步状态--releaseShared

 public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();//释放同步状态
            return true;
        }
        return false;
    }

  doReleaseShared

 private void doReleaseShared() {
        for (;;) {//死循环,共享模式,持有同步状态的线程可能有多个,采用循环CAS保证线程安全
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;          
                    unparkSuccessor(h);//唤醒后继结点
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                
            }
            if (h == head)              
                break;
        }
    }

  代码逻辑比较容易理解,需要注意的是,共享模式,释放同步状态也是多线程的,此处采用了CAS自旋来保证。

 

 

三 自定义同步组件的实现。

  编写一个允许最大访问量为3 的共享锁。

public class ThreeLock implements Lock {
    private  Sync sync = new Sync(3); //最大访问量为3

    private  static  class  Sync extends AbstractQueuedSynchronizer{

        public Sync(int count) {
            if (count <= 0) {
                throw new IllegalArgumentException("count must >0");
            }
            setState(count);
        }
        //共享式的获取锁
        @Override
        protected int tryAcquireShared(int reduceCount) {
            for (;;){
                int count = getState();
                int newCount = count - reduceCount;
                if(newCount<0||compareAndSetState(count,newCount)){
                    return  newCount;
                }
            }
        }
        //共享锁的释放
        @Override
        protected boolean tryReleaseShared(int returnCount) {
            for (;;){
                int count = getState();
                int newCount = count+returnCount;
                if(compareAndSetState(count,newCount)){
                    return true;
                }
            }
        }
    }


    @Override
    public void lock() {
        sync.acquireShared(1);  //每次获取锁都是state-1
    }
    @Override
    public void unlock() {
        sync.releaseShared(1);    //每次释放锁都是state+1
    }
xxxx略去一些方法
}

测试:

public class TestThreeLock {
    public static void main(String[] args) {
      new TestThreeLock().test();
    }

    public  void test(){

        Lock threeLock = new ThreeLock();
        Runnable rn = new Runnable() {
            @Override
            public void run() {
                threeLock.lock();
                System.out.println(Thread.currentThread().getName());

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    threeLock.unlock();
                }
            }
        };
        
        for(int i=0;i<10;i++){
            new Thread(rn).start();
        }

    }
}

结果:线程名称以每三个一组的形式打印出来,测试成功!

转载:https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html

 

猜你喜欢

转载自blog.csdn.net/qq_36647176/article/details/85238441