AQS是 AbstractQueuedSynchronizer ,译为抽象的队列同步器。它主要为了保证我们高并发场景下的一个线程安全。这里我们通过自己实现一个简单版本的AQS来理解他的核心原理。
一、同步加锁思路
首先保证加锁的话,任意时刻只能是有一个线程加锁成功,也就是保证它的原子性。这里就提供一个 state 这样一个字段,来记录当前加锁的一个状态,也就是次数。我们要做的就是保证任意时刻,只有一个线程拿到这个锁,当线程拿到这个锁之后,就对这个 state 值进行一个修改,把他改成1。这里为了保证只有一个线程拿到这个值,可以使用 CAS 比较交换的原子算法(Atomic原子类就是通过这个算法来实现)。
使用 CAS 的话就要用到 Unsafe 这个类,这个类可以绕过虚拟机直接去操作底层的内存,这个类中的 API 都是 nativa 方法,这里我们要调用的就是这三个 native 方法。如图:
还需要有 lockholder 这个字段来存储当前持有锁的线程,以方便我们对线程进行一个操作代码如下:
/**
* 记录当前加锁的状态,0表示无线程持有,1表示有一个线程持有锁。
*/
private volatile int state = 0;
/**
* 当前持有锁的线程
*/
private Thread lockHolder;
public int getState() {
return state;
}
public void setLockHolder(Thread lockHolder) {
this.lockHolder = lockHolder;
}
二、使用Unsafe类
要想实现同步操作,肯定就要了解原子操作,所谓原子操作就是执行过程中无法被打断这样一种操作,例如赋值操作。
要实现这样的原子操作,就需要去调用上面说到 Unsafe 这个类中的方法,但是在 Java 中是不建议去使用这个类的。顾名思义,这个类是不安全的。如果我们直接去 new 新建的话,Java 中会直接报错,他这里内部给定了一个 getUnsafe() 方法,根据双亲委派机制,限定你这个类必须通过引导类加载器(bootstrap classloader,又称启动类加载器)加载出来,才可以去使用。否则就会抛出一个安全异常。源码如下:
由于上述原因,我们就只能通过反射的方法来获取到这个 Unsafe 类,这里创建一个 UnsafeInstance 类,代码如下:
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class UnsafeIntance {
public static Unsafe reflectGetUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
//设置为可访问
field.setAccessible(true);
return (Unsafe)field.get(null);
} catch (IllegalAccessException | NoSuchFieldException e) {
e.printStackTrace();
}
return null;
}
}
三、compareAndSwapInt()方法使用
我们使用 Unsafe 类主要就是需要通过他的 CAS 的方法来对我们的 state 字段进行一个修改,但是这个方法一共有四个参数,源代码如下:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
这四个参数类型分别为 Object ,long, int ,int 。
- Object var1:第一个 Object 类型参数,代表的就是当前的一个类对象,也就是传入我要修改字段的类名。
- long var2:第二个 long 类型参数表示的是我要修改的字段在这个对象结构中的一个偏移量。对象在内存中的结构主要是对象头、实例数据区和对齐填充。这里可以参照《Java对象的结构组成》这篇文章。我们要找到字段在类中的一个位置,就需要获取到这个字段在数据区中的一个偏移量(从对象存储的起始地址开始)。通过这个偏移量,就能获取到这个字段了。由于CAS需要原始数据值、预期值还有修改值。通过类名和偏移量,就可以找到原始数据值了。
- int var4:CAS 的预期值 expect。
- int var5:CAS 的修改值 update。
该部分代码如下:
public final boolean compareAndSetState(int expect, int updata) {
return UNSAFE.compareAndSwapInt(this, STATEOFFSET, expect, updata);
}
/**
* 通过反射获取一个 Unsafe 实例对象
* 调用刚才 UnsafeIntance 类的 reflectGetUnsafe()方法
*/
private static final Unsafe UNSAFE = aqs.UnsafeIntance.reflectGetUnsafe();
/**
* state字段偏移量
*/
private static final long STATEOFFSET;
static {
try {
//获取偏移量
STATEOFFSET = UNSAFE.objectFieldOffset(Aqs.class.getDeclaredField("state"));
} catch (Exception e) {
throw new Error();
}
}
四、加锁
首先进行加锁操作,需要先取到当前线程,然后再通过 getState() 方法来获取到当前这个类对象中的 state 字段,然后对这个 state 进行一个判断,如果为 0,就说明当前这个锁还没有被持有。这里的思路其实就是多个线程为了获取锁,去争抢着通过 CAS 修改 state 这个字段,修改为1,这时 compareAndSwapInt() 这个方法会返回true,因为你是修改成功了,然后就把 lockHolder 设置为该线程,并且让他持有锁。此时其他线程再看到 state 已经为1了,就说明这个值已经被修改了,也就是说他们修改失败,会返回一个 false 。
上面说到的是线程修改成功,并且加锁成功,但此时其他线程加锁失败,我们不能让他从方法中出来,因为一旦出来了,就会和加锁的线程一样执行后面的逻辑代码。为了避免让加锁失败的线程跳出方法,这里就通过一个循环,让他在循环里面不断地自旋。但是也不能让他们一直处在死循环的状态,这样会浪费 CPU 资源。这里就可以用到 Unsafe 类另外提供的方法,park() 和 unpark()方法,如图:
这两个方法也是 native 本地方法,这两个方法就是用来阻塞/唤醒线程的。当然这里我们不用去直接调用 Unsafe 类中的这两个方法,可以通过 LockSupport 这个类来实现阻塞/唤醒线程,这个类里面提供了 park() 和 unpark() 的API,在内部就包装了 Unsafe 类中的 native 方法。如图:
所以说,为了避免获取锁失败而继续占用 CPU 资源,就可以调用 LockSupport.park() 方法来对获取锁失败线程进行一个阻塞。但这里还有一个问题,我们不能只把线程阻塞住就不管了,在之后也需要其他线程加锁,所以我们使用 ConcurrentLinkedQueue 这个队列来保存加锁失败的线程。代码如下:
/**
* 线程安全的队列---基于CAS算法
*/
private static ConcurrentLinkedQueue<Thread> waiters = new ConcurrentLinkedQueue<Thread>();
至于为什么不是用 ArrayBlockingQueue 和 LinkedBlockingQueue 是因为这两个阻塞式队列就是基于AQS来实现的,我们当前就是要实现一个简单的AQS,所以说暂时放弃这两个队列。
由于是公平锁,所以说这里需要进行一个判断,看看在你这个线程之前还有没有线程排队了,也就是队列中还有没有线程,或者说你当前这个线程就是出于队列头部,你的优先级最高。如果队列是空的,或者你就是队列的队首元素,就可以让你进行加锁操作。
整个加锁过程代码如下:
/**
* 线程安全的队列---基于CAS算法
*/
private static ConcurrentLinkedQueue<Thread> waiters = new ConcurrentLinkedQueue<Thread>();
/**
* 尝试获取锁
*/
public boolean aquire() {
Thread current = Thread.currentThread();
//当前加锁的状态
int c = getState();
if (c == 0) {
//当前同步器还没有被持有
//canUse用来确定当前这个尝试加锁的线程是不是满足公平锁条件
boolean canUse = waiters.isEmpty() || current == waiters.peek();
if (canUse && compareAndSetState(0, 1)) {
//修改成功的线程,设置为持有者
setLockHolder(current);
return true;
}
}
return false;
}
/**
* 加锁后的处理
*/
public void lock() {
//加锁成功
if (aquire()) {
return;
}
//没有加锁成功
Thread current = Thread.currentThread();
waiters.add(current);
//自旋:死循环确保没有获取到锁的线程不再 执行后续代码/占有CPU
for (;;){
if (aquire()) {
//唤醒队头线程的话,就需要从队列中移除该线程,让后面的线程排到队首
waiters.poll();
return;
}
LockSupport.park();//阻塞--释放CPU使用权,被刷入到运行时状态段
}
}
五、释放锁
首先需要判断一下当前线程,你到底是不是当前的锁持有者 lockHolder 。如果你是当前的锁持有者,就再去调用 compareAndSwapInt() 方法,把 state 这个字段再进行一个修改,把1修改为0 。也就表明是一个初始状态—无锁状态,修改之后我就把当前这个锁持有者修改为 null 。
但释放这个锁之后,我还需要去唤醒在队列中的其他线程,此时就可以去除队首元素 first ,如果你不是 null 的话,就调用 LockSupport.unpark() 方法,唤醒 first 。这里我们不是用 wait() 是因为我需要指定唤醒某一个线程,而不是通过 notify() 和 notifyAll() 去随机性的唤醒线程。
代码如下:
/**
* 释放锁
*/
public void unlock() {
if (lockHolder != Thread.currentThread()) {
//抛出异常
throw new RuntimeException("Lockholder is not current thread");
}
int state = getState();
if (compareAndSetState(state, 0)) {
setLockHolder(null);
//拿到队首元素这个线程的引用。
Thread first = waiters.peek();
if (first != null) {
//就需要唤醒队首线程
LockSupport.unpark(first);
}
}
}
这里我们只是简单实现了一个 队列同步器,真正AQS的实现还是有其他附加的功能,不过可以通过自己实现来理解一下AQS的核心内部原理。
GitHub源码:https://github.com/zhangdididi/MyAbstractQueuedSynchronizer