共计 4373 个字符,预计需要花费 11 分钟才能阅读完成。
导读 | 本文主要讲解了 StampedLock的功能和使用,至于原理,StampedLock 虽然不像其它锁一样定义了内部类来实现 AQS 框架,但是 StampedLock 的基本实现思路还是利用 CLH 队列进行线程的管理,通过同步状态值来表示锁的状态和类型,具体的源码实现大家感兴趣的自己可以追踪看看。 |
想到读写锁,大家第一时间想到的可能是 ReentrantReadWriteLock。实际上,在 jdk8 以后,java 提供了一个性能更优越的读写锁并发类 StampedLock,该类的设计初衷是作为一个内部工具类,用于辅助开发其它线程安全组件,用得好,该类可以提升系统性能,用不好,容易产生死锁和其它莫名其妙的问题。本文主要和大家一起学习下 StampedLock 的功能和使用。
StampedLock的状态由版本和模式组成。锁获取方法返回一个戳,该戳表示并控制对锁状态的访问。StampedLock 提供了 3 种模式控制访问锁:
获取写锁,它是独占的,当锁处于写模式时,无法获得读锁,所有乐观读验证都将失败。
long stamp = lock.writeLock(); | |
try {....} finally {lock.unlockWrite(stamp); | |
} |
悲观的方式后去非独占读锁。
long stamp = lock.readLock(); | |
try {....} finally {lock.unlockRead(stamp); | |
} |
乐观读也就是若读的操作很多,写的操作很少的情况下,你可以乐观地认为,写入与读取同时发生几率很少,因此不悲观地使用完全的读取锁定,程序可以查看读取资料之后,是否遭到写入执行的变更,再采取后续的措施 (重新读取变更信息,或者抛出异常),这一个小小改进,可大幅度提高程序的吞吐量。
StampedLock支持 tryOptimisticRead() 方法,读取完毕后做一次戳校验,如果校验通过,表示这期间没有其他线程的写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据一致性。
long stamp = lock.tryOptimisticRead(); | |
// 验戳 | |
if(!lock.validate(stamp)){// 锁升级} |
此外,StampedLock 提供了 api 实现上面 3 种方式进行转换:
如果锁状态与给定的戳记匹配,则执行以下操作之一。如果戳记表示持有写锁,则返回它。或者,如果是读锁,如果写锁可用,则释放读锁并返回写戳记。或者,如果是乐观读,则仅在立即可用时返回写戳记。该方法在所有其他情况下返回零
如果锁状态与给定的戳记匹配,则执行以下操作之一。如果戳记表示持有写锁,则释放它并获得读锁。或者,如果是读锁,返回它。或者,如果是乐观读,则仅在立即可用时才获得读锁并返回读戳记。该方法在所有其他情况下返回零。
如果锁状态与给定的戳记匹配,那么如果戳记表示持有锁,则释放它并返回一个观察戳记。或者,如果是乐观读,则在验证后返回它。该方法在所有其他情况下返回 0,因此作为“tryUnlock”的形式可能很有用。
下面用一个例子演示下 StampedLock 的使用,例子来源 jdk 中的 javadoc。
public class Point { | |
private double x, y; | |
private final StampedLock sl = new StampedLock(); | |
void move(double deltaX, double throws{ | |
// 涉及对共享资源的修改,使用写锁 - 独占操作 | |
long stamp = sl.writeLock(); | |
log.info("writeLock lock success"); | |
Thread.sleep(500); | |
try { | |
x += deltaX; | |
y += deltaY; | |
} finally {sl.unlockWrite(stamp); | |
log.info("unlock write lock success"); | |
} | |
} | |
/** | |
* 使用乐观读锁访问共享资源 | |
* 注意:乐观读锁在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候可能其他写线程已经修改了数据,* 而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。* | |
* @return | |
*/ | |
double distanceFromOrigin() throws{long stamp = sl.tryOptimisticRead(); // 使用乐观读锁 | |
log.info("tryOptimisticRead lock success"); | |
// 睡一秒中 | |
Thread.sleep(1000); | |
double currentX = x, currentY = y; // 拷贝共享资源到本地方法栈中 | |
if (!sl.validate(stamp)) { // 如果有写锁被占用,可能造成数据不一致,所以要切换到普通读锁模式 | |
log.info("validate stamp error"); | |
stamp = sl.readLock(); | |
log.info("readLock success"); | |
try { | |
currentX = x; | |
currentY = y; | |
} finally {sl.unlockRead(stamp); | |
log.info("unlock read success"); | |
} | |
} | |
return Math.sqrt(currentX * currentX + currentY * currentY); | |
} | |
void moveIfAtOrigin(double newX, double{ // upgrade | |
// Could instead start with optimistic, not read mode | |
long stamp = sl.readLock(); | |
try {while (x == 0.0 && y == 0.0) {long ws = sl.tryConvertToWriteLock(stamp); // 读锁转换为写锁 | |
if (ws != 0L) { | |
stamp = ws; | |
x = newX; | |
y = newY; | |
break; | |
} else {sl.unlockRead(stamp); | |
stamp = sl.writeLock();} | |
} | |
} finally {sl.unlock(stamp); | |
} | |
} | |
} |
测试用例:
public void testStamped() throws InterruptedException {Point point = new Point(); | |
point.setX(1); | |
point.setY(2); | |
// 线程 0 执行了乐观读 | |
Thread thread0 = new Thread(() -> { | |
try { | |
// 乐观读 | |
point.distanceFromOrigin();} catch (InterruptedException e) {e.printStackTrace(); | |
} | |
}, "thread-0"); | |
thread0.start(); | |
Thread.sleep(500); | |
// 线程 1 执行写锁 | |
Thread thread1 = new Thread(() -> { | |
// 乐观读 | |
try {point.move(3, 4); | |
} catch (InterruptedException e) {e.printStackTrace(); | |
} | |
}, "thread-1"); | |
thread1.start(); | |
thread0.join(); | |
thread1.join();} |
结果:
性能对比
正是由于 StampedLock的乐观读模式,早就 StampedLock 的高性能和高吞吐量,那么具体的性能提高有多少呢?
下图是和 ReadWritLock 相比,在一个线程情况下,读速度是其 4 倍左右,写是 1 倍。
下图是 16 个线程情况下,读性能是其几十倍,写性能也是近 10 倍左右:
下图是吞吐量提高:
那么这样是不是说 StampedLock可以全方位的替代 ReentrantReadWriteLock, 答案是否定的,StampedLock相对于 ReentrantReadWriteLock 有下面两个问题:
所以最终选择 StampedLock还是 ReentrantReadWriteLock,还是要看具体的业务场景。
本文主要讲解了 StampedLock的功能和使用,至于原理,StampedLock 虽然不像其它锁一样定义了内部类来实现 AQS 框架,但是 StampedLock 的基本实现思路还是利用 CLH 队列进行线程的管理,通过同步状态值来表示锁的状态和类型,具体的源码实现大家感兴趣的自己可以追踪看看。
