问题
如果读取执行情况很多,写入很少的情况下,使用 ReentrantReadWriteLock 可能会使写入线程遭遇饥饿(Starvation)问题,也就是写入线程迟迟无法竞争到锁定而一直处于等待状态。
2 StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁
3 该类是一个读写锁的改进,它的思想是读写锁中读不仅不阻塞读,同时也不应该阻塞写。 乐观读不阻塞写的实现思路: 在乐观读的时候如果发生了写,则应当重读而不是在读的时候直接阻塞写! 因为在读线程非常多而写线程比较少的情况下,写线程可能发生饥饿现象,也就是因为大量的读线程存在并且读线程都阻塞写线程, 因此写线程可能几乎很少被调度成功!当读执行的时候另一个线程执行了写,则读线程发现数据不一致则执行重读即可。所以读写都存在的情况下, 使用StampedLock就可以实现一种无障碍操作,即读写之间不会阻塞对方,但是写和写之间还是阻塞的!
4 适用场景: 乐观读取模式仅用于短时间读取操作时经常能够降低竞争和提高吞吐量。当然,它的使用在本质上是脆弱的。乐观读取的区域应该只包括字段,并且在validation之后用局部变量持有它们从而在后续使用。 乐观模式下读取的字段值很可能是非常不一致的,所以它应该只用于那些你熟悉如何展示数据,从而你可以不断检查一致性和调用方法validate
5 优化点:
a 乐观读不阻塞悲观读和写操作,有利于获得写锁
b 队列头结点采用有限次数SPINS次自旋(增加开销),增加获得锁几率(因为闯入的线程会竞争锁),有效够降低上下文切换
c 读模式的集合通过一个公共节点被聚集在一起(cowait链),当队列尾节点为RMODE,通过CAS方法将该节点node添加至尾节点的cowait链中,node成为cowait中的顶元素,cowait构成了一个LIFO队列。 当队列尾节点为WMODE,当前节点直接拼接到尾节点后面,保证了相对较公平的锁。
d 不支持锁重入,如果只悲观读锁和写锁,效率没有ReentrantReadWriteLock高。
StampedLock是并发包里面jdk8版本新增的一个锁,该锁提供了三种模式的读写控制,三种模式分别如下:
- 悲观写锁writeLock,是个排它锁或者叫独占锁,同时只有一个线程可以获取该锁,当一个线程获取该锁后,其它请求的线程必须等待,当目前没有线程持有读锁或者写锁的时候才可以获取到该锁,请求该锁成功后会返回一个stamp票据变量用来表示该锁的版本,当释放该锁时候需要unlockWrite并传递参数stamp。
- 悲观读锁readLock,是个共享锁,在没有线程获取独占写锁的情况下,同时多个线程可以获取该锁,如果已经有线程持有写锁,其他线程请求获取该读锁会被阻塞。这里讲的悲观其实是参考数据库中的乐观悲观锁的,这里说的悲观是说在具体操作数据前悲观的认为其他线程可能要对自己操作的数据进行修改,所以需要先对数据加锁,这是在读少写多的情况下的一种考虑,请求该锁成功后会返回一个stamp票据变量用来表示该锁的版本,当释放该锁时候需要unlockRead并传递参数stamp。
- 乐观读锁tryOptimisticRead,是相对于悲观锁来说的,在操作数据前并没有通过CAS设置锁的状态,如果当前没有线程持有写锁,则简单的返回一个非0的stamp版本信息,获取该stamp后在具体操作数据前还需要调用validate验证下该stamp是否已经不可用,也就是看当调用tryOptimisticRead返回stamp后到到当前时间间是否有其他线程持有了写锁,如果是那么validate会返回0,否者就可以使用该stamp版本的锁对数据进行操作。由于tryOptimisticRead并没有使用CAS设置锁状态所以不需要显示的释放该锁。该锁的一个特点是适用于读多写少的场景,因为获取读锁只是使用与或操作进行检验,不涉及CAS操作,所以效率会高很多,但是同时由于没有使用真正的锁,在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候可能其他写线程已经修改了数据,而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。
public class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
public void move(double deltaX, double deltaY) { // an exclusively locked method
/**
* stampedLock调用writeLock和unlockWrite时候都会导致stampedLock的state(锁状态)属性值的变化
* 即每次高8位 +1,直到加到最大值,然后从0重新开始.
* 当锁被写模式所占有,没有读或者乐观的读操作能够成功。
*/
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
//释放写锁
sl.unlockWrite(stamp);
}
}
public double distanceFromOrigin() { // A read-only method
/**
* tryOptimisticRead是一个乐观的读,使用这种锁的读不阻塞写
* 每次读的时候得到一个当前的stamp值
*/
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
/**
* validate()方法校验从调用tryOptimisticRead()之后有没有线程获得写锁,
* true:无写锁,state与stamp匹配
* false:有写锁,state与stamp不匹配,或者stamp=0(调用tryOptimisticRead()时已经被其他线程持有写锁)
*/
if (!sl.validate(stamp)) {
/**
* 被写锁入侵需要使用悲观读锁重读,阻塞写锁(防止再次出现脏数据) 或者 等待写锁释放锁
* 当然重读的时候还可以使用tryOptimisticRead,此时需要结合循环了,即类似CAS方式
*/
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
//释放读锁
sl.unlockRead(stamp);
}
}
return (currentX + currentY);
}
/**
* 初始化 x,y
* @param newX
* @param newY
*/
public void moveIfAtOrigin(double newX, double newY) {
// 以乐观读锁的方式开始,而不是悲观读锁
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) {
/**
* 尝试转换成写锁
* 0:获得写锁失败
* 非0:获得写锁成功
*/
long ws = sl.tryConvertToWriteLock(stamp);
//持有写锁
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
}
//否则调用writeLock()直到获得写锁
else {
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
//释放锁,可以是writeLock,也可是readLock
sl.unlock(stamp);
}
}
}
常用API
long | writeLock() | 获取写锁,获取失败会一直阻塞,直到获得锁成功; 返回可以用来解锁或转换模式的戳记(128的整数) |
long | tryWriteLock() | 没有任何锁时则获取写锁,否则返回0; 返回可以用来解锁或转换模式的戳记(128的整数),获取失败返回0 |
long | readLock() | 悲观读锁,非独占锁,为获得锁一直处于阻塞状态,直到获得锁为止 |
long | tryReadLock() | 可以立即获得锁,则获取读锁,否则返回0 |
long | tryOptimisticRead() | 获取乐观读锁,返回邮票stamp |
void | unlockWrite(long stamp) | state匹配stamp则释放写锁, |
void | unlockRead(long stamp) | state匹配stamp则释放读锁, |
| unlock(long stamp) | state匹配stamp时,释放一个读锁或写锁 |
long | tryConvertToWriteLock(long stamp) | state匹配stamp时, 执行下列操作之一: 1、stamp 已经持有写锁,直接返回. 2、读模式,但是没有更多的读取者,并返回一个写锁stamp. 3、有一个乐观读锁,只在即时可用的前提下返回一个写锁stamp 4、其他情况都返回0 |
long | tryConvertToReadLock(long stamp) | state匹配stamp时, 执行下列操作之一: 1、stamp 表示持有写锁,释放写锁,并持有读锁 2 stamp 表示持有读锁 ,返回该读锁 3 有一个乐观读锁,只在即时可用的前提下返回一个读锁stamp 4、其他情况都返回0,表示失败 |
long | tryConvertToOptimisticRead(long stamp) | |
boolean | tryUnlockWrite() | 如果持有写锁,释放写锁。该方法可以对发生错误后的恢复 |
boolean | tryUnlockRead() | 如果持有读锁,释放一个读锁。该方法可以对发生错误后的恢复 |