Java多线程-14丨JUC-AQS(同步器)简介

Posted by jiefang on November 4, 2019

JUC-AQS(同步器)简介

JUC三板斧

了解以下JUC的设计套路,总结三板斧:

状态,队列,CAS

  • 状态:一般是一个state属性,它基本是整个工具的核心,通常整个工具都是在设置和修改状态,很多方法的操作都依赖于当前状态是什么。由于状态是全局共享的,一般会被设置成volatile类型,以保证其修改的可见性;
  • 队列:队列通常是一个等待的集合,大多数以链表的形式实现。队列采用的是悲观锁的思想,表示当前所等待的资源,状态或者条件短时间内可能无法满足。因此,它会将当前线程包装成某种类型的数据结构,扔到一个等待队列中,当一定条件满足后,再从等待队列中取出。
  • CAS:CAS操作是最轻量的并发处理,通常我们对于状态的修改都会用到CAS操作,因为状态可能被多个线程同时修改,CAS操作保证了同一个时刻,只有一个线程能修改成功,从而保证了线程安全,CAS操作基本是由Unsafe工具类的compareAndSwapXXX来实现的;CAS采用的是乐观锁的思想,因此常常伴随着自旋,如果发现当前无法成功地执行CAS,则不断重试,直到成功为止,自旋的的表现形式通常是一个死循环for(;;)

AQS概述

AbstractQueuedSynchronizer,简称AQS。是一个用于构建锁和同步器的框架,许多同步器都可以通过AQS很容易并且高效地构造出来,如常用的ReentrantLockSemaphoreCountDownLatch等。基于AQS来构建同步器能带来许多好处。它不仅能极大地减少实现工作,而且也不必处理在多个位置上发生的竞争问题。在基于AQS构建的同步器中,只可能在一个时刻发生阻塞,从而降低上下文切换的开销,并提高吞吐量。

AQS核心实现

状态

AQS中状态是由volatile类型的state属性来表示。

1
private volatile int state;

该属性的值即表示了锁的状态,state为0表示锁没有被占用,state大于0表示当前已经有线程持有该锁,这里之所以说大于0而不说等于1是因为可能存在可重入的情况。可以把state变量当做是当前持有该锁的线程数量。

在监视器锁中,ObjectMonitor对象的_owner属性记录了当前拥有监视器锁的线程,而在AQS中,通过exclusiveOwnerThread属性表示当前持有锁的线程:

1
2
//继承自AbstractOwnableSynchronizer
private transient Thread exclusiveOwnerThread;

队列

AQS的内部实现了两个队列,一个同步队列和一个条件队列

  • 条件队列是为Lock实现的一个基础同步器,并且一个线程可能会有多个条件队列,只有在使用了Condition才会存在条件队列。
  • 同步队列的作用是,在线程获取资源失败后,进入同步队列队尾保持自旋等待状态, 在同步队列中的线程在自旋时会判断其前节点是否为head节点,如果为head节点则不断尝试获取资源/锁,获取成功则退出同步队列。当线程执行完逻辑后,会释放资源/锁,释放后唤醒其后继节点。

同步队列和条件队列,都由节点Node组成,它是AQS的内部类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
    //node节点状态字段,初始化时为0
    volatile int waitStatus;
    //前驱节点
    volatile Node prev;
    //后继节点
    volatile Node next;
    //节点所代表的线程
    volatile Thread thread;
    //独占模式是null,该属性用于条件队列或者共享锁
    Node nextWaiter;
}

AQS中双向链表的头节点和尾节点:

1
2
3
4
//头结点,不代表任何线程,是一个哑结点
private transient volatile Node head;
//尾节点,每一个请求锁的线程会加到队尾
private transient volatile Node tail;

image

四种方法

  • tryAcquire(int arg):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int arg):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int arg):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int arg):共享方式。尝试释放资源,成功则返回true,失败则返回false。