AQS机制

Posted ljl150

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AQS机制相关的知识,希望对你有一定的参考价值。

一,Lock接口
  锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java SE 5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。使用synchronized关键字将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。当然,这种方式简化了同步的管理,可是扩展性没有显示的锁获取和释放来的好。
1.lock锁的使用形式
1     Lock lock = new ReentrantLock(); //可以是自己实现的Lock接口的实现类,也可以是jdk提供的同步组件
2     lock.lock();//一般不能放到try语句中
3      try { 
4     } finally { 
5         lock.unlock(); //一般要求放到finally中,确保即使发生异常也能安全释放掉锁
6     }    
  • 在finally块中释放锁,目的是保证在获取到锁之后,即使发生异常,锁依然能被顺利释放,从而避免死锁情况的发生。
  • 不要将获取锁的过程写在try块中。假设放到try中,如果在获取锁时发生了异常,即锁没有被成功获取到,但finally语句中有释放锁的操作,这就会造成死锁,因为根本没有获取到锁,而底下又要求释放锁。如果没有放到try中,当获取锁失败时,代码立即会报异常而终止运行,因此就避免了死锁。
2.Lock接口的方法
  lock接口在java.util.cincurrent.locks包路径下
技术图片
 技术图片

 3.相比于synchronized,Lock接口所具备的其他特性
  ①尝试非阻塞的获取锁tryLock():当前线程尝试获取锁,如果该时刻锁没有被其他线程获取到,就能成功获取并持有锁,接着返回true,如果没有获取到则返回false。
  ②能被中断的获取锁lockInterruptibly():获取锁的线程能够响应中断。当线程在获取锁定过程中,如果锁被其他线程占用,则线程一直处于休眠状态,直到获取到锁或被其他线程中断才返回。要注意该线程允许其他线程调用Thread.interrupt()方法来中断等待的线程,当线程被中断掉,不会在去获取锁,会抛出interruptedException异常。
  ③超时的获取锁tryLock(long time, TimeUnit unit):在指定的截止时间获取锁,如果没有获取到锁返回false。

二,AbstractQueuedSynchronizer介绍
  谈到并发,不得不谈ReentrantLock;而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)!
  AbstractQueuedSynchronizer,简称AQS(同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,为构建不同的同步组件(重入锁,读写锁,CountDownLatch等)提供了可扩展的基础框架,如下图所示。
技术图片
   同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。
  子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。
  同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。
1.AQS的核心思想
   AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
  CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
  用大白话来说,AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
2.AQS的框架
技术图片

  在AQS中维护了一个volatile int state(代表共享资源)和一个FIFO存放被阻塞的线程的同步队列(多线程争用资源被阻塞时会进入此队列)。

  其中state可以使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们使用CAS操作能够保证状态的改变是安全的。

那AQS的该如何使用呢?

  首先,我们需要去继承AbstractQueuedSynchronizer这个类,然后我们根据我们的需求去重写相应的方法,比如要实现一个独占锁,那就去重写tryAcquire,tryRelease方法,要实现共享锁,就去重写tryAcquireShared,tryReleaseShared;最后,在我们的组件中调用AQS中的模板方法就可以了,而这些模板方法是会调用到我们之前重写的那些方法的。也就是说,我们只需要很小的工作量就可以实现自己的同步组件,重写的那些方法,仅仅是一些简单的对于共享资源state的获取和释放操作,至于像是获取资源失败,线程需要阻塞之类的操作,自然是AQS帮我们完成了。

  我们来看看AQS定义的这些可重写的方法:

    protected boolean tryAcquire(int arg) : 独占式获取同步状态,试着获取,成功返回true,反之为false

    protected boolean tryRelease(int arg) :独占式释放同步状态,等待中的其他线程此时将有机会获取到同步状态;

    protected int tryAcquireShared(int arg) :共享式获取同步状态,返回值大于等于0,代表获取成功;反之获取失败;

    protected boolean tryReleaseShared(int arg) :共享式释放同步状态,成功为true,失败为false

    protected boolean isHeldExclusively() : 是否在独占模式下被线程占用。

接下来我们举一个自定义实现锁的实例的代码:

 1 package juc;
 2 import java.util.concurrent.locks.AbstractQueuedSynchronizer;
 3 //Mutex是我们自定的锁
 4 public class Mutex implements java.io.Serializable {
 5     //静态内部类,继承AQS
 6     private static class Sync extends AbstractQueuedSynchronizer {
 7         //是否处于占用状态
 8         protected boolean isHeldExclusively() {
 9             return getState() == 1;
10         }
11         //当状态为0的时候获取锁,CAS操作成功,则state状态为1,
12         public boolean tryAcquire(int acquires) {
13             if (compareAndSetState(0, 1)) {
14                 setExclusiveOwnerThread(Thread.currentThread());
15                 return true;
16             }
17             return false;
18         }
19         //释放锁,将同步状态置为0
20         protected boolean tryRelease(int releases) {
21             if (getState() == 0) throw new IllegalMonitorStateException();
22             setExclusiveOwnerThread(null);
23             setState(0);
24             return true;
25         }
26     }
27         //同步对象完成一系列复杂的操作,我们仅需指向它即可
28         private final Sync sync = new Sync();
29         //加锁操作,代理到acquire(模板方法)上就行,acquire会调用我们重写的tryAcquire方法
30         public void lock() {
31             sync.acquire(1);
32         }
33         public boolean tryLock() {
34             return sync.tryAcquire(1);
35         }
36         //释放锁,代理到release(模板方法)上就行,release会调用我们重写的tryRelease方法。
37         public void unlock() {
38             sync.release(1);
39         }
40         public boolean isLocked() {
41             return sync.isHeldExclusively();
42         }
43 }

  上面是锁的实现,其使用的方法和ReentrantLock的使用方法一样,因为ReentrantLock也是基于AQS实现的。

 

  通过前面介绍AQS的框架和使用方法,我们知道它是基于同步对列和state变量实现的,使用同步队列来存放被阻塞的线程。接下来就是介绍它是怎样运用同步队列的?

3.AQS的同步队列
  AQS的内部结构主要由同步等待队列(CLH)构成。同步器依赖内部的FIFO同步队列(一个虚拟的双向链表)来完成同步状态的管理,当前线程获取锁失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当锁释放时,会把下一个等待的节点中的线程唤醒,使其再次尝试获取锁。
  同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点,节点的属性类型与名称以及描述如下所示。
  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;
技术图片

  前面我们提到过,AQS维护一个共享资源state,通过内置的FIFO来完成获取资源线程的排队工作。(这个内置的同步队列称为"CLH"队列)。该队列由一个一个的Node结点组成,每个Node结点维护一个prev引用和next引用,分别指向自己的前驱和后继结点。AQS维护两个指针,分别指向队列头部head和尾部tail。注意队列中的第一个元素表示正在使用锁的线程,而队列中第二个结点才是第一个真正排队的结点,同步队列的基本结构如图所示。

  技术图片

  其实就是个双端双向链表

为了接下来能够更好的理解加锁和解锁过程的源码,对该同步队列的特性进行简单的讲解:

  • 1.同步队列是个先进先出(FIFO)队列,获取锁失败的线程将构造结点并加入队列的尾部,并阻塞自己。如何才能线程安全的实现入队是后面讲解的重点,毕竟我们在讲锁的实现,这部分代码肯定是不能用锁的。
  • 2.队列首结点可以用来表示当前正获取锁的线程。
  • 3.当前线程释放锁后将尝试唤醒后续处结点中处于阻塞状态的线程。

 3.AQS的底层源码分析

技术图片

 

 

  之前看的这篇博客感觉写的不错,在这里就直接引用下:https://blog.csdn.net/java_lyvee/article/details/98966684

  下面是我根据博客梳理的AQS的tryAcquire()的执行过程图:

 技术图片

 https://www.cnblogs.com/chengxiao/p/7141160.html

 

以上是关于AQS机制的主要内容,如果未能解决你的问题,请参考以下文章

AQS源码解析

浅谈AQS锁实现机制(含ReentrantReadWriteLock读写锁加锁解锁相关源码分析)

AQS机制

AQS:Java 中悲观锁的底层实现机制

AQS源码学习

一文看懂JUC之AQS机制