J.U.C
一、AQS 原理
1、概述
AQS 全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架
特点:
- 用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取 锁和释放锁
- getState - 获取 state 状态
- setState - 设置 state 状态
- compareAndSetState - cas 机制设置 state 状态
- 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
- 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
- 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet
原理:
- AQS通过一个int同步状态码,和一个(先进先出)队列来控制多个线程访问资源
- 支持独占和共享两种模式获取同步状态码
- 当线程获取同步状态失败会被加入到同步队列中
- 当线程释放同步状态,会唤醒后继节点来获取同步状态
- 共享模式下的节点获取到同步状态或者释放同步状态时,不仅会唤醒后继节点,还会向后传播,唤醒所有同步节点
- 使用volatile关键字保证状态码在线程间的可见性,CAS操作保证修改状态码过程的原子性。
子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)
- tryAcquire
- tryRelease
- tryAcquireShared
- tryReleaseShared
- isHeldExclusively
获取锁的方式:
// 如果获取锁失败
if (!tryAcquire(arg)) {
// 入队, 可以选择阻塞当前线程
park unpark
}
释放锁的方式:
// 如果释放锁成功
if (tryRelease(arg)) {
// 让阻塞线程恢复运行
}
2、使用AQS实现不可重入锁
自定义同步器
/**
* 自定义锁(不可重入锁)
*/
final class MyLock implements Lock {
//独占锁,同步器类
class MySync extends AbstractQueuedSynchronizer{
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0,1)) {
//加锁成功,并设置owner为当前线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
setExclusiveOwnerThread(null);
setState(0); //state属性是volatile的,它具有写屏障可以保证它之前的操作对其他线程可见
return true;
}
//是否持有独占
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
protected Condition newCondition(){
return new ConditionObject();
}
}
private MySync sync = new MySync();
//加锁(不成功会进入等待队列等待)
@Override
public void lock() {
sync.acquire(1);
}
//加锁,可打断
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
//尝试加锁(一次)
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
//有时限的尝试加锁
@Override
public boolean tryLock(long time, @NotNull TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
//解锁
@Override
public void unlock() {
sync.release(1);
}
//新建环境变量
@NotNull
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
测试:
public static void main(String[] args) {
MyLock lock = new MyLock();
new Thread(()->{
lock.lock();
try {
TheadPrint.print("lock....");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
TheadPrint.print("unlock....");
lock.unlock();
}
}, "t1").start();
new Thread(()->{
lock.lock();
try {
TheadPrint.print("lock....");
} finally {
TheadPrint.print("unlock....");
lock.unlock();
}
}, "t2").start();
}
结果如下所示,可以看到我们成功是使用MyLock对t1和t2加了,t2在t1解锁之后才继续执行。
14:26:26:201 Thread[t1,5,main]: lock....
14:26:28:203 Thread[t1,5,main]: unlock....
14:26:28:204 Thread[t2,5,main]: lock....
14:26:28:205 Thread[t2,5,main]: unlock....
同时我们实现的是不可重入锁,也就是同一个线程如果重复加锁也会发生阻塞,测试代码如下:
public static void main(String[] args) {
MyLock lock = new MyLock();
new Thread(()->{
lock.lock();
TheadPrint.print("lock....");
lock.lock();
TheadPrint.print("relock....");
TheadPrint.print("unlock....");
lock.unlock();
}, "t1").start();
}
结果如下所示,可以看到relock并没有被打印出来,也就是第二次lock.lock();
并没加锁成功,而是同样被阻塞了。下面的内容就是看看ReentrantLock 如何实现可重入锁。
14:32:12:728 Thread[t1,5,main]: lock....
二、ReentrantLock 原理
1、非公平锁实现原理
加锁解锁流程
先从构造器开始看,默认为非公平锁实现
public ReentrantLock() {
sync = new NonfairSync();
}
NonfairSync 继承自 AQS
没有竞争时,Thread-0可以直接设置exclusiveOwnerThread为自己
第一个竞争出现时
Thread-1 执行了
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- CAS 尝试将 state 由 0 改为 1,结果失败
- 进入 tryAcquire 逻辑,这时 state 已经是1,结果仍然失败
- 接下来进入 addWaiter 逻辑,构造 Node 队列
- 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态
- Node 的创建是懒惰的
- 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程
当前线程进入 acquireQueued 逻辑
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞
- 如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
- 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,这次返回 false
- shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
- 当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回 true
- 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)
再次有多个线程经历上述过程竞争失败,变成这个样子
Thread-0 释放锁,进入 tryRelease 流程,如果成功
- 设置 exclusiveOwnerThread 为 null
- state = 0
当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程
找到队列中离 head 近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1
回到 Thread-1 的 acquireQueued 流程
如果加锁成功(没有竞争),会设置
- exclusiveOwnerThread 为 Thread-1,state = 1
- head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread
- 原本的 head 因为从链表断开,而可被垃圾回收
如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了
如果不巧又被 Thread-4 占了先
- Thread-4 被设置为 exclusiveOwnerThread,state = 1
- Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞
2、可重入原理
通过对state的++和–来控制可重入
static final class NonfairSync extends Sync {
// ...
// Sync 继承过来的方法, 方便阅读, 放在此处
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入
else if (current == getExclusiveOwnerThread()) {
// state++
int nextc = c + acquires;
if (nextc < 0) // overflow // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryRelease(int releases) {
// state--
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 支持锁重入, 只有 state 减为 0, 才释放成功
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}
3、可打断原理
不可打断模式
在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了
// Sync 继承自 AQS
static final class NonfairSync extends Sync {
// ...
private final boolean parkAndCheckInterrupt() {
// 如果打断标记已经是 true, 则 park 会失效
LockSupport.park(this);
// interrupted 会清除打断标记
return Thread.interrupted();
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (; ; ) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
failed = false;
// 还是需要获得锁后, 才能返回打断状态
return interrupted;
}
if (shouldParkAfterFailedAcquire(p,node)&&parkAndCheckInterrupt()) {
// 如果是因为 interrupt 被唤醒, 返回打断状态为 true
interrupted = true;
}
}
} finally {
if (failed) cancelAcquire(node);
}
}
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
// 如果打断状态为 true
selfInterrupt();
}
}
static void selfInterrupt() {
// 重新产生一次中断
Thread.currentThread().interrupt();
}
}
可打断模式
static final class NonfairSync extends Sync {
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 如果没有获得到锁, 进入 ㈠
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
// ㈠ 可打断的获取锁流程
private void doAcquireInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (; ; ) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node)&&parkAndCheckInterrupt()) {
// 在 park 过程中如果被 interrupt 会进入此
// 这时候抛出异常, 而不会再次进入 for (;;)
throw new InterruptedException();
}
}
} finally {
if (failed) cancelAcquire(node);
}
}
}
4、公平锁实现原理
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
// 与非公平锁主要区别在于 tryAcquire 方法的实现
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 先检查 AQS 队列中是否有前驱节点, 没有才去竞争
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;
}
// ㈠ AQS 继承过来的方法, 方便阅读, 放在此处
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// h != t 时表示队列中有 Node
return h != t && (
// (s = h.next) == null 表示队列中还有没有老二
(s = h.next) == null
// 或者队列中老二线程不是此线程
|| s.thread != Thread.currentThread()
);
}
}
5、条件变量实现原理
每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject
await 流程
开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程
// ㈠ 添加一个 Node 至等待队列
private Node addConditionWaiter() {
Node t = lastWaiter;
// 所有已取消的 Node 从队列链表删除, 见 ㈡
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
// 创建一个关联当前线程的新 Node, 添加至队列尾部
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部
接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁
unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功
park 阻塞 Thread-0
signal 流程
假设 Thread-1 要来唤醒 Thread-0
进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node
// 唤醒 - 将没取消的第一个节点转移至 AQS 队列
private void doSignal(Node first) {
do {
// 已经是尾节点了
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (
// 将等待队列中的 Node 转移至 AQS 队列, 不成功且还有节点则继续循环 ㈢
!transferForSignal(first) &&
// 队列还有节点
(first = firstWaiter) != null);
}
执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的 waitStatus 改为 -1
// 外部类方法, 方便阅读, 放在此处
// ㈢ 如果节点状态是取消(任务放弃执行), 返回 false 表示转移失败, 否则转移成功
final boolean transferForSignal(Node node) {
// 如果状态已经不是 Node.CONDITION, 说明被取消了
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 加入 AQS 队列尾部
Node p = enq(node);
int ws = p.waitStatus;
if (// 上一个节点被取消
ws > 0 ||
// 上一个节点不能设置状态为 Node.SIGNAL
!compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// unpark 取消阻塞, 让线程重新同步状态
LockSupport.unpark(node.thread);
return true;
}
Thread-1 释放锁,进入 unlock 流程,略
三、读写锁
1、使用
当读操作频率远远高于写操作时,这时候使用 读写锁 让 读-读 可以并发提高性能。 类似于数据库中的 select ... from ... lock in share mode
提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法
class DataContainer {
private Object data;
private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
//读锁
private ReentrantReadWriteLock.ReadLock r = rw.readLock();
//写锁
private ReentrantReadWriteLock.WriteLock w = rw.writeLock();
//读操作
public Object readData() {
TheadPrint.print("获取读锁...");
r.lock();
try {
TheadPrint.print("读操作");
Thread.sleep(1000);
return data;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
TheadPrint.print("释放读锁...");
r.unlock();
}
return null;
}
//写操作
public void writeData(Object object) {
TheadPrint.print("获取写锁...");
w.lock();
try {
TheadPrint.print("写操作");
Thread.sleep(1000);
data = object;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
TheadPrint.print("释放写锁...");
w.unlock();
}
}
}
测试代码:
public static void main(String[] args) {
DataContainer dc = new DataContainer();
new Thread(()->{
dc.readData();
}, "t1").start();
new Thread(()->{
dc.readData();
}, "t2").start();
}
从结果可以看出,读-读操作并没有相互阻塞:
18:17:22:222 Thread[t1,5,main]: 获取读锁...
18:17:22:228 Thread[t1,5,main]: 读操作
18:17:22:231 Thread[t2,5,main]: 获取读锁...
18:17:22:231 Thread[t2,5,main]: 读操作
18:17:23:229 Thread[t1,5,main]: 释放读锁...
18:17:23:232 Thread[t2,5,main]: 释放读锁...
如果让t1进行读操作,然后让t2进行写操作,也即是读-写操作,结果如下。可以看出t2等到一秒后t1执行完毕之后才只能获得到锁开始执行:
18:28:31:284 Thread[t1,5,main]: 获取读锁...
18:28:31:286 Thread[t1,5,main]: 读操作
18:28:31:292 Thread[t2,5,main]: 获取写锁...
18:28:32:286 Thread[t1,5,main]: 释放读锁...
18:28:32:286 Thread[t2,5,main]: 写操作
18:28:33:287 Thread[t2,5,main]: 释放写锁...
如果将t1和t2改成写-写操作,则和读-写操作一样,t1仍然需要t2完成之后才能进行操作:
18:29:07:663 Thread[t2,5,main]: 获取写锁...
18:29:07:669 Thread[t2,5,main]: 写操作
18:29:07:696 Thread[t1,5,main]: 获取写锁...
18:29:08:669 Thread[t2,5,main]: 释放写锁...
18:29:08:670 Thread[t1,5,main]: 写操作
18:29:09:672 Thread[t1,5,main]: 释放写锁...
2、注意事项
- 读锁不支持条件变量
- 重入时读锁不支持升级为写锁:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待
r.lock();
try {
//已经有读锁,不能升级,写锁永久等待
w.lock();
try {
} finally {
w.unlock();
}
} finally {
r.unlock();
}
- 重入时写锁支持降级读锁:即持有写锁的情况下去获取读锁
class CachedData {
Object data;
data
// 是否有效,如果失效,需要重新计算 data
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// 获取写锁前必须释放读锁
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// 判断是否有其它线程已经获取了写锁、更新了缓存, 避免重复更新
if (!cacheValid) {
data = ...;
cacheValid = true;
}
// 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock();
}
}
// 自己用完数据, 释放读锁
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
3、读写锁原理
读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个
3.1 t1 设置写锁(w.lock),t2设置读锁(r.lock )
- t1 成功上锁,流程与 ReentrantLock 加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位
- t2 执行 r.lock,这时进入读锁的 sync.acquireShared(1) 流程,首先会进入 tryAcquireShared 流程。如果有写 锁占据,那么 tryAcquireShared 返回 -1 表示失败
tryAcquireShared 返回值表示
- -1 表示失败
- 0 表示成功,但后继节点不会继续唤醒
- 正数表示成功,而且数值是还有几个后继节点需要唤醒,读写锁返回 1
//1、 r.lock
public void lock() {
sync.acquireShared(1);
}
//2
public final void acquireShared(int arg) {
//尝试加读锁
if (tryAcquireShared(arg) < 0)
//小于零说明加锁失败,下面介绍
doAcquireShared(arg);
}
//3、尝试加读锁
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 如果是其它线程持有写锁, 获取读锁失败
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
//低16位不为0,说明加了写锁,而且当前Owner不为自己,则不能加读锁
return -1;
int r = sharedCount(c);
if (
// 读锁不该阻塞(如果老二是写锁,读锁该阻塞), 并且
!readerShouldBlock() &&
// 小于读锁计数, 并且
r < MAX_COUNT &&
// 尝试增加计数成功
compareAndSetState(c, c + SHARED_UNIT)) {
// ... 省略不重要的代码
return 1;
}
return fullTryAcquireShared(current);
}
- 这时会进入 sync.doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态
private void doAcquireShared(int arg) {
// 将当前线程关联到一个 Node 对象上, 模式为共享模式
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
// 再一次尝试获取读锁
int r = tryAcquireShared(arg);
// 如果成功
if (r >= 0) {
// ㈠
// r 表示可用资源数, 在这里总是 1 允许传播
//(唤醒 AQS 中下一个 Share 节点)
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if ( // 是否在获取读锁失败时阻塞(前一个阶段 waitStatus == Node.SIGNAL)
shouldParkAfterFailedAcquire(p, node) &&
// park 当前线程
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
-
t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared(1) 来尝试获取锁
-
如果没有成功,在 doAcquireShared 内 for (;😉 循环一次,把前驱节点的 waitStatus 改为 -1,再 for (;😉 循环一 次尝试 tryAcquireShared(1) 如果还不成功,那么在 parkAndCheckInterrupt() 处 park
3.2 t3 r.lock,t4 w.lock
在上面的基础上,假设又有 t3 加读锁和 t4 加写锁,这期间 t1 仍然持有锁,就变成了下面的样子
3.3 t1 w.unlock
这时会走到写锁的 sync.release(1) 流程,调用 sync.tryRelease(1) 成功,变成下面的样子
//sync.release(1)
public final boolean release(int arg) {
//尝试释放锁
if (tryRelease(arg)) {
// unpark AQS 中等待的线程
Node h = head;
if (h != null && h.waitStatus != 0)
//调用unpark进行唤醒
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
// 因为可重入的原因, 写锁计数为 0, 才算释放成功
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
- 接下来执行唤醒流程 sync.unparkSuccessor,即让老二恢复运行,这时 t2 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行
- 这回再来一次 for (;😉 执行 tryAcquireShared 成功则让读锁计数加一
- 这时 t2 已经恢复运行,接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
- 在 setHeadAndPropagate 方法内还会检查下一个节点是否是 shared,如果是则调用 doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒老二,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行。这就是读锁可以共享的一部分原因
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//如果后面的老二,仍然是读锁,则继续唤醒
if (s == null || s.isShared())
doReleaseShared();
}
}
- 这回再来一次 for (;😉 执行 tryAcquireShared 成功则让读锁计数加一
- 这时 t3 已经恢复运行,接下来 t3 调 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
- 下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点
3.4 t2 r.unlock,t3 r.unlock
- t2 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但由于计数还不为零,此时不会唤醒t4
- t3 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,这回计数为零了,进入 doReleaseShared() 将头节点从 -1 改为 0 并唤醒老二,即
- 之后 t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;😉 这次自己是老二,并且没有其他 竞争,tryAcquire(1) 成功,修改头结点,流程结束
4、StampedLock
该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用
加解读锁
long stamp = lock.readLock();
lock.unlock(stamp);
加解写锁
long stamp = lock.writeLock();
lock.unlockWrite(stamp);
乐观读,StampedLock 支持 tryOptimisticRead()
方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通 过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。
long stamp = lock.tryOptimisticRead();
// 验戳
if(!lock.validate(stamp)){
// 锁升级
}
使用示例
提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法
class DataContainerStamped {
private int data;
private final StampedLock lock = new StampedLock();
public DataContainerStamped(int data) {
this.data = data;
}
public int read(int readTime) {
long stamp = lock.tryOptimisticRead();
log.debug("optimistic read locking...{}", stamp);
sleep(readTime);
//首先尝试乐观读,如果stamp没有改变说明读成功,返回结果
if (lock.validate(stamp)) {
log.debug("read finish...{}, data:{}", stamp, data);
return data;
}
//否则升级为读锁
log.debug("updating to read lock... {}", stamp);
try {
stamp = lock.readLock();
log.debug("read lock {}", stamp);
sleep(readTime);
log.debug("read finish...{}, data:{}", stamp, data);
return data;
} finally {
log.debug("read unlock {}", stamp);
lock.unlockRead(stamp);
}
}
public void write(int newData) {
long stamp = lock.writeLock();
log.debug("write lock {}", stamp);
try {
sleep(2);
this.data = newData;
} finally {
log.debug("write unlock {}", stamp);
lock.unlockWrite(stamp);
}
}
}
测试 读-读
可以优化
public static void main(String[] args) {
DataContainerStamped dataContainer = new DataContainerStamped(1);
new Thread(() -> {
dataContainer.read(1);
}, "t1").start();
sleep(0.5);
new Thread(() -> {
dataContainer.read(0);
}, "t2").start();
}
结果可以看出,t1和t2并没相互阻塞,而且都没有升级为读锁
15:58:50.217 c.DataContainerStamped [t1] - optimistic read locking...256
15:58:50.717 c.DataContainerStamped [t2] - optimistic read locking...256
15:58:50.717 c.DataContainerStamped [t2] - read finish...256, data:1
15:58:51.220 c.DataContainerStamped [t1] - read finish...256, data:1
测试 读-写
时优化读补加读锁
public static void main(String[] args) {
DataContainerStamped dataContainer = new DataContainerStamped(1);
new Thread(() -> {
dataContainer.read(1);
}, "t1").start();
sleep(0.5);
new Thread(() -> {
dataContainer.write(100);
}, "t2").start();
}
结果可以看出读操作被写操作阻塞,而且乐观读升级为了读锁
15:57:00.219 c.DataContainerStamped [t1] - optimistic read locking...256
15:57:00.717 c.DataContainerStamped [t2] - write lock 384
15:57:01.225 c.DataContainerStamped [t1] - updating to read lock... 256
15:57:02.719 c.DataContainerStamped [t2] - write unlock 384
15:57:02.719 c.DataContainerStamped [t1] - read lock 513
15:57:03.719 c.DataContainerStamped [t1] - read finish...513, data:1000
15:57:03.719 c.DataContainerStamped [t1] - read unlock 513
注意
- StampedLock 不支持条件变量
- StampedLock 不支持可重入
上面两个特性决定了它替代不不了ReentrantReadWriteLock
和ReentrantLock