要不要来点 "AQS"
今天我们要来探讨一下 synchronized 以及 锁 是如何实现线程同步的。
今天我决定采用倒叙的写法来阐述这篇文章。synchronized 以及 Java 中的锁是通过一个AQS 的类来实现同步的,那么问题就是转变成AQS是怎么去实现线程同步的。
AQS是什么
AQS 的全称是 AbstractQueuedSynchronizer,他是用来构建的Java中锁和其他同步组件一个基本框架。
那么 AQS 是怎么发挥他的作用的呢?
因为上面说到 Java 中的锁机制都是通过 AQS 来实现的,所以接下来经常用到的 ReentrantLock ( 可重入锁 )来进行讲解。
首先进入的到 ReentrantLock 的源码,我们都知道 ReentrantLock 是实现了 Lock 接口,因此会实现 Lock 接口中的方法:
//ReentrantLock 实现了Lock接口
public class ReentrantLock implements Lock, java.io.Serializable
那么我们来看下在实现方法中,ReentrantLock 做了什么,进入到 ReentrantLock 的 lock 方法中:
//这里的 lock 方法为实现了 Lock 接口的方法
public void lock() {
sync.lock();
}
在这里我们可以看到在 lock 方法里面调用了 sync 的 lock 方法,所以接下来看下这个 sync 是什么:
abstract static class Sync extends AbstractQueuedSynchronizer{
...
abstract void lock();
//以下是 AQS 中的方法,重写里面的某些方法完成我们的需求
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
...
}
从上面可以看到,在 ReentrantLock 内部定义了 一个静态抽象内部类,这个内部类继承了 AbstractQueuedSynchronizer,然后在这个类里面去实现 AQS(AbstractQueuedSynchronizer) 的方法,接着通过让其他类继承 Sync 从而实现线程同步。下面通过非公平锁来举例说明:
static final class NonfairSync extends Sync {
//这个UID是为了序列化的版本号使用的
private static final long serialVersionUID = 7316153563782823691L;
//实现 sync 中的抽象方法
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//重写 AQS 中的 tryAcquire 方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
通过 NonfairSync 这个类继承 Sync 从而来实现 AQS 的里面的锁机制。我们可以看到 ReentrantLock 的构造方法里根据传入的布尔值来实例不同继承了 Sync 的子类:
//父类Sync —依赖倒置原则
private final Sync sync;
//默认是非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//根据传入的布尔值来判断实例哪个对象
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
**讲完 AQS 在 Java 中锁是怎么是发挥作用的,接下来来看看 AQS 中的实现原理:
- AQS中有一个代表状态的参数:state:
private volatile int state;
可以发现这是一个 int 型带有 volatile 关键字的变量。因为带有 volatile 关键字,因此当state 一发生改变就会刷新到主存里面,每次需要使用 state 时都从主存里面去拿数据。
这个变量表示的是锁的状态(当 state = 0 时表示当前没有线程持有锁,当state >= 1时表示有线程正持有锁)
每次加锁,释放锁都会通过这个 state 来进行判断,因此就需要以下几个重要方法:**
// 获取 锁 状态
protected final int getState() {
return state;
}
//这里使用到了一个类似CAS 的操作(对比和设置)使用这个方法能保证原子性,用于每次加锁,解锁的时候对 state 设置状态
protected final boolean compareAndSetState(int expect, int update) {
return U.compareAndSwapInt(this, STATE, expect, update);
}
//独占式获取同步状态,先判断状态是否符合预期,然后再进行CAS设置同步状态
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//独占式释放同步状态,等待获取同步状态的线程有机会获得同步状态
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
- AQS 里面同时还使用到了一个模板方法的设计模式,我们从上面可以看到 AQS 里面我列出的这几个方法都使用了 protected 关键字,我们在 前面提到的 Sync 类中去重写对应的方法来达到我们的需要的效果。
从前面源码可以知道 ReentrantLock 中可以根据传入的布尔值设置为公平锁与非公平锁,那具体是怎么实现的呢:
//公平锁实现方式
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//重点关注 hasQueuedPredecessors 这一条件,这里是和非公平锁唯一不同
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//这里判断是否是当前线程
else if (current == getExclusiveOwnerThread()) {
//如果是当前线程则 +1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
//非公平锁实现方式
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//可以看出加锁操作都是根据 state 这个关键字来判断的
int c = getState();
//当 state == 0 时表示没有其线程获取到锁
if (c == 0) {
//可以发现这里相对于公平锁少了 hasQueuedPredecessors 这一条件
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
=========================================
//这个方法是在公平锁与非公平锁的区别。在 ReentrantLock 维护有一条链表,把新加入的线程放在链表的尾端,在每次加锁前会判断
//该线程前面是否还有其他线程,没有则执行,有则阻塞在队列中等待;
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
从上面可以看到 AQS 里 state 关键字是用来判断能否加锁的依据,以及通过 compareAndSetState ,setState 等方法来设置新的 state 值。
- 从上面可以发现公平与非公平的区别是判断要加锁的线程是否处于队列的头部,当处于队列头部时,才允许加锁操作。
- 而对于可重入锁 与非可重入锁
CLH队列锁
CLH 队列锁是一个基于链表的高可用的,高性能的,公平的自旋锁,当申请线程时,仅在本地变量上,不断轮询前驱节点的锁状态,当轮询到前驱锁释放,当前节点就停止自旋,获取节点:其中,一个节点包含前驱节点的引用,以及是否需要获取锁的状态,需要获取锁时则将 locked 置为 true。
而在 Java 中 CLH 是 AQS 的一个变体实现。