前言
AQS即AbstractQueuedSynchronizer ,提供加锁解锁的一套模板,具体实现细节由子类实现,可以通过"三板斧"的辅助概念来理解:
- 状态 status
- 队列 queue
- cas
状态 state
private volatile int state;//同步状态变量
state为整个AQS的核心,是全局共享的一个状态,为保证其修改的可见性,用volatile修饰。state在不同的子类中有不同的含义。
以ReentrantLock为例,state表示该锁被线程重入的次数:当state=0时表示该锁不被任何线程持有;state=1表示当前线程持有该锁1次;state>1表示当前线程重入锁的次数;
state有三种访问方式:getState();setState();compareAndSetState(),都是原子操作。
队列
同步等待队列是一个Node节点的双向链表来实现,队列采用悲观锁的思想,即当前线程需要获取锁时就有其他线程也来获锁。因此,它会把当前线程包装成一个Node节点,放入等待队列中,当一定条件满足后,再从等待队列中移除。
-
Node节点
volatile int waitStatus;//当前节点等待的状态
volatile Node prev;//前驱
volatile Node next;//后继
volatile Thread thread;//当前节点的线程
waitStatus的状态有5种:
static final int CANCELLED(1);//当前节点已取消调度,当timeout或者中断会触发更新成改状态,终态
static final int SINGAL(-1);//后继节点在等待当前节点唤醒,当后继结点加入时会触发更新为改状态
static final int CONDITION(-2);//结点等待在Condition上,当其他线程调用了Condition的signal()方
//法后,CONDITION 状态的节点将从等待队列转移到同步队列中,等待获取同步锁
static final int PROPAGATE(-3);//共享模式下,前驱结点不仅会唤醒后继结点,也有可能唤醒后继的后继
0;默认状态
- 双向CLH链表
private transient volatile Node head;
private transient volatile Node tail;
head结点:是一个哑结点(dummy node),它不代表任何线程,一次head所指向的Node的thread永远是null。只有从次结点往后的所有结点才表示所有等待锁的线程。
AQS定义了两种资源共享方式:独占式(Exclusive)和共享式(Share)。
- 独占式:只有一个线程能执行,具体的Java实现有ReentrantLock。
- 共享式:多个线程可同时执行,具体的Java实现有Semaphore和CountDownLatch。
AQS只是一个框架,只定义了一个接口,具体资源的获取、释放都交由自定义同步器去实现。不同的自定义同步器争用共享资源的方式也不同,自定义同步器在实现时只需实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护,如获取资源失败入队、唤醒出队等,AQS已经在顶层实现好,不需要具体的同步器再做处理。
独占:
ReentrantLock对AQS的独占方式实现为:
- ReentrantLock中的state初始值为0时表示无锁状态。
- 在线程执行tryAcquire()获取该锁后ReentrantLock中的state+1,这时该线程独占ReentrantLock锁,其他线程在通过tryAcquire()获取锁时均会失败,直到该线程释放锁后state再次为0,其他线程才有机会获取该锁。
- 该线程在释放锁之前可以重复获取此锁,每获取一次便会执行一次state+1,因此ReentrantLock也属于可重入锁。但获取多少次锁就要释放多少次锁,这样才能保证state最终为0。
- 如果获取锁的次数多于释放锁的次数,则会出现该线程一直持有该锁的情况;如果获取锁的次数少于释放锁的次数,则运行中的程序会报锁异常。
- ReentrantLock 默认是非公平的,提高吞吐
共享:
CountDownLatch对AQS的共享方式实现为:
- CountDownLatch将任务分为N个子线程去执行,将state也初始化为N, N与线程的个数一致,N个子线程是并行执行的,
- 每个子线程都在执行完成后countDown()一次,state会执行CAS操作并减1。
- 在所有子线程都执行完成(state=0)时会unpark()主线程,然后主线程会从await()返回,继续执行后续的动作。
unparkSuccessor 方法中for循环从tail开始而不是head
这个要看加入的地方
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//看这里
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
新节点pre指向tail,tail指向新节点,这里后继指向前驱的指针是由CAS操作保证线程安全的。而cas操作之后t.next=node之前,可能会有其他线程进来。所以出现了问题,从尾部向前遍历是一定能遍历到所有的节点。
ABA问题
CAS 并不是万能的,CAS 更新有 ABA 问题。即 T1 读取内存变量为 A
,T2 修改内存变量为 B
,T2 修改内存变量为 A
,这时 T1 再 CAS 操作 A
时是可行的。但实际上在 T1 第二次操作 A
时,已经被其他线程修改过了。
AtomicStampedReference
对于 ABA 问题,比较有效的方案是引入版本号,内存中的值每发生一次变化,版本号都 +1
;在进行CAS操作时,不仅比较内存中的值,也会比较版本号,只有当二者都没有变化时,CAS才能执行成功。 AtomicStampedReference
便是使用版本号来解决ABA问题的。类似的还有 AtomicMarkableReference
, AtomicStampedReference
是使用 pair 的 int stamp
作为计数器使用, AtomicMarkableReference
的 pair 使用的是 boolean mark
。
ReentrantReadWriteLock
线程进入读锁的前提条件:
- 没有其他线程的写锁,
- 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。
线程进入写锁的前提条件:
- 没有其他线程的读锁
- 没有其他线程的写锁
而读写锁的特性:
(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
(2)重进入:读锁和写锁都支持线程重进入。
(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
说明:Sync抽象类继承自AQS抽象类,Sync类提供了对ReentrantReadWriteLock的支持。
Sync类内部存在两个内部类,分别为HoldCounter和ThreadLocalHoldCounter,其中HoldCounter主要与读锁配套使用,其中,HoldCounter源码如下。
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
/**
* ThreadLocal subclass. Easiest to explicitly define for sake
* of deserialization mechanics.
*/
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}