Java并发编程--JUC并发工具类之StampedLock

摘要

  • 本文介绍StampedLock相关技术

  • 本文基于jdk1.8

StampedLock介绍

  • 如果我们深入分析ReentrantReadWriteLock,会发现它有个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。

  • 为了进一步提升并发执行效率,Java 8引入了新的读写锁:StampedLock。

  • StampedLockReentrantReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入!在原先读写锁的基础上新增了一种叫乐观读(Optimistic Reading)的模式。该模式并不会加锁,所以不会阻塞线程,会有更高的吞吐量和更高的性能。

  • 它的设计初衷是作为一个内部工具类,用于开发其他线程安全的组件,提升系统性能,并且编程模型也比ReentrantReadWriteLock 复杂,所以用不好就很容易出现死锁或者线程安全等莫名其妙的问题。

  • 注意:StampedLock是Java 8引入的类,需要使用支持Java 8及更高版本的编译器和运行时环境。

StampLock三种访问模式

  • Writing(独占写锁):writeLock 方法会使线程阻塞等待独占访问,可类比ReentrantReadWriteLock 的写锁模式,同一时刻有且只有一个写线程获取锁资源;

  • Reading(悲观读锁):readLock方法,允许多个线程同时获取悲观读锁,悲观读锁与独占写锁互斥,与乐观读共享。

  • Optimistic Reading(乐观读):这里需要注意了,乐观读并没有加锁,也就是不会有 CAS 机制并且没有阻塞线程。仅当当前未处于 Writing 模式 tryOptimisticRead 才会返回非 0 的邮戳(Stamp),如果在获取乐观读之后没有出现写模式线程获取锁,则在方法validate返回 true ,允许多个线程获取乐观读以及读锁,同时允许一个写线程获取写锁。

乐观读编程模型的模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void optimisticRead() {
// 1. 非阻塞乐观读模式获取版本信息
long stamp = lock.tryOptimisticRead();

// 2. 拷贝共享数据到线程本地栈中,即将共享变量赋值给方法内变量
copyVaraibale2ThreadMemory();

// 3. 校验乐观读模式读取的数据是否被修改过
if (!lock.validate(stamp)) {
// 3.1 校验未通过,上读锁
stamp = lock.readLock();
try {
// 3.2 拷贝共享变量数据到局部变量
copyVaraibale2ThreadMemory();
} finally {
// 释放读锁
lock.unlockRead(stamp);
}
}

// 3.3 校验通过,使用线程本地栈的数据进行逻辑操作
useThreadMemoryVarables();
}

StampedLock的代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import java.util.concurrent.locks.StampedLock;

public class StampedLockExample {
private double x;
private double y;
private final StampedLock lock = new StampedLock();

/**
* 移动坐标点
*
* @param deltaX X轴增量
* @param deltaY Y轴增量
*/
public void move(double deltaX, double deltaY) {
long stamp = lock.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
lock.unlockWrite(stamp); // 释放写锁
}
}

/**
* 计算当前坐标点与原点的距离
*
* @return 原点的距离
*/
public double distanceFromOrigin() {
long stamp = lock.tryOptimisticRead(); // 尝试获取乐观读锁
double currentX = x;
double currentY = y;
if (!lock.validate(stamp)) { // 校验乐观读锁是否有效
stamp = lock.readLock(); // 乐观读锁无效则获取悲观读锁
try {
currentX = x;
currentY = y;
} finally {
lock.unlockRead(stamp); // 释放悲观读锁
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}

/**
* 如果当前坐标在原点,则移动坐标
*
* @param newX 新的X坐标
* @param newY 新的Y坐标
*/
public void moveIfAtOrigin(double newX, double newY) {
long stamp = lock.readLock(); // 获取悲观读锁
try {
while (x == 0.0 && y == 0.0) {
long writeStamp = lock.tryConvertToWriteLock(stamp); // 尝试将悲观读锁升级为写锁
if (writeStamp != 0L) { // 升级成功
x = newX;
y = newY;
stamp = writeStamp;
break;
} else {
lock.unlockRead(stamp); // 释放悲观读锁
stamp = lock.writeLock(); // 获取写锁
}
}
} finally {
lock.unlock(stamp); // 释放锁(读锁或写锁)
}
}
}
  • 在上述示例中,StampedLockExample类使用了StampedLock来管理对x和y坐标的访问。

  • 其中move方法使用写锁来更新x和y坐标的值。

  • distanceFromOrigin方法使用乐观读锁尝试读取xy坐标的值,如果乐观读锁无效,则获取悲观读锁来读取。

  • moveIfAtOrigin方法首先获取悲观读锁,然后检查当前坐标是否在原点。如果在原点,则尝试将悲观读锁升级为写锁,以便进行坐标更新。如果升级失败,则释放悲观读锁并获取写锁。

StampedLock使用场景

  • 适用于读多写少的高并发场景

使用StampedLock的注意事项

  • StampedLock 写锁是不可重入的,如果当前线程已经获取了写锁,再次重复获取的话就会死锁,使用过程中一定要注意;

  • StampedLock的悲观读、写锁都不支持条件变量 Conditon ,当需要这个特性的时候需要注意;

  • 如果线程阻塞在 StampedLockreadLock() 或者 writeLock() 上时,此时调用该阻塞线程的 interrupt() 方法,会导致 CPU 飙升。所以,使用 StampedLock 一定不要调用中断操作,如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()

ReentrantLock 、 ReentrantReadWriteLock 和 StampedLock 的比较

特性 ReentrantLock ReentrantReadWriteLock StampedLock
锁类型 独占锁(Exclusive Lock) 读写锁(Read-Write Lock) 乐观读锁和悲观读锁(Optimistic and Pessimistic Read Locks)
读-读并发性 不支持 支持 支持
读-写并发性 不支持 支持 支持
写-写并发性 不支持 不支持 不支持
锁的公平性 支持设置为公平或非公平锁 支持设置为公平或非公平锁 不支持设置公平性(非公平锁)
性能 适用于读操作少、写操作多的场景 适用于读操作频繁、写操作较少的场景 适用于乐观读多于写的场景
锁降级 不支持 支持 不支持
可重入性 支持 支持 不支持
API 提供基本锁操作方法(lock、unlock、tryLock等) 提供与ReentrantLock类似的锁操作方法,以及读锁和写锁的获取方法 提供乐观读锁和悲观读锁的获取方法,以及锁的转换操作
适用场景 读操作较少、写操作较多的场景,互斥访问共享资源 读操作频繁、写操作较少的场景,读多写少的并发访问 读操作多于写操作的场景,乐观读锁性能好