1.ReentrantReadWriteLock使用
当读操作远远高于写操作时,这时候使用 读写锁让 读-读可以并行,提高性能。读-写,写-写操作都是相互互斥的!
代码举例
提供一个 数据容器类内部分别使用读锁保护数据的read()方法,写锁保护数据的 write()方法
/**
* @ClassName TestReadWriteLock
* @author: shouanzh
* @Description 读读可以并发、读写、写写互斥
* @date 2022/3/19 13:27
*/
@Slf4j
public class TestReadWriteLock {
public static void main(String[] args) throws InterruptedException {
DataContainer dataContainer = new DataContainer();
Thread t1 = new Thread(()->{
// dataContainer.read();
dataContainer.write();
},"t1");
Thread t2 = new Thread(()->{
// dataContainer.read();
dataContainer.write();
},"t2");
t1.start();
Thread.sleep(100);
t2.start();
}
}
@Slf4j
class DataContainer {
// 要保护的共享数据
private Object data;
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 读锁
private final ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
// 写锁
private final ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
/**
* 读操作
* @return Object
*/
public Object read() {
log.debug("获取读锁...");
readLock.lock();
try {
log.debug("读取操作");
Thread.sleep(1000);
return data;
} catch (InterruptedException e) {
e.printStackTrace();
return null;
} finally {
log.debug("释放读锁...");
readLock.unlock();
}
}
/**
* 写操作
*/
public void write() {
log.debug("获取写锁...");
writeLock.lock();
try {
log.debug("写入操作");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log.debug("释放写锁...");
writeLock.unlock();
}
}
}
注意事项
- 读锁不支持条件变量
- 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待
readLock.lock();
try {
// ...
writeLock.lock();
try {
// ...
} finally{
writeLock.unlock();
}
} finally{
readLock.unlock();
}
- 重入时降级支持:即持有写锁的情况下去获取读锁
class CachedData {
Object 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();
}
}
}
2.ReentrantReadWriteLock原理
读写锁用的是同一个Sycn同步器,因此等待队列、 state等也是同一个
2.1 图解流程
t1 writeLock.lock(),t2 readLock.lock() ,t1线程加写锁 t2线程加读锁
1) t1成功上锁,流程与 ReentrantLock加锁相比没有特殊之处,不同是写锁状态占了 state的低16位,而读锁使用的是 state的高16位
2)t2 执行 readLock.lock() ,这时进入读锁的 sync.acquireShared(1)流程,首先会进入 tryAcquireShared流程.如果有写锁占据,那么 tryAcquireShared 返回-1表示失败
tryAcquireShared 返回值表示
-1表示失败
0表示成功,但后继节点不会继续唤醒
正数表示成功,而且数值是还有几个后继节点需要唤醒,读写锁返回1
3)这时会进入 sync.doAcquireShared(1)流程,首先也是调用addWaiter 添加节点,不同 之处在于节点被设置为Node.SHARED模式而非Node.EXCLUSIVE模式,注意此时t2 仍处于活跃状态
4) t2会看看自己的节点是不是老二,如果是,还会再次调用tryAcquireShared(1)来尝 试获取锁
5)如果没有成功,在doAcquireShared 内 for(;; )循环一次,把前驱节点的 waitStatus 改 为-1,再 for(;; )循环一次尝试 tryAcquireShared(1)如果还不成功,那么在 parkAndCheckInterrupt() 处 park
又继续执行:t3 r.lock,t4 w.lock
这种状态下,假设又有t3加读锁和t4加写锁,这期间t1仍然持有锁,就变成了下面的 样子
继续执行 t1 w.unlock
这时会走到写锁的 sync.release(1)流程,调用sync.tryRelease(1)成功,变成下面的样子
接下来执行唤醒流程 sync.unparkSuccessor,即让老二恢复运行,这时 t2 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行
这回再来一次for(;; )执行 tryAcquireShared 成功则让读锁计数加一
这时t2已经恢复运行,接下来t2调用 setHeadAndPropagate(node, 1),它原本所在节点 被置为头节点
事情还没完,在 setHeadAndPropagate 方法内还会检查下一个节点是否是 shared, 如果 是则调用 doReleaseShared() 将 head 的状态从-1 改为0并唤醒老二,这时t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行
这回再来一次for(;; )执行 tryAcquireShared 成功则让读锁计数加一
这时t3已经恢复运行,接下来t3调用 setHeadAndPropagate(node, 1),它原本所在节点 被置为头节点
下一个节点不是 shared了,因此不会继续唤醒t4所在节点
再继续执行t2 r.unlock,t3 r.unlock
t2进入 sync.releaseShared(1)中,调用tryReleaseShared(1)让计数减一,但由于计数还不 为零
t3进入 sync.releaseShared(1)中,调用 tryReleaseShared(1)让计数减一,这回计数为零 了,进入 doReleaseShared()将头节点从-1改为0并唤醒老二,即
之后t4在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for(;; )这次自己是 老二,并且没有其他竞争,tryAcquire(1)成功,修改头结点,流程结束
2.2 源码分析
写锁上锁流程
static final class NonfairSync extends Sync {
// ... 省略无关代码
// 外部类 WriteLock 方法, 方便阅读, 放在此处
public void lock() {
sync.acquire(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final void acquire(int arg) {
if (
// 尝试获得写锁失败
!tryAcquire(arg) &&
// 将当前线程关联到一个 Node 对象上, 模式为独占模式
// 进入 AQS 队列阻塞
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
) {
selfInterrupt();
}
}
// Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryAcquire(int acquires) {
// 获得低 16 位, 代表写锁的 state 计数
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
if (
// c != 0 and w == 0 表示有读锁返回错误,读锁不支持锁升级, 或者
w == 0 ||
// c != 0 and w == 0 表示有写,如果 exclusiveOwnerThread 不是自己
current != getExclusiveOwnerThread()
) {
// 获得锁失败
return false;
}
// 写锁计数超过低 16 位, 报异常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 写锁重入, 获得锁成功
setState(c + acquires);
return true;
}
if (
// 判断写锁是否该阻塞这里返回false, 或者
writerShouldBlock() ||
// 尝试更改计数失败
!compareAndSetState(c, c + acquires)
) {
// 获得锁失败
return false;
}
// 获得锁成功
setExclusiveOwnerThread(current);
return true;
}
// 非公平锁 writerShouldBlock 总是返回 false, 无需阻塞
final boolean writerShouldBlock() {
return false;
}
}
写锁释放流程:
static final class NonfairSync extends Sync {
// ... 省略无关代码
// WriteLock 方法, 方便阅读, 放在此处
public void unlock() {
sync.release(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final boolean release(int arg) {
// 尝试释放写锁成功
if (tryRelease(arg)) {
// unpark AQS 中等待的线程
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// Sync 继承过来的方法, 方便阅读, 放在此处
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;
}
}
读锁上锁流程
static final class NonfairSync extends Sync {
// ReadLock 方法, 方便阅读, 放在此处
public void lock() {
sync.acquireShared(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final void acquireShared(int arg) {
// tryAcquireShared 返回负数, 表示获取读锁失败
if (tryAcquireShared(arg) < 0) {
doAcquireShared(arg);
}
}
// Sync 继承过来的方法, 方便阅读, 放在此处
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 如果是其它线程持有写锁, 获取读锁失败
if (
exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current
) {
return -1;
}
int r = sharedCount(c);
if (
// 读锁不该阻塞(如果老二是写锁,读锁该阻塞), 并且
!readerShouldBlock() &&
// 小于读锁计数, 并且
r < MAX_COUNT &&
// 尝试增加计数成功
compareAndSetState(c, c + SHARED_UNIT)
) {
// ... 省略不重要的代码
return 1;
}
return fullTryAcquireShared(current);
}
// 非公平锁 readerShouldBlock 看 AQS 队列中第一个节点是否是写锁
// true 则该阻塞, false 则不阻塞
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
// AQS 继承过来的方法, 方便阅读, 放在此处
// 与 tryAcquireShared 功能类似, 但会不断尝试 for (;;) 获取读锁, 执行过程中无阻塞
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) {
// ... 省略不重要的代码
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
// ... 省略不重要的代码
return 1;
}
}
}
// AQS 继承过来的方法, 方便阅读, 放在此处
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);
}
}
// ㈠ AQS 继承过来的方法, 方便阅读, 放在此处
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
// 设置自己为 head
setHead(node);
// propagate 表示有共享资源(例如共享读锁或信号量)
// 原 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
// 现在 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 如果是最后一个节点或者是等待共享读锁的节点
if (s == null || s.isShared()) {
// 进入 ㈡
doReleaseShared();
}
}
}
// ㈡ AQS 继承过来的方法, 方便阅读, 放在此处
private void doReleaseShared() {
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE, 为了解决 bug, 见后面分析,参考这里:http://www.tianxiaobo.com/2018/05/01/AbstractQueuedSynchronizer-%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90-%E7%8B%AC%E5%8D%A0-%E5%85%B1%E4%BA%AB%E6%A8%A1%E5%BC%8F/#5propagate-%E7%8A%B6%E6%80%81%E5%AD%98%E5%9C%A8%E7%9A%84%E6%84%8F%E4%B9%89
for (;;) {
Node h = head;
// 队列还有节点
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 下一个节点 unpark 如果成功获取读锁
// 并且下下个节点还是 shared, 继续 doReleaseShared
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
}
读锁释放流程
static final class NonfairSync extends Sync {
// ReadLock 方法, 方便阅读, 放在此处
public void unlock() {
sync.releaseShared(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryReleaseShared(int unused) {
// ... 省略不重要的代码
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc)) {
// 读锁的计数不会影响其它获取读锁线程, 但会影响其它获取写锁线程
// 计数为 0 才是真正释放
return nextc == 0;
}
}
}
// AQS 继承过来的方法, 方便阅读, 放在此处
private void doReleaseShared() {
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// 如果有其它线程也在释放读锁,那么需要将 waitStatus 先改为 0
// 防止 unparkSuccessor 被多次执行
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
// 如果已经是 0 了,改为 -3,用来解决传播性,见后文信号量 bug 分析
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
}
3.StampedLock
该类自 JDK8加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用
加解读锁
long stamp = lock.readLock();
lock.unlockRead(stamp);
加解写锁
long stamp = lock.writeLock();
lock.unlockWrite(stamp);
乐观读,StampedLock支持tryOptimisticRead()方法(乐观读),读取完毕后需要做一次戳校验 如果校验 通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安 全。
long stamp = lock.tryOptimisticRead();
// 验戳
if(!lock.validate(stamp)){
// 锁升级
}
代码演示
提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法。
/**
* StampedLock 不支持条件变量
* StampedLock 不支持可重入
*/
@Slf4j
public class StampedLockTest {
public static void main(String[] args) throws InterruptedException {
StampedLockDataContainer dataContainer = new StampedLockDataContainer(1);
Thread t1 = new Thread(() -> {
try {
int read = dataContainer.read(1);
log.debug("{}",read);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
Thread.sleep(500);
Thread t2 = new Thread(() -> {
// try {
// dataContainer.read(0);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
dataContainer.write(1000);
}, "t2");
t2.start();
}
}
@Slf4j
class StampedLockDataContainer {
private int data;
private final StampedLock stampedLock = new StampedLock();
public StampedLockDataContainer(int data) {
this.data = data;
}
public int read(int readTime) throws InterruptedException {
long stamp = stampedLock.tryOptimisticRead();
log.info("optimistic read locking ...{}", stamp);
Thread.sleep(readTime * 1000L);
if (stampedLock.validate(stamp)) {
log.info("read finish... {}", stamp);
return data;
}
// 锁升级 - 读锁
log.info("update to read lock ...{}",stamp);
try {
stamp = stampedLock.readLock();
log.info("read lock {}", stamp);
Thread.sleep(readTime * 1000L);
log.info("read finish ... {}", stamp);
return data;
} finally {
stampedLock.unlockRead(stamp);
}
}
public void write(int newData) {
long stamp = stampedLock.writeLock();
try {
log.info("write lock {}", stamp);
this.data = newData;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("write finish ... {}", stamp);
log.info("write newData ... {}", this.data);
} finally {
stampedLock.unlockWrite(stamp);
}
}
}
运行结果
读读操作
2022-03-19 17:19:33 [t1] - optimistic read locking ...256
2022-03-19 17:19:33 [t2] - optimistic read locking ...256
2022-03-19 17:19:33 [t2] - read finish... 256
2022-03-19 17:19:34 [t1] - read finish... 256
2022-03-19 17:19:34 [t1] - 1
Process finished with exit code 0
读写操作
2022-03-19 17:22:27 [t1] - optimistic read locking ...256
2022-03-19 17:22:28 [t2] - write lock 384
2022-03-19 17:22:28 [t1] - update to read lock ...256
2022-03-19 17:22:29 [t2] - write finish ... 384
2022-03-19 17:22:29 [t2] - write newData ... 1000
2022-03-19 17:22:29 [t1] - read lock 513
2022-03-19 17:22:30 [t1] - read finish ... 513
2022-03-19 17:22:30 [t1] - 1000
Process finished with exit code 0