0%

StampedLock:有没有比读写锁更快的锁

读写锁允许多个线程同时读共享变量,适用于读多写少的场景。Java 1.8版本提供了StampedLock的锁,性能比读写锁更好。

StampedLock支持的三种锁模式

ReadWriteLock支持两种模式:读锁、写锁
StampedeLock支持三种模式:写锁、悲观读锁、乐观读。其中写锁、悲观读锁的语义和ReadWriteLock的写锁、读锁的语义非常类似,允许多个线程同时获取悲观读锁,但是只允许一个线程获取写锁,写锁和悲观读锁是互斥的。不同的是,StampedLock里的写锁和悲观读锁加锁成功之后,都会返回一个stamp;解锁的时候,需要传入这个stamp。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
final StampedLock sl = new StampedLock();

//获取&释放悲观读锁
long rstamp = sl.readLock();
try {
//...
} finally {
sl.unlockRead(rstamp);
}

//获取&释放写锁
long wstamp = sl.writeLock();
try {
//...
} finally {
sl.unlockWrite(wstamp);
}

乐观读

StampedLock的性能之所以比ReadWriteLock还要好,关键是StampedeLock支持乐观读的方式。ReadWriteLock支持多个线程同时读,但是当多个线程同时读的时候,所有的写操作会被阻塞;而StampedLock提供的乐观读,是允许一个线程获取写锁的,不是所有的写操作都被阻塞。

乐观读不是乐观读锁,乐观读这个操作是无锁的。相比较ReadWriteLock的读锁,乐观读的性能更好一些。

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
class Point {
private int x, y;
final StampedLock sl = new StampedLpck();

//计算到原点的距离
int distanceFromOrigin() {
//乐观读
long stamp = sl.tryOptimisticRead();
//将共享变量xy读入局部变量中,读的过程数据可能被修改
int curX = x, curY = y;
//判断执行读操作期间,是否存在写操作validate(stamp),如果存在,则sl.validate返回false
if(!sl.validate(stamp)) {
//升级为悲观读锁
stamp = sl.readLock();
try {
curX = x;
curY = y;
} finally {
//释放悲观读锁
sl.unlockRead(stamp);
}
}
return Math.sqrt(curX * curX + curY * curY);
}
}

如果执行乐观读操作期间,存在写操作,会把乐观读升级为悲观读锁。否则需要在一个循环里反复执行乐观读,直到执行乐观读操作的期间没有写操作(只有这样才能保证x和y的正确性和一致性),而循环读会浪费大量的CPU。升级为悲观读锁,代码简练且不易出错。

乐观读只保证,读的时候数据是正确的一致的,读后用前数据可能发生了变化。(强制用前一致只能使用互斥锁)

进一步理解乐观读

StampedLock的乐观读和数据库的乐观锁有异曲同工之妙。

在ERP生产模块里,会有多个人通过ERP系统提供的UI同时修改同一条生产订单,在生产订单的表product_doc里增加一个数值版本号字段version,每次更新product_doc这个表的时候,都将version字段加1。生产订单的UI在展示的时候,需要查询数据库,此时将这个version字段和其它业务字段一起返回给生产订单UI。

select id, …, version from product_doc where id = 777;

用户在生产订单UI执行保存操作的时候,后台利用下面的SQL语句更新生产订单。

update product_doc set version = version+1, … where id = 777 and version = 9;

如果这条SQL语句执行成功并且返回的条数等于1,则说明生产订单UI执行查询操作到执行保存操作期间,没有其他人修改过数据。如果这期间其他人修改过这条数据,那么版本号字段一定大于9。

数据库中的乐观锁,查询的时候需要把version字段查出来,更新的时候要利用version字段做验证。这个version字段就类似于StampedLock里面的stamp。

StampedLock使用注意事项

对于读多写少的场景StampedLock性能很好,简单的应用场景基本上可以替代ReadWriteLock,但是StampedLock的功能仅仅是ReadWriteLock的子集。

  • StampedLock在命名上并没有增加Reentrant,StampedLock不支持重入
  • StampedLock的悲观读锁、写锁都不支持条件变量

    ReadWriteLock:只有写锁支持条件变量,读锁不支持条件变量。Semaphore不支持条件概念,只唤醒一个线程。

  • StampedLock支持锁的降级(通过tryConvertToReadLock()方法实现)和升级(通过tryConvertToWriteLock()方法实现)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    private double x, y;
    final StampedLock sl = new StampedLock();
    void moveIfAtOrigin(double newX, double newY) {
    long stamp = sl.readLock();
    try {
    while(x == 0.0 && y == 0.0) {
    long ws = sl.tryConvertToWriteLock(stamp);
    //tryConvertToWriteLock(stamp)这个方法内部会释放悲观读锁stamp(升级成功的话)
    if(ws != 0L) {
    x = newX;
    y = newY;
    stamp = ws;
    break;
    } else {
    sl.unlockRead(stamp);
    stamp = sl.writeLock();
    }
    }
    } finally {
    sl.unlock(stamp);
    }
    }
  • 使用StampedLock一定不要调用中断操作,如果需要支持中断功能,一定使用可中断的悲观读锁readLockInterruptibly()和写锁writeLockInterruptibly()

    如果线程阻塞在StampedLock的readLock()或者writeLock()上时,此时调用该阻塞线程的interrupt()方法,会导致CPU飙升。(内部实现里while循环里面对中断的处理有问题)

    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
    //线程T1获取写锁之后将自己阻塞,线程T2尝试获取悲观读锁,也会阻塞;  
    //如果此时调用线程T2的interrupt()方法来中断线程T2,则线程T2所在CPU会飙升到100%
    final StampedLock lock = new StampedLock();
    Thread T1 = new Thread(() -> {
    //获取写锁
    lock.writeLock();
    //永远阻塞在此处,不释放写锁
    LockSupport.park();
    });
    T1.start();

    //保证T1获取写锁
    Thread.sleep(100);

    Thread T2 = new Thread(() ->
    //阻塞在悲观读锁
    lock.readLock();
    );
    T2.start();

    //保证T2阻塞在读锁
    Thread.sleep(100);

    //中断线程T2,会导致线程T2所在CPU飙升
    T2.interrupt();
    T2.join();

总结

StampedLock最佳实践。

StampedLock读模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
final StampedLock sl = new StampedLock();

//乐观读
long stamp = sl.tryOptimisticRead();
//读入方法局部变量
...
//校验stamp
if(!sl.validate(stamp)) {
//升级为悲观读锁
stamp = sl.readLock();
try {
//读入到方法局部变量
...
} finally {
//释放悲观读锁
sl.unlockRead(stamp);
}
}
//使用方法局部变量执行业务操作
...

StampedLock写模板:

1
2
3
4
5
6
7
long stamp = sl.writeLock();
try {
//写共享变量
...
} finally {
sl.unlockWrite(stamp);
}