1. AQS
AQS全称为AbstractQueuedSynchronizer, 抽象队列同步器.AQS是JUC中一个非常重要的组件, 像ReentReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore等很多同步类都使用到了AQS,以内部抽象类的形式引入.这里涉及到了设计模式中的模板方法模式.
模板方法模式:
定义一个操作中算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变算法的结构即可重定义该算法的某些特定步骤.
简而言之,AQS中定义了一些算法骨架.同步类中实现其中的抽象方法.这样可以实现不同对象的所需.
2.为什么有AQS?
在JDK1.6之前,只有synchronized关键字来进行加锁,而且加锁直接就是pthread_mutex_lock,直接就进入内核态了.重量级锁是很消耗性能的.没有偏向锁、轻量锁之分.Java大神Doug Lea于是为了提升同步加锁的性能,于是就有了AQS.道哥想的是,在Java语言层面去解决用户态和内核态的切换.而不是直接去操作系统中执行。于是就有了AQS.然后JDK就收入了道哥的代码.在此致敬道哥,一些理念真的是很超前.道哥将性能提升到了极致.
**切记:带着 "极致的性能"的理念去看源码
3.ReentrantLock
3.1 特点:
1.有公平锁和非公平锁
2.支持重入
3.CAS的使用
4.可以关联多个条件变量(lock.newCondition)
3.2 公平锁和非公平锁区别
3.2.1 什么是公平锁?
当锁对象被其他线程占有时,其他线程会检查锁对象是否被人持有,如果没有被人持有,则会查看队列是否形成,如果队列有人在排队,则将自己进行排队.如果没有人排队,则将锁对象的占有者设置为自身.
简而言之,线程来加锁,验证锁对象是否被人持有,如果有人持有则进行排队,如果没有别人持有,则检查队列是否有人在排队,没有排队就占有锁.
此处先不考虑CAS。当锁对象释放锁时.会顺序地将队列中的线程唤醒.这就是公平锁.
3.2.2 什么是非公平锁?
线程直接来进行加锁,如果执行失败则进行排队.不管队列中的其他线程是否在等待锁对象,直接进行加锁.这也是一朝排队,永久排队的特点.
3.2.3 公平锁和非公平锁的应用场景和性能对比
3.2.3.1 应用场景:
如果没有特殊要求线程之间的获取锁对象的顺序,则尽可能地使用非公平锁.
公平锁一般应用在有顺序要求地获取锁对象的场景中,反之则非公平锁.下面会介绍原因。
3.2.3.2 性能对比:
由于公平锁在锁对象释放持有者线程时,都需要从队列中唤醒下一个线程节点.此种行为会带来线程上下文的切换.这会带来性能之间的损耗.因为Java中的线程操作是直接对应到操作系统中的.Java本身不具备调度线程的能力,公平锁的性能损耗比较大.相反非公平锁呢,当有线程来加锁,一旦持有锁对象被释放了,则会直接利用CAS进行加锁,这个过程省去了线程切换的损耗,在高并发的情况下,吞吐量要比公平锁高.
4.源码解读
state:
在解读源码之前,我先来讲解下AQS的队列结构,内部维护了一个int类型的state来表示当前锁对象的状态,这个int类型设置的相当巧妙,在ReentranReadWriteLock中利用这个int类型还可以表示读写锁,高16位是读锁的数量低16位是写锁的数量.在后续篇章中会详细解释.
队列:
内部维护的这个队列被称为同步队列,同步队列的内部结构是用双向链表来链接节点的.但是跟锁对象关联的条件变量队列却不是双向链表.同步队列的目的
是维护由于竞争资源无法获得锁的线程队列,并使之呈现一定序列(无序或者有序).在队列中的head节点表示正在获取锁的线程(可以理解为火车票排队的第一个人它本身就是在享受资源),当前线程释放了锁时,head节点会去唤醒下一个队列中的线程节点,并将下一个线程节点置为头结点,在唤醒的过程中下一个节点会判断前驱节点是否为head节点、state是否为0.这是为什么同步队列是双向链表的原因.
waitStatus:
waitStatus的取值主要有以下取值:
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
CANCELLED表示当前节点被取消
SIGNAL表示当前节点在释放锁时要唤醒下一个节点
CONDITION表示当前节点正在处于条件变量队列中的节点,当条件变量队列被唤醒时会迁移到同步队列中,迁移过程中会将CONDITION置为0
PROPAGATE表示当前节点可以被共享
4.1 公平锁
4.1.1 加锁
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取锁的状态
int c = getState();
// 验证当前锁是否被人持有
if (c == 0) {
// 判断是否有人在排队,如果没有则进行CAS加锁并将当前
//线程置为持有者
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 重入 如果锁被人持有,则验证当前线程是否重入
else if (current == getExclusiveOwnerThread()) {
// 重复次数计算
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
通过源码可以看到,公平锁在state为0时会去队列中判断是否有人在排队,还会判断是否重入.接下来看一下源码的设计:
hasQueuedPredecessors源码设计地比较巧妙,设计的理念依旧是极致的性能
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
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());
}
当第一个线程来加锁时,head和tail节点是没有进行初始化的.这时判断队列中是否有人在排队的条件是 h != t 也就是说head节点和tail节点是否相等,这里是相等的, && 是短路运算,所以这个时候将会直接返回.加锁成功.
第一个线程加锁的成本仅仅是CAS运算,不需要其他额外多余的操作,极致的性能体现在,第一个线程来加锁时,本身是不需要排队的,因为没有人会竞争锁,如果初始化队列,将会造成空间资源的浪费.
当第二个线程来加锁,这个时候t1还未释放锁,t2在tryAcquire中返回false.
接下来t2将先被封装成独占锁模式的Node节点,执行入队操作的流程.
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
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;
}
}
}
}
t2在addWaiter方法中拿到的tail节点为空,这个时候执行enq方法,在enq方法中首先验证了tail节点是否为空,为空则进行初始化队列.这里是一个for的死循环,当初始化队列完成之后,将会执行入队.将前驱后继节点进行连接.
执行完入队操作后,会将t2进行park(LockSupport)让出CPU资源进行睡眠,它是一个Linux系统中内置函数中的parkCondition中的一个函数,跟synchornized中的park函数有点区别,synchrnoized中的park方法是parkEvent中的函数,两者都会设计到内核态的转换,LockSupport在后续的JDK版本中会逐渐变成parkEvent中.
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取到当前节点的前驱节点
final Node p = node.predecessor();
// 验证前驱节点是否是head节点,如果是head节点
// 则进行尝试加锁.因为有可能当前线程在被封装成
// node节点入队的操作过程中,前驱节点可能会释放锁
// 虽然说概率比较小,但是为了极致的性能,还是有必要的
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 如果前驱节点不是head节点,那么将会park
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
t2在睡眠之前,是需要通知前驱节点当锁被释放的时候来唤醒我(调用LockSupport的unpark).进行的操作是获取前驱节点的waitStatus,将其置为SIGNAL状态.
巧妙之处又来了, compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 将其CAS替换之后并没有立刻返回true,而是再次循环,因为这个置换过程中也有可能锁被释放,所以再次循环,第二次循环时在这个才会返回true.接着去睡眠.
如果CAS替换失败将会持续循环
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
&& 前面的条件成立才会进入到该方法中.
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
以上就是公平锁的加锁流程
4.1.2 解锁
解锁的过程相比加锁要简洁很多
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
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;
}
先将exclusiveOwnerThread持有者线程置为null,然后修改state.unpark下一个节点
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
// 获取head节点的waitStatus,由于head节点的waitStatus为-1 SIGNAL,不成立
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
// 获取当前传入的head节点的下一个节点
Node s = node.next;
// 如果下一个节点为null,说明head节点就是最后一个节点不会执行for循环
// 如果下一个节点被取消掉了,
// 那么将会其置为null,从队尾开始获取waitStatus不被取消的节点
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
4.2 非公平锁
4.2.1 加锁
非公平锁是直接上来就进行加锁
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
tryAcquire有多种实现方式,这里看的是非公平的tryAcquire
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取state
int c = getState();
// 如果state为0,则直接进行加锁
if (c == 0) {
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;
}
非公平的tryAcquire和公平锁相比,是比较暴力的,区别在于锁一旦释放,非公平则直接进行加锁,公平锁则还需要判断是否需要排队.其他的地方和公平锁的大致一样.
4.3 公平锁和非公平锁的获取流程对比
公平锁:
非公平锁:
4.ReenReadWriteLock
在传统的对共享变量的操作过程中,无论是对其进行读操作还是写操作,都会进行加锁.对于写操作而言加锁的意义就比较明显因为写操作要修改共享变量这就需要不同线程之间的互斥.但是读操作却不涉及到修改操作,仅仅是把共享变量加载到线程的工作内存中.AQS中的ReenReadWriteLock内部实现了读读并发,读写/写读,写写互斥.
源码解读
ReenReadWriteLock对于AQS内部维护的state跟ReentrantLock略有不同,state被切割为了两部分,一个int类型的变量占4个字节,1个字节占8个长度.state将高16位表示共享锁(读锁),低16位表示独占锁(写锁).那么为什么要这样设计呢?为何不把两个模式的加锁设成两个变量?
个人认为有以下几个原因:
1.空间利用率方面.一把锁的状态不大可能超过2的16次幂.也就是65536.无论是读锁还是写锁也好.几率可以说非常小.如果用32位来表示,可想而知空间利用率多么地低下.
2.时间效率方面.这两种状态是密不可分的,也就是说两种状态存在一定的依赖性.典型的例子就是锁降级.加载两个状态值的时间花费肯定是大于加载一个状态值的.每次从内存中读取另一个状态值也是需要消耗时间的.位运算是要比从内存中读取快很多的.
两种状态值的获取方式:
独占锁:将state与1左移 16位做与运算.
共享锁:直接将state右移16位
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
1.writeLock
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
// 1.获取当前线程
Thread current = Thread.currentThread();
// 2.获取锁的状态
int c = getState();
// 3.获取独占锁的数量
int w = exclusiveCount(c);
// 4.判断当前锁是不是自由状态(没有读锁也没有写锁)
if (c != 0) {
// 5.当前锁被人持有
// 6.当前锁的状态是共享锁还是独占锁
// 如果是共享锁,判断当前上锁的线程是否是自己
// 注意:这里是一个逻辑或运算,如果第一个条件为true,会发生短路.
// 第二个条件不会进行判断
// 场景一:如果没有人来进行独占锁加锁,如果当前线程来加锁.意味着要锁升级,
// 这是不允许的.需要等待读锁释放完毕才可以进行加锁
// 这可能会导致脏数据的产生,另外也违背了互斥性原则
// 场景二:锁被人持有,但是w不为0,说明有人在上独占锁,第一个条件为false
// 接着走第二个条件,当前锁的持有者是不是自己,如果是自己,则进行锁的重入.
// 如果不是自己,那么遵循互斥性原则的写写互斥.当前线程要加锁失败
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
// 7.该方法在非公平模式下,直接返回的是false.公平锁模式下是判断是否有队列形成
if (writerShouldBlock() ||
// 8. CAS加锁失败直接返回
!compareAndSetState(c, c + acquires))
return false;
// 9.将当前持有者线程设为自己
setExclusiveOwnerThread(current);
return true;
}
获取失败后进行入队.写锁的上锁模式相比于ReentrantLock还是比较容易理解的.需要注意的是锁升级的禁止.接下来我们来重点看下Doug Lea是如何做到读读并发的.在未看源码之前,我们可以自己设想一下,我们自己应该如何设计.
2.readLock
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
// 1.获取当前线程
Thread current = Thread.currentThread();
// 2.获取锁的状态
int c = getState();
// 3.判断当前锁的状态是否为独占锁,如果是独占锁则判断独占锁是否是自己.
// 如果当前状态是独占锁,并且持有者线程不是自己,直接返回-1执行入队操作
// 写写互斥
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 4. 获取共享锁
int r = sharedCount(c);
// 5.该方法判断了队列是否形成,并且head的下一个节点是否是独占锁,
// 因为如果head节点下一个节点是独占锁的话,!readerShouldBlock()则是false,
// 这里顺着代码分析,如果head节点的下一个节点是共享锁,则判断当前的读锁是否小于
// 最大读锁.接着CAS加锁
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 6.如果队列没有形成,并且当前线程是第一次来进行加锁.
// firstReader 置为当前线程.这里设计的目的是,当这个线程重入时,
// 可以直接进行加锁,有点像sync的偏向锁
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 7.如果当前当前线程不是第一次加锁,第二次进行加锁,是锁的重入.
// 则firstReaderHoldCount++
firstReaderHoldCount++;
} else {
// 8.如果锁的状态是被人持有共享锁.进入以下判断
// 9.这里的HoldCounter只有两个属性
// 一个是count一个是tid(这个tid是根据偏移量计算出来的一个值)
// HoldCounter是用来统计每个线程的共享锁的上锁次数的.
HoldCounter rh = cachedHoldCounter;
// 如果当前线程没有缓存过,短路
// 直接进行存储,readHolds底层是一个ThreadLocal
// key是Thread value是int类型的count
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
写锁的释放可以参考<AQS之ReentrantLock>在此不再赘述
https://blog.csdn.net/Cover_sky/article/details/117737555
接下来我们再来看下读锁是如何释放的
protected final boolean tryReleaseShared(int unused) {
// 1.获取当前线程
Thread current = Thread.currentThread();
// 2.判断当前线程是否是第一个加共享锁的线程
if (firstReader == current) {
// 3.如果是第一个加共享锁的线程,并且只添加过一次
// 那么直接将firstReader 置为NULL
// 反之则自减
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
// 4.如果来释放共享锁的线程不是第一个来加共享锁的线程
// 获取最后获取共享锁的线程信息
// 如果当前线程不是最后一个加共享锁的线程
// 则从readHolds中获取该线程对应的ThreadLocal
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
// 5.获取该线程对应的共享锁次数
int count = rh.count;
// 6.判断是否小于等于1,
if (count <= 1) {
// 7.小于等于1 则删除该线程对应的ThreadLocal
readHolds.remove();
// 8. 并发场景下移除ThreadLocal将直接抛出异常
if (count <= 0)
throw unmatchedUnlockException();
}
// 9.该线程曾多次添加共享锁,进行自减
--rh.count;
}
// 10.以上操作只是把该线程对应的加锁次数进行了修改.state还没有进行修改
// 下方是CAS修改state的操作
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}