java8新特性回顾(五)---并发增强之stampedLock
2017-11-07 22:58
218 查看
锁分离思想和饥饿现象:
ReentrantReadWriteLock 在沒有任何读写锁时,才可以取得写入锁,这可用于实现了悲观读取(Pessimistic
Reading),即如果执行中进行读取时,经常可能有另一执行要写入的需求,为了保持同步,ReentrantReadWriteLock 的读取锁定就可派上用场。
然而,如果读取执行情况很多,写入很少的情况下,使用
ReentrantReadWriteLock 可能会使写入线程遭遇饥饿(Starvation)问题,也就是写入线程迟迟无法竞争到锁定而一直处于等待状态。
java8中引入stampedLock来改进读写锁的写饥饿现象,它的思想是读写锁中读不仅不阻塞读,同时也不应该阻塞写。
读不阻塞写:在读的时候如果发生了写,则应当重读而不是在读的时候直接阻塞写。即读写之间不会阻塞对方,当然写与写之间仍然要阻塞。
StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。
StampedLock的实现思想
在StampedLock中使用了CLH自旋锁,如果发生了读失败,不立刻把读线程挂起,锁当中维护了一个等待线程队列。
所有申请锁但是没有成功的线程都会记录到这个队列中,每一个节点(一个节点表示一个线程)保存一个标记位(locked),
用于判断当前线程是否已经释放锁。当一个未标记到队列中的线程试图获得锁时,会取得当前等待队列尾部的节点作为其前序节点,
并使用类似如下代码(一个空的死循环)判断前序节点是否已经成功的释放了锁:
while(pred.locked){ }
解释:pred表示当前试图获取锁的线程的前序节点,如果前序节点没有释放锁,则当前线程就执行该空循环并不断判断前序节点的锁释放,
即类似一个自旋锁的效果,避免被系统挂起。当循环一定次数后,前序节点还没有释放锁,则当前线程就被挂起而不再自旋,
因为空的死循环执行太多次比挂起更消耗资源。
java doc给出下列一个例子:
public class
Point {
//一个点的x,y坐标
private double
x,y;
/**Stamped类似一个时间戳的作用,每次写的时候对其+1来改变被操作对象的Stamped值
* 这样其它线程读的时候发现目标对象的Stamped改变,则执行重读*/
private final
StampedLock stampedLock = new
StampedLock();
// an exclusively locked method
void move(doubledeltaX,doubledeltaY)
{
/**stampedLock调用writeLock和unlockWrite时候都会导致stampedLock的stamp值的变化
* 即每次+1,直到加到最大值,然后从0重新开始 */
longstamp =stampedLock.writeLock(); //写锁
try {
x +=deltaX;
y +=deltaY;
} finally {
stampedLock.unlockWrite(stamp);//释放写锁
}
}
double distanceFromOrigin()
{ // A read-only method
/**tryOptimisticRead是一个乐观的读,使用这种锁的读不阻塞写
* 每次读的时候得到一个当前的stamp值(类似时间戳的作用)*/
longstamp =stampedLock.tryOptimisticRead();
//这里就是读操作,读取x和y,因为读取x时,y可能被写了新的值,所以下面需要判断
double currentX =x,
currentY =y;
/**如果读取的时候发生了写,则stampedLock的stamp属性值会变化,此时需要重读,
* 再重读的时候需要加读锁(并且重读时使用的应当是悲观的读锁,即阻塞写的读锁)
* 当然重读的时候还可以使用tryOptimisticRead,此时需要结合循环了,即类似CAS方式
* 读锁又重新返回一个stampe值*/
if (!stampedLock.validate(stamp))
{
stamp =stampedLock.readLock(); //读锁
try {
currentX =x;
currentY =y;
}finally{
stampedLock.unlockRead(stamp);//释放读锁
}
}
//读锁验证成功后才执行计算,即读的时候没有发生写
return Math.sqrt(currentX *currentX + currentY *currentY);
}
}
小结:
StampedLock要比ReentrantReadWriteLock更加廉价,也就是消耗比较小。
下图是六个线程情况下,读性能是其几十倍,写性能也是近10倍左右:
下图是吞吐量提高:
2、ReentrantLock、ReentrantReadWriteLock,、StampedLock都是对象层面的锁定,要保证锁定一定会被释放,就必须将unLock()放到finally{}中;
3、StampedLock 对吞吐量有巨大的改进,特别是在读线程越来越多的场景下;
4、StampedLock有一个复杂的API,对于加锁操作,很容易误用其他方法;
5、当只有少量竞争者的时候,synchronized是一个很好的通用的锁实现;
6、当线程增长能够预估,ReentrantLock是一个很好的通用的锁实现;
StampedLock 可以说是Lock的一个很好的补充,吞吐量以及性能上的提升足以打动很多人了,但并不是说要替代之前Lock的东西,毕竟他还是有些应用场景的,起码API比StampedLock容易入手。
ReentrantReadWriteLock 在沒有任何读写锁时,才可以取得写入锁,这可用于实现了悲观读取(Pessimistic
Reading),即如果执行中进行读取时,经常可能有另一执行要写入的需求,为了保持同步,ReentrantReadWriteLock 的读取锁定就可派上用场。
然而,如果读取执行情况很多,写入很少的情况下,使用
ReentrantReadWriteLock 可能会使写入线程遭遇饥饿(Starvation)问题,也就是写入线程迟迟无法竞争到锁定而一直处于等待状态。
java8中引入stampedLock来改进读写锁的写饥饿现象,它的思想是读写锁中读不仅不阻塞读,同时也不应该阻塞写。
读不阻塞写:在读的时候如果发生了写,则应当重读而不是在读的时候直接阻塞写。即读写之间不会阻塞对方,当然写与写之间仍然要阻塞。
StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。
StampedLock的实现思想
在StampedLock中使用了CLH自旋锁,如果发生了读失败,不立刻把读线程挂起,锁当中维护了一个等待线程队列。
所有申请锁但是没有成功的线程都会记录到这个队列中,每一个节点(一个节点表示一个线程)保存一个标记位(locked),
用于判断当前线程是否已经释放锁。当一个未标记到队列中的线程试图获得锁时,会取得当前等待队列尾部的节点作为其前序节点,
并使用类似如下代码(一个空的死循环)判断前序节点是否已经成功的释放了锁:
while(pred.locked){ }
解释:pred表示当前试图获取锁的线程的前序节点,如果前序节点没有释放锁,则当前线程就执行该空循环并不断判断前序节点的锁释放,
即类似一个自旋锁的效果,避免被系统挂起。当循环一定次数后,前序节点还没有释放锁,则当前线程就被挂起而不再自旋,
因为空的死循环执行太多次比挂起更消耗资源。
java doc给出下列一个例子:
public class
Point {
//一个点的x,y坐标
private double
x,y;
/**Stamped类似一个时间戳的作用,每次写的时候对其+1来改变被操作对象的Stamped值
* 这样其它线程读的时候发现目标对象的Stamped改变,则执行重读*/
private final
StampedLock stampedLock = new
StampedLock();
// an exclusively locked method
void move(doubledeltaX,doubledeltaY)
{
/**stampedLock调用writeLock和unlockWrite时候都会导致stampedLock的stamp值的变化
* 即每次+1,直到加到最大值,然后从0重新开始 */
longstamp =stampedLock.writeLock(); //写锁
try {
x +=deltaX;
y +=deltaY;
} finally {
stampedLock.unlockWrite(stamp);//释放写锁
}
}
double distanceFromOrigin()
{ // A read-only method
/**tryOptimisticRead是一个乐观的读,使用这种锁的读不阻塞写
* 每次读的时候得到一个当前的stamp值(类似时间戳的作用)*/
longstamp =stampedLock.tryOptimisticRead();
//这里就是读操作,读取x和y,因为读取x时,y可能被写了新的值,所以下面需要判断
double currentX =x,
currentY =y;
/**如果读取的时候发生了写,则stampedLock的stamp属性值会变化,此时需要重读,
* 再重读的时候需要加读锁(并且重读时使用的应当是悲观的读锁,即阻塞写的读锁)
* 当然重读的时候还可以使用tryOptimisticRead,此时需要结合循环了,即类似CAS方式
* 读锁又重新返回一个stampe值*/
if (!stampedLock.validate(stamp))
{
stamp =stampedLock.readLock(); //读锁
try {
currentX =x;
currentY =y;
}finally{
stampedLock.unlockRead(stamp);//释放读锁
}
}
//读锁验证成功后才执行计算,即读的时候没有发生写
return Math.sqrt(currentX *currentX + currentY *currentY);
}
}
小结:
StampedLock要比ReentrantReadWriteLock更加廉价,也就是消耗比较小。
StampedLock与ReadWriteLock性能对比
下图是和ReadWritLock相比,在一个线程情况下,是读速度其4倍左右,写是1倍。下图是六个线程情况下,读性能是其几十倍,写性能也是近10倍左右:
下图是吞吐量提高:
总结
1、synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定;2、ReentrantLock、ReentrantReadWriteLock,、StampedLock都是对象层面的锁定,要保证锁定一定会被释放,就必须将unLock()放到finally{}中;
3、StampedLock 对吞吐量有巨大的改进,特别是在读线程越来越多的场景下;
4、StampedLock有一个复杂的API,对于加锁操作,很容易误用其他方法;
5、当只有少量竞争者的时候,synchronized是一个很好的通用的锁实现;
6、当线程增长能够预估,ReentrantLock是一个很好的通用的锁实现;
StampedLock 可以说是Lock的一个很好的补充,吞吐量以及性能上的提升足以打动很多人了,但并不是说要替代之前Lock的东西,毕竟他还是有些应用场景的,起码API比StampedLock容易入手。
相关文章推荐
- java8新特性回顾(四)---并发增强之Striped64和longAdder
- 【Java并发编程】之二十三:并发新特性—信号量Semaphore(含代码)
- JAVA 8 并发增强 (2)乐观读
- java8新特性-接口定义增强
- 黑马程序员————java基础--------JDK1.5新特性之泛型、自动拆装箱、增强for、静态导入、可变参数
- Java并发14:并发三特性-可见性定义、可见性问题与可见性保证技术
- 【Java并发编程】:并发新特性—信号量Semaphore
- 黑马程序员<Java 1.5新特性>静态导入、增强for、可变参数、自动装箱拆箱、枚举
- Java5.0新特性03-增强for循环
- 【Java多线程与并发库】2.传统定时器技术回顾
- JavaSE 拾遗(15)——JavaSE 高新技术基础增强...java5简单新特性和枚举
- Java多线程与并发库高级应用-传统定时器技术回顾
- 关于Java构造函数和继续特性的回顾
- 【Java并发编程】并发新特性—阻塞队列和阻塞栈(含代码)
- Java学习总结(一)—— >Java环境变量的配置,DOS窗口的带包编译,Jdk1.5的两个新特性(可变参数和增强的for循环)
- Java5新特性之静态导入、可变参数、增强for循环、自动拆装箱
- 【Java并发编程】之十九:并发新特性—Executor框架与线程池(含代码)
- 【Java并发编程】之十九:并发新特性—Executor框架与线程池(含代码)
- java8新特性(九):CompletableFuture多线程并发异步编程
- JAVA JDK1.5新特性<静态带入、可变参数、增强For循环、自动装箱及自动拆箱>