1.jdk文档:
2.ReentrantReadWriteLock概述
大型网站中很重要的一块内容就是数据的读写,ReentrantLock虽然具有完全互斥排他的效果(即同一时间只有一个线程正在执行lock后面的任务),但是效率非常低。所以在JDK中提供了一种读写锁ReentrantReadWriteLock,使用它可以加快运行效率。
读写锁表示两个锁,一个是读操作相关的锁,称为共享锁;另一个是写操作相关的锁,称为排他锁。我把这两个操作理解为三句话:
- 读和读之间不互斥,因为读操作不会有线程安全问题
- 写和写之间互斥,避免一个写操作影响另外一个写操作,引发线程安全问题
- 读和写之间互斥,避免读操作的时候写操作修改了内容,引发线程安全问题
总结起来就是,多个Thread可以同时进行读取操作,但是同一时刻只允许一个Thread进行写入操作。
即:
- 读与读:异步
- 读与写:同步
- 写与写:同步
3.案例:缓存
建一个缓存,可以往缓存中存入数据,读取数据,创建线程来写入和读取缓存中的数据。
3.1 没有加读写锁的情况:
public class ReadWriteLockTest {
public static void main(String[] args) {
MyCacheLock myCacheLock = new MyCacheLock();
// 写入
for (int i = 0; i < 5; i++) {
final String temp = String.valueOf(i);
new Thread(() -> {
myCacheLock.put(temp, temp);
}).start();
}
// 读取
for (int i = 0; i < 5; i++) {
final String temp = String.valueOf(i);
new Thread(() -> {
myCacheLock.get(temp);
}).start();
}
}
}
class MyCacheLock {
private volatile Map<String, String> map = new HashMap<>();
// 存
public void put(String key, String value) {
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入ok");
}
// 取
public void get(String key) {
System.out.println(Thread.currentThread().getName() + "读取" + key);
map.get(key);
System.out.println(Thread.currentThread().getName() + "读取ok");
}
}
结果:
Thread-2写入2
Thread-1写入1
Thread-4读取1
Thread-2写入ok
Thread-3读取0
Thread-3读取ok
Thread-0写入0
Thread-0写入ok
Thread-1写入ok
Thread-5读取2
Thread-5读取ok
Thread-4读取ok
从结果可以看到,2个写入线程同时进行了,而我们的写入是需要同步的,所以这里就出现了问题。
3.2 添加读写锁:
代码:
class MyCacheLock {
private volatile Map<String, String> map = new HashMap<>();
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 写, 写入的时候只能有一个线程在写
public void put(String key, String value) {
try {
readWriteLock.writeLock().lock();
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入ok");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
// 取, 读取的时候可以几个线程一起读
public void get(String key) {
try {
readWriteLock.readLock().lock();
System.out.println(Thread.currentThread().getName() + "读取" + key);
map.get(key);
System.out.println(Thread.currentThread().getName() + "读取ok");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
结果:
Thread-1写入1
Thread-1写入ok
Thread-2写入2
Thread-2写入ok
Thread-0写入0
Thread-0写入ok
Thread-3读取0
Thread-5读取2
Thread-4读取1
Thread-3读取ok
Thread-5读取ok
Thread-4读取ok
可以看到写入的时候是同步执行,但是读取的时候是异步执行。说明读写锁发挥了效果,读异步,写同步。
4.读写锁的注意事项:
- 读锁不支持条件变量
- 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待
r.lock();
try {
// ...
w.lock();
try {
// ...
} finally{
w.unlock();
}
} finally{
r.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();
}
}
}
5.应用之缓存:
5.1 缓存更新策略
- 更新时,是先清缓存还是先更新数据库?先清缓存
2. 先更新数据库:
- 补充一种情况,假设查询线程 A 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询:这种情况的出现几率非常小
5.2 读写锁实现一致性缓存:
/**
* ReentrantReadWriteLock读写锁测试
*/
public class Test35 {
public static void main(String[] args) {
GeneriCacheDao<Object> generiCacheDao = new GeneriCacheDao<>();
Object[] objects = new Object[2];
generiCacheDao.queryOne(Object.class,"Test",objects);
generiCacheDao.queryOne(Object.class,"Test",objects);
generiCacheDao.queryOne(Object.class,"Test",objects);
generiCacheDao.queryOne(Object.class,"Test",objects);
System.out.println(generiCacheDao.map);
generiCacheDao.update("Test",objects);
System.out.println(generiCacheDao.map);
}
}
class GeneriCacheDao<T>{
HashMap<SqlPair, T> map = new HashMap<>();
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
GenericDao genericDao = new GenericDao();
public int update(String sql, Object... params){
SqlPair sqlPair = new SqlPair(sql, params);
lock.writeLock().lock();
try {
// 先查询数据库再更新缓存,但是这里加了锁,谁先谁后都没关系
int update = genericDao.update(sql, params);
map.clear();
return update;
} finally {
lock.writeLock().unlock();
}
}
public T queryOne(Class<T> beanClass, String sql, Object... params){
SqlPair key = new SqlPair(sql, params);
// 加读锁, 防止其它线程对缓存更改
lock.readLock().lock();
try {
T t = map.get(key);
if (t!=null){
return t;
}
} finally {
lock.readLock().unlock();
}
// 加写锁, 防止其它线程对缓存读取和更改
lock.writeLock().lock();
// get 方法上面部分是可能多个线程进来的, 可能已经向缓存填充了数据
// 为防止重复查询数据库, 再次验证
try {
T value = map.get(key);
if (value==null){
value = (T) genericDao.queryOne(beanClass, sql, params);
map.put(key, value);
}
return value;
} finally {
lock.writeLock().unlock();
}
}
class SqlPair{
private String sql;
private Object[] params;
public SqlPair(String sql, Object[] params) {
this.sql = sql;
this.params = params;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SqlPair sqlMap = (SqlPair) o;
return Objects.equals(sql, sqlMap.sql) &&
Arrays.equals(params, sqlMap.params);
}
@Override
public int hashCode() {
int result = Objects.hash(sql);
result = 31 * result + Arrays.hashCode(params);
return result;
}
}
}
class GenericDao<T>{
public int update(String sql, Object... params){
return 1;
}
public T queryOne(Class<T> beanClass, String sql, Object... params){
System.out.println("查询数据库中");
return (T) new Object();
}
}
5.3 注意:
- 以上实现体现的是读写锁的应用,保证缓存和数据库的一致性,但有下面的问题没有考虑
- 适合读多写少,如果写操作比较频繁,以上实现性能低
- 没有考虑缓存容量
- 没有考虑缓存过期
- 只适合单机
- 并发性还是低,目前只会用一把锁
- 更新方法太过简单粗暴,清空了所有的key(考虑按类型分区或者重新设计key)
6.读写锁原理
读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个
6.1 执行1:t1线程加写锁,t2线程加读锁:t1 w.lock,t2 r.lock
6.1.1 t1 成功上锁
流程与 ReentrantLock 加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,如果是0就是未加锁,1就是已经锁;而读锁使用的是 state 的高 16 位
6.1.2 t2 执行 r.lock
这时进入读锁的 sync.acquireShared(1) 流程,首先会进入 tryAcquireShared 流程。如果有写 锁占据,那么 tryAcquireShared 返回 -1 表示失败
tryAcquireShared 返回值表示:
- -1 表示失败
- 0 表示成功,但后继节点不会继续唤醒
- 正数表示成功,而且数值是还有几个后继节点需要唤醒,我们这里的读写锁返回 1
6.1.3 进入 sync.doAcquireShared(1) 流程
首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态
6.1.4 t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared(1) 来尝试获取锁
6.1.5 没有成功
在 doAcquireShared 内 for (;;) 循环一次,把前驱节点的 waitStatus 改为 -1,再 for (;;) 循环一 次尝试 tryAcquireShared(1) 如果还不成功,那么在 parkAndCheckInterrupt() 处 park
6.2 继续执行2:t3线程加读锁,t4线程加解锁
这种状态下,假设又有 t3 加读锁和 t4 加写锁,这期间 t1 仍然持有锁,就变成了下面的样子
其中t2和t3都是读锁,所以是Shared,t4是写锁所以是Ex
t2和t3的状态是-1,有任务来幻想后继节点
6.3 继续执行t1 w.unlock
6.3.1 这时会走到写锁的 sync.release(1) 流程,调用 sync.tryRelease(1) 成功,变成下面的样子
6.3.2 接下来执行唤醒流程 sync.unparkSuccessor
即让老二恢复运行,这时 t2 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行,图中的t2从黑色变成了蓝色(注意这里只是恢复运行而已,并没有获取到锁!) 这回再来一次 for (;;) 执行 tryAcquireShared 成功则让读锁计数加一
6.3.3 这时 t2 已经恢复运行,接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
6.3.4 在 setHeadAndPropagate 方法内还会检查下一个节点是否是 shared
如果是则调用 doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒老二,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行
6.3.5 回再来一次 for (;;) 执行 tryAcquireShared 成功则让读锁计数加一
6.3.6 这时 t3 已经恢复运行,接下来 t3 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点
6.4 再继续执行t2 r.unlock,t3 r.unlock
6.4.1 t2 进入 sync.releaseShared(1) 中
调用 tryReleaseShared(1) 让计数减一,但由于计数还不为零
6.4.2 t3 进入 sync.releaseShared(1) 中
调用 tryReleaseShared(1) 让计数减一,这回计数为零了,进入 doReleaseShared() 将头节点从 -1 改为 0 并唤醒老二,即
6.4.3 之后 t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行
再次 for (;;) 这次自己是老二,并且没有其他 竞争,tryAcquire(1) 成功,修改头结点,流程结束
6.5 源码分析:
6.5.1 写锁上锁流程
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;
}
}
6.5.2 写锁释放流程
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;
}
}
6.5.3 读锁上锁流程
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;
}
}
}
6.5.4 读锁释放流程
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;
}
}
}
7. StampedLock读写锁
该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用
加解读锁
long stamp = lock.readLock();
lock.unlockRead(stamp);
加解写锁
long stamp = lock.writeLock();
lock.unlockWrite(stamp);
乐观读,StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。
long stamp = lock.tryOptimisticRead();
// 验戳
if(!lock.validate(stamp)){
// 锁升级
}
7.1 测试读读:
/**
* StampedLock 测试
* StampedLock 不支持条件变量
* StampedLock 不支持可重入
*/
@Slf4j(topic = "stampedLock")
public class TestStampedLock {
public static void main(String[] args) throws InterruptedException {
DataContainerStamped dataContainer = new DataContainerStamped(1);
/**
* 可以看到实际没有加读锁
*/
new Thread(() -> {
try {
dataContainer.read(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
Thread.sleep(500);
new Thread(() -> {
try {
dataContainer.read(0);
// dataContainer.write(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
}
}
@Slf4j(topic = "data-stamped")
class DataContainerStamped {
private int data;
final StampedLock lock = new StampedLock();
public DataContainerStamped(int data){
this.data = data;
}
public int read(int readTime) throws InterruptedException {
long stamp = lock.tryOptimisticRead();
log.debug("尝试使用乐观读...{}", stamp);
Thread.sleep(readTime);
if (lock.validate(stamp)){
log.debug("成功使用了乐观读{},数据 {}", stamp, data);
return this.data;
}
try {
// 锁升级 - 读锁
log.debug("乐观读锁升级到写锁加锁 {}", stamp);
stamp = lock.readLock();
log.debug("乐观读锁升级到写锁完成 {}", stamp);
Thread.sleep(readTime);
return data;
} finally {
log.debug("乐观读锁升级到写锁解锁 {}", stamp);
lock.unlock(stamp);
}
}
public void write(int data) throws InterruptedException {
long stamp = lock.writeLock();
log.info(" 加上写锁 {}", stamp);
try {
Thread.sleep(1000);
this.data = data;
} finally {
log.debug("写锁解锁 {}", stamp);
lock.unlock(stamp);
}
}
}
结果:可以看到没有加读锁
17:41:32.555 [t1] DEBUG data-stamped - 尝试使用乐观读...256
17:41:33.062 [t2] DEBUG data-stamped - 尝试使用乐观读...256
17:41:33.063 [t2] DEBUG data-stamped - 成功使用了乐观读256,数据 1
17:41:33.566 [t1] DEBUG data-stamped - 成功使用了乐观读256,数据 1
7.2 测试读写:
public static void main(String[] args) throws InterruptedException {
DataContainerStamped dataContainer = new DataContainerStamped(1);
/**
* 可以看到实际没有加读锁
*/
new Thread(() -> {
try {
dataContainer.read(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
Thread.sleep(500);
new Thread(() -> {
try {
// dataContainer.read(0);
dataContainer.write(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
}
结果:
17:40:06.391 [t1] DEBUG data-stamped - 尝试使用乐观读...256
17:40:06.891 [t2] INFO data-stamped - 加上写锁 384
17:40:07.410 [t1] DEBUG data-stamped - 乐观读锁升级到写锁加锁 256
17:40:07.904 [t2] DEBUG data-stamped - 写锁解锁 384
17:40:07.905 [t1] DEBUG data-stamped - 乐观读锁升级到写锁完成 513
17:40:08.920 [t1] DEBUG data-stamped - 乐观读锁升级到写锁解锁 513
7.3 不足之处
虽然有StampedLock,但是还是不能完全取代ReentrantReadWriteLock的作用:
- StampedLock不支持条件变量
- StampedLock不支持可重入