Java企业教程系列

Java 8的StampedLock

  没有人喜欢同步的代码,这会降低你的应用的吞吐量等性能指标,最坏的时候会挂起死机,但是即使这样你也没有太多选择。

很多理论和模式来实现多线程同步访问一个资源, 其中最著名常用的是读写锁ReadWriteLock,它是通过堵塞来降低多线程消费一个资源引起的竞争,理论上听起来不错,但是在现实中锁意味着性能慢,特别是有大量写线程的情况下。

Java 8 引入了一个新的读写锁叫StampedLock. 不仅这个锁更快,而且它提供强大的乐观锁API,这意味着你能以一个较低的代价获得一个读锁, 在这段时间希望没有写操作发生,当这段时间完成后,你可以查询一下锁,看是否在刚才这段时间是否有写操作发生?然后你可以决定是否需要再试一次 或升级锁或放弃。

通常我们的同步锁如下代码:

synchronized(this)

// do operation

}

Java 6提供的ReentrantLock代码如下:

rwlock.writeLock().lock();

try {

// do operation

} finally {

rwlock.writeLock().unlock();

}

 ReentrantReadWriteLock, ReentrantLock 和synchronized锁都有相同的内存语义,不管怎么说synchronized代码要更容易书写些。ReentrantLock的代码必须严格按照这种写法,否则就会造成严重的问题。

StampedLock要比ReentrantReadWriteLock更加廉价,也就是消耗比较小。

StampedLock控制锁有三种模式,一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。

下面是StampedLock的代码应用:

public class BankAccountWithStampedLock {

private final StampedLock lock = new StampedLock();

private double balance;

 

public void deposit(double amount) {

long stamp = lock.writeLock();

try {

balance = balance + amount;

} finally {

lock.unlockWrite(stamp);

}

}

 

public double getBalance() {

long stamp = lock.readLock();

try {

return balance;

} finally {

lock.unlockRead(stamp);

}

}

}

如果我们要实现跨字段的不变性访问,比如下面MyPoint 中x 和y两个变量必须同时增加,这就需要排他锁:

public class MyPoint {

private double x, y;

private final StampedLock sl = new StampedLock();

 

// method is modifying x and y, needs exclusive lock

public void move(double deltaX, double deltaY) {

long stamp = sl.writeLock();

try {

x += deltaX;

y += deltaY;

} finally {

sl.unlockWrite(stamp);

}

}

 

对于有条件的状态改变,需要将读锁转为写锁,如下代码:

public void moveIfAt(double oldX, double oldY, double newX, double newY) {

long stamp = sl.readLock(); //获得一个读悲观锁

try {

while (x == oldX && y == oldY) {  //循环,检查当前状态是否符合

long writeStamp = sl.tryConvertToWriteLock(stamp);//将读锁转为写锁

if (writeStamp != 0L) {//这是确认转为写锁是否成功

stamp = writeStamp; //如果成功 替换票据

x = newX; y = newY; //进行状态改变

break;

} else {              //如果不能成功转换为写锁

sl.unlockRead(stamp);//我们显式释放读锁

stamp = sl.writeLock(); //显式直接进行写锁 然后再通过循环再试

}

}

} finally {

sl.unlock(stamp); //释放读锁或写锁

}

}

 

以上是悲观读锁案例,下面看看乐观读锁案例:

public double distanceFromOrigin() {

long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁

double currentX = x, currentY = y; //将两个字段读入本地局部变量

if (!sl.validate(stamp)) {//检查发出乐观读锁后同时是否有其他写锁发生?

stamp = sl.readLock();//如果没有,我们再次获得一个读悲观锁

try {

currentX = x;// 将两个字段读入本地局部变量

currentY = y;

} finally {

sl.unlockRead(stamp); //释放读锁

}

}

return Math.sqrt(currentX * currentX + currentY * currentY);

}

 

性能对比

下图是和ReadWritLock相比,在一个线程情况下,是读速度其4倍左右,写是1倍。

下图是六个线程情况下,读性能是其几十倍,写性能也是近10倍左右:

 

下图是吞吐量提高:

相关:

Java 8 LongAdders

并发主题