StampedLock是并发包里面jdk8版本新增的一个锁,该锁提供了三种模式的读写控制,三种模式分别以下:数据库
下面经过JDK8注释里面的一个例子讲解来加深对上面讲解的理解。编程
class Point { // 成员变量 private double x, y; // 锁实例 private final StampedLock sl = new StampedLock(); // 排它锁-写锁(writeLock) void move(double deltaX, double deltaY) { long stamp = sl.writeLock(); try { x += deltaX; y += deltaY; } finally { sl.unlockWrite(stamp); } } // 乐观读锁(tryOptimisticRead) double distanceFromOrigin() { // 尝试获取乐观读锁(1) long stamp = sl.tryOptimisticRead(); // 将所有变量拷贝到方法体栈内(2) double currentX = x, currentY = y; // 检查在(1)获取到读锁票据后,锁有没被其余写线程排它性抢占(3) if (!sl.validate(stamp)) { // 若是被抢占则获取一个共享读锁(悲观获取)(4) stamp = sl.readLock(); try { // 将所有变量拷贝到方法体栈内(5) currentX = x; currentY = y; } finally { // 释放共享读锁(6) sl.unlockRead(stamp); } } // 返回计算结果(7) return Math.sqrt(currentX * currentX + currentY * currentY); } // 使用悲观锁获取读锁,并尝试转换为写锁 void moveIfAtOrigin(double newX, double newY) { // 这里可使用乐观读锁替换(1) long stamp = sl.readLock(); try { // 若是当前点在原点则移动(2) while (x == 0.0 && y == 0.0) { // 尝试将获取的读锁升级为写锁(3) long ws = sl.tryConvertToWriteLock(stamp); // 升级成功,则更新票据,并设置坐标值,而后退出循环(4) if (ws != 0L) { stamp = ws; x = newX; y = newY; break; } else { // 读锁升级写锁失败则释放读锁,显示获取独占写锁,而后循环重试(5) sl.unlockRead(stamp); stamp = sl.writeLock(); } } } finally { // 释放锁(6) sl.unlock(stamp); } } }
如上代码Point类里面有两个成员变量,和三个操做成员变量的方法,另外实例化了一个StampedLock对象用来保证操做的原子性。多线程
首先分析下move方法,该函数做用是在添加增量,改变当前point坐标的位置,代码先获取到了写锁,而后对point坐标进行修改,而后释放锁。该锁是排它锁,这保证了其余线程调用move函数时候会被阻塞,直到当前线程显示释放了该锁,也就是保证了对变量x,y操做的原子性。并发
而后看下distanceFromOrigin方法,该方法做用是计算当前位置到原点的距离,代码(1)首先尝试获取乐观读锁,若是当前没有其它线程获取到了写锁,那么(1)会返回一个非0的stamp用来表示版本信息,代码(2)拷贝变量到本地方法栈里面,代码(3)检查在(1)获取到的票据是否还有效,之因此还要在此校验是由于代码(1)获取读锁时候并无经过CAS操做修改锁的状态而是简单的经过与或操做返回了一个版本信息,这里校验是看在在获取版本信息到如今的时间段里面是否有其余线程持有了写锁,若是有则以前获取的版本信息就无效了。这里若是校验成功则执行(7)使用本地方法栈里面的值进行计算而后返回。须要注意的是在代码(3)校验成功后,代码(7)计算中其余线程可能获取到了写锁而且修改了x,y的值,而当前线程执行代码(7)进行计算时候采用的才是对修改前值的拷贝,也就是操做的值是对以前值的一个拷贝,并非新的值。另外还有个问题,代码(2)和(3)可否互换,答案是不能,假设位置换了,那么首先执行validate,假如验证经过了,要拷贝x,y值到本地方法栈,而在拷贝的过程当中颇有可能其余线程已经修改了x,y中的一个,这就形成了数据的不一致性了。那么你可能会问,那不交换(2)和(3)时候在拷贝x,y值到本地方法栈里面时候也会存在其余线程修改了x,y中的一个值那,这个确实会存在,可是,别忘了拷贝后还有一道validate,若是这时候有线程修改了x,y中的值,那么确定是有线程在调用validate前sl.tryOptimisticRead后获取了写锁,那么validate时候就会失败。如今应该明白了吧,这也是乐观读设计的精妙之处也是使用时候容易出问题的地方。下面继续分析validate失败后会执行代码(4)获取悲观读锁,若是这时候骑行线程持有写锁则代码(4)会致使的当前线程阻塞直到其它线程释放了写锁。获取到读锁后,代码(5)拷贝变量到本地方法栈,而后就是代码(6)释放了锁,拷贝的时候因为加了读锁在拷贝期间其它线程获取写锁时候会被阻塞,这保证了数据的一致性。最后代码(7)使用方法栈里面数据计算返回,同理这里在计算时候使用的数据也可能不是最新的,其它写线程可能已经修改过原来的x,y值了。函数
最后一个方法moveIfAtOrigin方法做用是若是当前坐标为原点则移动到指定的位置。代码(1)获取悲观读锁,保证其它线程不能获取写锁修改x,y值,而后代码(2)判断若是当前点在原点则更新坐标,代码(3)尝试升级读锁为写锁,这里升级不必定成功,由于多个线程均可以同时获取悲观读锁,当多个线程都执行到(3)时候只有一个能够升级成功,升级成功则返回非0的stamp,否非返回0,这里假设当前线程升级成功,而后执行步骤(4)更新stamp值和坐标值而后退出循环,若是升级失败则执行步骤(5)首先释放读锁而后申请写锁,获取到写锁后在循环从新设置坐标值。最后步骤(6)释放锁。性能
使用乐观读锁仍是很容易犯错误的,必需要当心,必需要保证以下的使用顺序:测试
long stamp = lock.tryOptimisticRead(); //非阻塞获取版本信息 copyVaraibale2ThreadMemory();//拷贝变量到线程本地堆栈 if(!lock.validate(stamp)){ // 校验 long stamp = lock.readLock();//获取读锁 try { copyVaraibale2ThreadMemory();//拷贝变量到线程本地堆栈 } finally { lock.unlock(stamp);//释放悲观锁 } } useThreadMemoryVarables();//使用线程本地堆栈里面的数据进行操做
总结: 相比ReentrantLock读写锁,StampedLock经过提供乐观读锁在多线程多写线程少的状况下提供更好的性能,由于乐观读锁不须要进行CAS设置锁的状态而只是简单的测试状态。更具体测试数据期待Java并发编程基础之并发包源码剖析一书的出版。线程