【Java并发编程】AQS(1)——并发三板斧

自己定的目标不能一开始就垮了啊,明天就放假了,所以赶紧开始更新Java并发编程系列的第一篇文章(准确来说是第二篇,因为前面还写过一篇FutureTask源码解读),也是AQS系列的第一篇文章。其实关于AQS系列的早就写好了,但是一直在反复修改而没有发上来,原因是我希望自己的文章是有信息有价值的。作为一名面向搜索引擎编程的软件工程幼狮,我每天也会接触许多无用信息,所以秉着爱护网络,人人有责的理念,对于以后发出来的文章,我都会认真斟酌检查

 

不说废话了,再说真成信息垃圾了,下面进入正题

 

一.  AQS是什么

AQS全称为AbstractQueuedSynchronizer(后面都以AQS简称),翻译过来叫做抽象队列同步器,是一个抽象类。我们把它拆开看:抽象、队列、同步器,这里我先不解释,等看完我AQS系列的文章就知道为什么要取这个名字了。我们读源码很重要的一步就是读源码的注释,所以我将AQS的注释大致翻译总结如下:

AQS是为开发人员提供的一种同步框架,它已经帮我们实现了CLH队列、线程入队出队、线程阻塞与唤醒等一系列复杂逻辑,我们只需要根据自己的需要去实现下面相应的方法

  • tryAcquire:独占模式下获取同步状态

  • tryRelease:独占模式下释放同步状态

  • tryAcquireShared:共享模式下获取同步状态

  • tryReleaseShared:共享模式下释放同步状态

  • isHeldExclusively:独占模式下,查看当前线程是否获取同步状态 

AQS提供两种模式:独占模式共享模式

独占模式意味着每次只有一个线程能拿到锁并执行相关代码,而共享模式则意味着同一时间可以有多个线程能拿到锁来进行协同工作。如果你需要子类是采用独占模式,则只需复写上面的1、2方法;如果想采用共享模式,则只需要复写3、4方法;第5种方法一般是使用到AQS中的ConditionObject(它就是条件变量)时才重写

AQS其实就是模板模式的经典用例,上面的五个方法就是模板方法。concurrent包中的ReentrantLock,CountDownLatch,Semaphor等都是AQS这个"模板"造出来的,所以我们掌握了AQS,基本上concurrent包下其他类的源码都能很快掌握,下面我们就具体来看看AQS吧

二.  什么是三板斧

我们在分析AQS时,先把这三个点重点提出来,这三点是AQS的基石,也是Concurrent包中的基石,我们可以将这三点称之为Java并发编程的三板斧:

  1. 状态:我们AQS及其子类的核心,AQS及其子类所有操作都是依据状态进进行的。状态一般会设置成volatile,保证其具有可见性,一定程度上具有有有序性

  2. CAS:CAS操作是由Unsafe工具类来实现的,其操作具有原子性,我们一般般通过CAS来改变状态。(状态被volatile修饰,因此具有可见性和有序性,所以CAS改变状态时是线程安全的)

  3. 队列:用来保存等待操作的资源,其数据结构一般为链表。当线程的请求在短时间内得不到满足时,线程会被包装成某种类型的数据结构放入队列中,当条件满足时则会拿出队列去重新获取锁 

下面我们就一一介绍AQS这里面的三板斧

三.  第一板斧:状态

第一把斧:状态:

/**
* The synchronization state.
*/
private volatile int state;

在AQS中也叫同步状态,是被volatile修饰的int型属性,被CAS操作时是线程安全的。如果是独占模式,说明同一时间只有一个线程能拿到锁执行,所以state只有0和1,为0时则说明此时有线程已经持有锁了,而为1则说明锁还在,此时可以拿锁执行。如果是共享模式,则state最大值为同一时间线程可以拿到锁的数量。我们在讲解AQS的时候其实不会涉及到对这个同步状态的操作,因为对这个同步状态操作一般都是在我们子类实现的模板方法中进行的,所以这个我会在ReentrantLock和Semaphore中说明

这个同步状态是与锁有关的状态,我们根据这个状态来判断是否还有锁。而与我们线程相关的状态,在AQS的内部类Node中,而我们三板斧中的队列是由Node构成,所以关于线程的状态我们放在队列中讲解

四.  第二板斧:CAS

我们先贴上AQS中CAS的代码

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;

static {
   try {
       stateOffset = unsafe.objectFieldOffset
           (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
       headOffset = unsafe.objectFieldOffset
           (AbstractQueuedSynchronizer.class.getDeclaredField("head"));
       tailOffset = unsafe.objectFieldOffset
           (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
       waitStatusOffset = unsafe.objectFieldOffset
           (Node.class.getDeclaredField("waitStatus"));
       nextOffset = unsafe.objectFieldOffset
           (Node.class.getDeclaredField("next"));

   } catch (Exception ex) { throw new Error(ex); }
}


private final boolean compareAndSetHead(Node update) {
   return unsafe.compareAndSwapObject(this, headOffset, null, update);
}


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


private static final boolean compareAndSetWaitStatus(Node node, int expect, int update) {
   return unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update);
}

private static final boolean compareAndSetNext(Node node, Node expect, Node update) {
   return unsafe.compareAndSwapObject(node, nextOffset, expect, update);
}

首先可以看到有五个long类型的属性,这五个属性分别代表state、head、tail,waitStatus、next的偏移量(我们当成地址就行),其中state、head、tail为AQS的属性,waitStatus、next为AQS内部类Node的属性。我们后面会看到,这几个属性都是volatile修饰的,因此可以猜到这些属性肯定是多线程争着修改的目标。

静态块里则是对这五个属性偏移量进行初始化

至于后面的四个方法最终调用Unsafe类里面的CAS方法。CAS叫做比较交换操作,我们以下面代码为例说下CAS大致的执行流程

unsafe.compareAndSwapObject(this, tailOffset, expect, update);

CAS执行时,会拿地址tailOffset上的值与expect做比较,如果相同,则会将地址上的值更新为update,并返回true,否则直接返回false。关于CAS更详细介绍,大家可以网上找找相关资料看下

 

五.  第3板斧:队列

AQS中分为两种队列,一种叫做等待队列,还有一种叫条件队列,等待队列是双向链表结构,条件队列是单向链表,它们都是通过下面要介绍的AQS内部类Node来实现的,下面是Node的代码


static final class Node {
    
   static final Node SHARED = new Node();

   static final Node EXCLUSIVE = null;

   static final int CANCELLED =  1;

   static final int SIGNAL    = -1;

   static final int CONDITION = -2;

   static final int PROPAGATE = -3;

   volatile int waitStatus;

   volatile Node prev;

   volatile Node next;

   volatile Thread thread;

   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
   }


   Node(Thread thread, Node mode) {     // Used by addWaiter
       this.nextWaiter = mode;
       this.thread = thread;
   }

   Node(Thread thread, int waitStatus) { // Used by Condition
       this.waitStatus = waitStatus;
       this.thread = thread;
   }
}

这个结构其实是一个典型的双向链表结构(等待队列),既然能做双向链表,肯定也是能做单向链表的(条件队列)。它可以用来保存等待的线程以及线程的状态等信息。

我们先介绍volatile修饰的属性,被volatile修饰的属性一般都是通过CAS来操作,也说明这几个属性需要在并发情况下保证线程安全

  1. prev:双向链表的前驱节点

  2. next:双向链表的后继节点

  3. thread:节点所代表的线程

  4. waitStatus:该节点线程所处的状态,即等待锁的状态

然后再看下面四个static修饰的属性

  1. CANCELLED:此节点的线程被取消了

  2. SIGNAL:此节点的后继节点线程被挂起,需要被唤醒

  3. CONDITION:此节点的线程在等待信号,也表明当前节点不在同步队列中,而在条件队列中

  4. PROPAGATE:此节点下一个acquireShared应该无条件传播

这四个属性就是waitStatus属性的具体状态,还有一个隐式的具体状态,即waitStatus初始化时为0。在独占模式下,我们只需要用到CANCELLED和SIGNAL,这里需要注意的是SIGNAL,它代表的不是自己线程的状态,而是它后继节点的状态,当一个节点的waitStatus被置为SIGNAL时,表明此节点的后继节点被挂起,当此节点释放锁或被取消放弃拿锁时,应该唤醒后继节点。而在共享模式时,我们会用到CANCELLED和PROPAGATE

最后看Node类型的三个属性

  1. nextWaiter:标记此Node处于何种模式;如果为null,则为独占模式,为空空Node则为共享模式

  2. SHARED:nextWaiter的具体状态,代表共享模式

  3. EXCLUSIVE:nextWaiter的具体状态,代表独占模式

好了AQS的第一篇文章并发三板斧就讲完啦,后面还有四篇,这几天会陆续整理放上来。明天就是清明节了,疫情还未结束,大家还是待在家里休息休息为国家做做贡献吧。

(未完)

欢迎大家关注我的公众号 “程序员进阶之路”,里面记录了一个非科班程序员的成长之路

                                                 

发布了114 篇原创文章 · 获赞 199 · 访问量 20万+

猜你喜欢

转载自blog.csdn.net/qq_36582604/article/details/105303748