JUC解析-AQS抽象队列同步器

抽象队列同步器(AQS-AbstractQueuedSynchronizer)

网站设计制作过程拒绝使用模板建站;使用PHP+MYSQL原生开发可交付网站源代码;符合网站优化排名的后台管理系统;成都网站建设、网站制作收费合理;免费进行网站备案等企业网站建设一条龙服务.我们是一家持续稳定运营了十余年的创新互联建站网站建设公司。

从名字上来理解:

  • 抽象:是抽象类,具体由子类实现
  • 队列:数据结构是队列,使用队列存储数据
  • 同步:基于它可以实现同步功能

我们就从这几个方面来入手解读,但首先,我们得先知道以下几个它的特点,以便于理解

AbstractQueuedSynchronizer特点

1.AQS可以实现独占锁和共享锁。

2.独占锁exclusive是一个悲观锁。保证只有一个线程经过一个阻塞点,只有一个线程可以获得锁。

3.共享锁shared是一个乐观锁。可以允许多个线程阻塞点,可以多个线程同时获取到锁。它允许一个资源可以被多个读操作,或者被一个写操作访问,但是两个操作不能同时访问。

4.AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state = 0无锁。它提供了三个方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))来对同步状态state进行操作,可以确保对state的操作是安全的。

5.AQS是通过一个CLH队列实现的(CLH锁即Craig, Landin, and Hagersten (CLH) locks,CLH锁是一个自旋锁,能确保无饥饿性,提供先来先服务的公平性。CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。)

抽象

我们来扒一扒源码可以看到它继承于AbstractOwnableSynchronizer它是一个抽象类.

 
 
 
 
  1. public abstract class AbstractQueuedSynchronizer
  2.     extends AbstractOwnableSynchronizer
  3.     implements java.io.Serializable

AQS内部使用了一个volatile的变量state来作为资源的标识。同时定义了几个获取和改变state的protected方法,子类可以覆盖这些方法来实现自己的逻辑.

可以看到类中为我们提供了几个protected级别的方法,它们分别是:

 
 
 
 
  1. //创建一个队列同步器实例,初始state是0
  2. protected AbstractQueuedSynchronizer() { }
  3. //返回同步状态的当前值。
  4. protected final int getState() {
  5.         return state;
  6. }
  7. //设置同步状态的值
  8. protected final void setState(int newState) {
  9.         state = newState;
  10.     }
  11. //独占方式。尝试获取资源,成功则返回true,失败则返回false。
  12. protected boolean tryAcquire(int arg) {
  13.         throw new UnsupportedOperationException();
  14.     }    
  15.     
  16.     
  17. //独占方式。尝试释放资源,成功则返回true,失败则返回false。
  18. protected boolean tryRelease(int arg) {
  19.         throw new UnsupportedOperationException();
  20.     }
  21.     
  22.     
  23. //共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源
  24. protected int tryAcquireShared(int arg) {
  25.         throw new UnsupportedOperationException();
  26.     }    
  27.     
  28.     
  29. //共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
  30. protected boolean tryReleaseShared(int arg) {
  31.         throw new UnsupportedOperationException();
  32.     }
  33.     

这些方法虽然都是protected方法,但是它们并没有在AQS具体实现,而是直接抛出异常,AQS实现了一系列主要的逻辑 由此可知,AQS是一个抽象的用于构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的同步器,比如我们提到的ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。

我们自己也能利用AQS非常轻松容易地构造出自定义的同步器,只要子类实现它的几个protected方法就可以了.

队列

AQS类本身实现的是具体线程等待队列的维护(如获取资源失败入队/唤醒出队等)。它内部使用了一个先进先出(FIFO)的双端队列(CLH),并使用了两个指针head和tail用于标识队列的头部和尾部。其数据结构如图:

队列并不是直接储存线程,而是储存拥有线程的Node节点。

我们来看看Node的结构:

 
 
 
 
  1. static final class Node {
  2.     // 标记一个结点(对应的线程)在共享模式下等待
  3.     static final Node SHARED = new Node();
  4.     // 标记一个结点(对应的线程)在独占模式下等待
  5.     static final Node EXCLUSIVE = null; 
  6.     // waitStatus的值,表示该结点(对应的线程)已被取消
  7.     static final int CANCELLED = 1; 
  8.     // waitStatus的值,表示后继结点(对应的线程)需要被唤醒
  9.     static final int SIGNAL = -1;
  10.     // waitStatus的值,表示该结点(对应的线程)在等待某一条件
  11.     static final int CONDITION = -2;
  12.     
  13.     //waitStatus的值,表示有资源可用,新head结点需要继续唤醒后继结点
  14.     //(共享模式下,多线程并发释放资源,而head唤醒其后继结点后,
  15.     //需要把多出来的资源留给后面的结点;设置新的head结点时,会继续唤醒其后继结点)
  16.     static final int PROPAGATE = -3;
  17.     // 等待状态,取值范围,-3,-2,-1,0,1
  18.     volatile int waitStatus;
  19.     volatile Node prev; // 前驱结点
  20.     volatile Node next; // 后继结点
  21.     volatile Thread thread; // 结点对应的线程
  22.     Node nextWaiter; // 等待队列里下一个等待条件的结点
  23.     // 判断共享模式的方法
  24.     final boolean isShared() {
  25.         return nextWaiter == SHARED;
  26.     }
  27.     Node(Thread thread, Node mode) {     // Used by addWaiter
  28.         this.nextWaiter = mode;
  29.         this.thread = thread;
  30.     }
  31.     // 其它方法忽略,可以参考具体的源码
  32. }
  33. // AQS里面的addWaiter私有方法
  34. private Node addWaiter(Node mode) {
  35.     // 使用了Node的这个构造函数
  36.     Node node = new Node(Thread.currentThread(), mode);
  37.     // 其它代码省略
  38. }

过Node我们可以实现两个队列,一是通过prev和next实现CLH队列(线程同步队列,双向队列),二是nextWaiter实现Condition条件上的等待线程队列(单向队列),这个Condition主要用在ReentrantLock类中

同步

两种同步方式:

  • 独占模式(Exclusive):资源是独占的,一次只能一个线程获取。如ReentrantLock。
  • 共享模式(Share):同时可以被多个线程获取,具体的资源个数可以通过参数指定。如Semaphore/CountDownLatch。

同时实现两种模式的同步类,如ReadWriteLock

获取资源

获取资源的入口是acquire(int arg)方法。arg是要获取的资源的个数,在独占模式下始终为1。

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

首先调用tryAcquire(arg)尝试去获取资源。前面提到了这个方法是在子类具体实现的如果获取资源失败,就通过addWaiter(Node.EXCLUSIVE)方法把这个线程插入到等待队列中。其中传入的参数代表要插入的Node是独占式的。这个方法的具体实现:

 
 
 
 
  1. private Node addWaiter(Node mode) {
  2.     // 生成该线程对应的Node节点
  3.     Node node = new Node(Thread.currentThread(), mode);
  4.     // 将Node插入队列中
  5.     Node pred = tail;
  6.     if (pred != null) {
  7.         node.prev = pred;
  8.         // 使用CAS尝试,如果成功就返回
  9.         if (compareAndSetTail(pred, node)) {
  10.             pred.next = node;
  11.             return node;
  12.         }
  13.     }
  14.     // 如果等待队列为空或者上述CAS失败,再自旋CAS插入
  15.     enq(node);
  16.     return node;
  17. }
  18. //AQS中会存在多个线程同时争夺资源的情况,
  19. //因此肯定会出现多个线程同时插入节点的操作,
  20. //在这里是通过CAS自旋的方式保证了操作的线程安全性。
  21. // 自旋CAS插入等待队列
  22. private Node enq(final Node node) {
  23.     for (;;) {
  24.         Node t = tail;
  25.         if (t == null) { // Must initialize
  26.             if (compareAndSetHead(new Node()))
  27.                 tail = head;
  28.         } else {
  29.             node.prev = t;
  30.             if (compareAndSetTail(t, node)) {
  31.                 t.next = node;
  32.                 return t;
  33.             }
  34.         }
  35.     }
  36. }

若设置成功就代表自己获取到了锁,返回true。状态为0设置1的动作在外部就有做过一次,内部再一次做只是提升概率,而且这样的操作相对锁来讲不占开销。如果状态不是0,则判定当前线程是否为排它锁的Owner,如果是Owner则尝试将状态增加acquires(也就是增加1),如果这个状态值越界,则会抛出异常提示,若没有越界,将状态设置进去后返回true(实现了类似于偏向的功能,可重入,但是无需进一步征用)。如果状态不是0,且自身不是owner,则返回false。

现在通过addWaiter方法,已经把一个Node放到等待队列尾部了。而处于等待队列的结点是从头结点一个一个去获取资源的。具体的实现我们来看看acquireQueued方法:

 
 
 
 
  1. final boolean acquireQueued(final Node node, int arg) {
  2.     boolean failed = true;
  3.     try {
  4.         boolean interrupted = false;
  5.         // 自旋
  6.         for (;;) {
  7.             final Node p = node.predecessor();
  8.             // 如果node的前驱结点p是head,表示node是第二个结点,就可以尝试去获取资源了
  9.             if (p == head && tryAcquire(arg)) {
  10.                 // 拿到资源后,将head指向该结点。
  11.                 // 所以head所指的结点,就是当前获取到资源的那个结点或null。
  12.                 setHead(node); 
  13.                 p.next = null; // help GC
  14.                 failed = false;
  15.                 return interrupted;
  16.             }
  17.             // 如果自己可以休息了,就进入waiting状态,直到被unpark()
  18.             if (shouldParkAfterFailedAcquire(p, node) &&
  19.                 parkAndCheckInterrupt())
  20.                 interrupted = true;
  21.         }
  22.     } finally {
  23.         if (failed)
  24.             cancelAcquire(node);
  25.     }
  26. }

这里parkAndCheckInterrupt方法内部使用到了LockSupport.park(this),顺便简单介绍一下park。LockSupport类是Java 6 引入的一个类,提供了基本的线程同步原语。LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数:park(boolean isAbsolute, long time):阻塞当前线程 unpark(Thread jthread):使给定的线程停止阻塞

所以结点进入等待队列后,是调用park使它进入阻塞状态的。只有头结点的线程是处于活跃状态的。

acquire方法 获取资源的流程:

当然,获取资源的方法除了acquire外,还有以下三个:

  • acquireInterruptibly:申请可中断的资源(独占模式)
  • acquireShared:申请共享模式的资源
  • acquireSharedInterruptibly:申请可中断的资源(共享模式)

可中断的意思是,在线程中断时可能会抛出InterruptedException

释放资源

释放资源相比于获取资源来说,会简单许多。在AQS中只有一小段实现。

源码:

 
 
 
 
  1. public final boolean release(int arg) {
  2.     if (tryRelease(arg)) {
  3.         Node h = head;
  4.         if (h != null && h.waitStatus != 0)
  5.             unparkSuccessor(h);
  6.         return true;
  7.     }
  8.     return false;
  9. }

tryRelease方法 这个动作可以认为就是一个设置锁状态的操作,而且是将状态减掉传入的参数值(参数是1),如果结果状态为0,就将排它锁的Owner设置为null,以使得其它的线程有机会进行执行。

在排它锁中,加锁的时候状态会增加1(当然可以自己修改这个值),在解锁的时候减掉1,同一个锁,在可以重入后,可能会被叠加为2、3、4这些值,只有unlock()的次数与lock()的次数对应才会将Owner线程设置为空,而且也只有这种情况下才会返回true。这一点大家写代码要注意,如果是在循环体中lock()或故意使用两次以上的lock(),而最终只有一次unlock(),最终可能无法释放锁。导致死锁.

 
 
 
 
  1. private void unparkSuccessor(Node node) {
  2.     // 如果状态是负数,尝试把它设置为0
  3.     int ws = node.waitStatus;
  4.     if (ws < 0)
  5.         compareAndSetWaitStatus(node, ws, 0);
  6.     // 得到头结点的后继结点head.next
  7.     Node s = node.next;
  8.     // 如果这个后继结点为空或者状态大于0
  9.     // 通过前面的定义我们知道,大于0只有一种可能,就是这个结点已被取消
  10.     if (s == null || s.waitStatus > 0) {
  11.         s = null;
  12.         // 等待队列中所有还有用的结点,都向前移动
  13.         for (Node t = tail; t != null && t != node; t = t.prev)
  14.             if (t.waitStatus <= 0)
  15.                 s = t;
  16.     }
  17.     // 如果后继结点不为空,
  18.     if (s != null)
  19.         LockSupport.unpark(s.thread);
  20. }

方法unparkSuccessor(Node),意味着真正要释放锁了,它传入的是head节点,内部首先会发生的动作是获取head节点的next节点,如果获取到的节点不为空,则直接通过:“LockSupport.unpark()”方法来释放对应的被挂起的线程.

新闻名称:JUC解析-AQS抽象队列同步器
文章起源:http://www.shufengxianlan.com/qtweb/news17/259767.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联