本文用于对AQS原理的概述。主要通过JavaGuide及两篇博客,在此基础上,作者进行补充说明、整理论述,使其能以一种更为逻辑地清晰地方式表达出“说一下你对AQS的原理”的理解,更多适应于java面试回答,亦可作对AQS原理和运作方式的简要了解。
目录
一、AQS是什么
二、AQS的设计思想
三、AQS内部实现原理
四、AQS实现方式
五、通过实例演示,加深理解
六、关于参照博客和源码分析
AQS英文全称是AbstractQueuedSynchronizer(抽象队列同步器),这个类在java.util.concurrent.locks包下。AQS 是一个用来构建锁和同步器的框架,独占式如ReentrantLock,共享式如Semaphore,CountDownLatch都是基于AQS实现的。
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
补充说明:CLH(Craig,Landin and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。
原理图(具体细节不作赘述,仅供读者简单了解过程):
AQS实现原理:AQS使用一个被volatile关键字修饰的int变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。状态信息通过procted类型的getState,setState。compareAndSet State进行操作。其中,state变量用于标记加锁(每次加可重入锁,state+1)和解锁(每次释放一把锁,state-1),当state为0时,即为真正释放所有的锁,类似于ReentrantLock可重入锁的原理。volatile保证state变量的可见性,每个线程在任何时刻访问的都是该变量的最新值。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
举例理解state变量: (关于独占式和共享式的详细介绍在下文,读者可先跳过该内容往下阅读)
①.独占式:
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
②.共享式
再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
③.补充说明
一般来说,自定义同步器要么是独占式,要么是共享式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
在此之前,我们先了解AQS定义线程访问资源的两种方式:
- 独占(Exclusive):至多只有一个线程能执行,如
ReentrantLock
。- 共享(Share):允许多个线程同时执行,如
CountDownLatch
、Semaphore。
接着,我们继续了解:AQS为我们定义好了顶层的处理实现逻辑,我们在使用AQS构建符合我们需求的同步组件时,只需重写tryAcquire,tryRelease,tryAcquireShared,tryReleaseShared几个方法,来决定同步状态的释放和获取即可,将部分简单的、可由使用者决定的操作逻辑延迟到子类中去实现。至于背后复杂的线程排队,线程阻塞/唤醒,如何保证线程安全,都由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() :
是否在独占模式下被线程占用。
最后, 具体实现:首先,我们需要去①.继承AbstractQueuedSynchronizer这个类,然后我们根据我们的需求去重写相应的方法,比如要实现一个独占锁,那就去重写tryAcquire,tryRelease方法,要实现共享锁,就去重写tryAcquireShared,tryReleaseShared;最后,②.在我们的组件中调用AQS中的模板方法就可以了,而这些模板方法是会调用到我们之前重写的那些方法的。也就是说,我们只需要很小的工作量就可以实现自己的同步组件,重写的那些方法,仅仅是一些简单的对于共享资源state的获取和释放操作,至于像是获取资源失败,线程需要阻塞之类的操作,自然是AQS帮我们完成了。
下面代码演示中, Mutex外部类为我们自定义实现的同步器类,内部类Sync继承了AbstractQueuedSynchronizer父类,然后重写了isHeldExclusively()、tryAcquire()、tryRelease()方法。此时,线程访问资源的方式是独占式。接着,在Mutex外部类里面,且在Sync内部类外面的范围中,我们的组件通过创建Sync类的实例对象去调用AQS中的模板方法(即上述的isHeldExclusively()、tryAcquire()、tryRelease() 方法),继而实现了加锁lock()方法、解锁unlock()方法和判断资源是否被占用的isLocked()方法,可类比ReentrantLock进行理解。
package juc;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
/**
* Created by chengxiao on 2017/3/28.
*/
public class Mutex implements java.io.Serializable {
//静态内部类,继承AQS
private static class Sync extends AbstractQueuedSynchronizer {
//是否处于占用状态
protected boolean isHeldExclusively() {
return getState() == 1;
}
//当状态为0的时候获取锁,CAS操作成功,则state状态为1,
public boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//释放锁,将同步状态置为0
protected boolean tryRelease(int releases) {
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
}
//同步对象完成一系列复杂的操作,我们仅需指向它即可
private final Sync sync = new Sync();
//加锁操作,代理到acquire(模板方法)上就行,acquire会调用我们重写的tryAcquire方法
public void lock() {
sync.acquire(1);
}
public boolean tryLock() {
return sync.tryAcquire(1);
}
//释放锁,代理到release(模板方法)上就行,release会调用我们重写的tryRelease方法。
public void unlock() {
sync.release(1);
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
}
下面演示,分别创建30个线程,每个线程 对int变量a增加10000次,在线程并发安全的情况下(利用上述自定义实现的独占式同步器Mutex类来保证并发安全),结果应该等于30 x 10000= 300000;在并发不安全的情况下(不加任何锁),该结果小于300000。
1 package juc;
2
3 import java.util.concurrent.CyclicBarrier;
4
5 /**
6 * Created by chengxiao on 2017/7/16.
7 */
8 public class TestMutex {
9 private static CyclicBarrier barrier = new CyclicBarrier(31);
10 private static int a = 0;
11 private static Mutex mutex = new Mutex();
12
13 public static void main(String []args) throws Exception {
14 //说明:我们启用30个线程,每个线程对i自加10000次,同步正常的话,最终结果应为300000;
15 //未加锁前
16 for(int i=0;i<30;i++){
17 Thread t = new Thread(new Runnable() {
18 @Override
19 public void run() {
20 for(int i=0;i<10000;i++){
21 increment1();//没有同步措施的a++;
22 }
23 try {
24 barrier.await();//等30个线程累加完毕
25 } catch (Exception e) {
26 e.printStackTrace();
27 }
28 }
29 });
30 t.start();
31 }
32 barrier.await();
33 System.out.println("加锁前,a="+a);
34 //加锁后
35 barrier.reset();//重置CyclicBarrier
36 a=0;
37 for(int i=0;i<30;i++){
38 new Thread(new Runnable() {
39 @Override
40 public void run() {
41 for(int i=0;i<10000;i++){
42 increment2();//a++采用Mutex进行同步处理
43 }
44 try {
45 barrier.await();//等30个线程累加完毕
46 } catch (Exception e) {
47 e.printStackTrace();
48 }
49 }
50 }).start();
51 }
52 barrier.await();
53 System.out.println("加锁后,a="+a);
54 }
55 /**
56 * 没有同步措施的a++
57 * @return
58 */
59 public static void increment1(){
60 a++;
61 }
62 /**
63 * 使用自定义的Mutex进行同步处理的a++
64 */
65 public static void increment2(){
66 mutex.lock();
67 a++;
68 mutex.unlock();
69 }
70 }
结果测试
加锁前,279204
加锁后,300000
综上,介绍完毕。
本文用于对AQS原理的概述。主要通过JavaGuide及两篇博客,在此基础上,作者进行补充说明、整理论述,使其能以一种更为逻辑地清晰地方式表达出“说一下你对AQS的原理”的理解,更多适应于java面试回答,亦可作对AQS原理和运作方式的简要了解。因此,此文不作源码分析,读者可从参照博客中进行深入阅读。如若本文有所帮助,请收藏点赞,谢谢你,朋友。
博客:
Java并发包基石-AQS详解 - dreamcatcher-cx - 博客园
Java并发之AQS详解 - waterystone - 博客园