读写锁、邮戳锁
创始人
2024-06-02 15:01:39

锁的演进过程

无锁->独占锁->读写锁->邮戳锁

独占锁

ReentrantLock、synchronized:有序,数据一致性,每次只能来一个线程,不管什么操作

缺点:读操作效率低

ReentrantReadWriteLock

读写互斥,读读可以共享,提升大面积的共享性能,同时多人读。

ReentrantReadWriteLock 实现了 ReadWriteLock ,是一个可重入的读写锁,既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。

读写锁进行并发控制的规则:读读不互斥、读写互斥、写写互斥(只有读读不互斥)

缺点:由于读写互斥,所以容易导致写线程饥饿(大多数情况是读多写少,读的过程中,如果没有释放,写线程不可以获得锁;必须读完后,才能有机会写)

可以锁降级
遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。

锁降级的目的:锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性(写后立即可以读,把写锁降为读锁,保证其他写进程无法进来写,没有读完,写不进去)

不能锁升级
当读锁被使用时,如果有线程尝试获取写锁,该写线程会被阻塞。所以,需要释放所有读锁,才可获取写锁。还可能会有死锁问题发生。举个例子:假设两个线程的读锁都想升级写锁,则需要对方都释放自己锁,而双方都不释放,就会产生死锁。

总结
一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁。也即一个资源可以被多个读操作访问或一个写操作访问,但两者不能同时进行。
只有在读多写少情境之下,读写锁才具有较高的性能体现。

StampedLock

比读写锁更快的锁,解决了读写锁的饥饿问题

stamp (戳记,long类型) 代表了锁的状态。当stamp返回零时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,都要传入最初获取的stamp值。

  • 所有获取锁的方法,都返回一个邮戳(Stamp) ,Stamp为零表示获取失败,其余都表示成功

  • 所有释放锁的方法,都需要一个邮戳(Stamp) ,这个Stamp必须是和成功获取锁时得到的Stamp一致。

  • StampedLock是不可重入的,(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)

StampedLock 的性能之所以比 ReadWriteLock 还要好,其关键是 StampedLock 支持乐观读的方式。ReadWriteLock 支持多个线程同时读,但是当多个线程同时读的时候,所有的写操作会被阻塞;而 StampedLock 提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有的写操作都被阻塞。注意这里,我们用的是“乐观读”这个词,而不是“乐观读锁”,是要提醒你,乐观读这个操作是无锁的,所以相比较 ReadWriteLock 的读锁,乐观读的性能更好一些。

文中下面这段代码是出自 Java SDK 官方示例,并略做了修改。在 distanceFromOrigin() 这个方法中,首先通过调用 tryOptimisticRead() 获取了一个 stamp,这里的 tryOptimisticRead() 就是我们前面提到的乐观读。之后将共享变量 x 和 y 读入方法的局部变量中,不过需要注意的是,由于 tryOptimisticRead() 是无锁的,所以共享变量 x 和 y 读入方法局部变量时,x 和 y 有可能被其他线程修改了。因此最后读完之后,还需要再次验证一下是否存在写操作,这个验证操作是通过调用 validate(stamp) 来实现的。

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

由于多数情况是读多写少,所以代码中的升级为悲观读锁往往不会执行,所以效率会比可重入读写锁要高的多。

对于读多写少的场景 StampedLock 性能很好,简单的应用场景基本上可以替代 ReadWriteLock,但是 StampedLock 的功能仅仅是 ReadWriteLock 的子集,在使用的时候,还是有几个地方需要注意一下。

  • StampedLock不支持重入( Reentrant)
  • StampedLock的悲观读锁,写锁不支持条件变量
  • 如果线程阻塞在StampedLock的readLock()或writeLock()上时,此时调用该阻塞线程的interrupt()会导致CPU飙升

例如下面的代码中,线程 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 一定不要调用中断操作,如果需要支持中断功能,一定使用可中断的悲观读锁readLockInterruptibly() 和写锁 writeLockInterruptibly()。这个规则一定要记清楚。

非中断锁线程调用interrupt()

个人理解:
正常情况(没有打断)下:线程而执行readLock获取读锁,会执行
上面代码执行readLock时会执行acquireRead(false, 0L)操作,acquireRead内部会执行U.park(false, time)操作,该操作表示当前线程无法阻塞,但是当前线程已经被写锁阻塞,所以会导致线程不断死循环。

    public long readLock() {long s = state, next;  // bypass acquireRead on common uncontended casereturn ((whead == wtail && (s & ABITS) < RFULL &&U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?next : acquireRead(false, 0L));}

相关内容

热门资讯

德龙汇能:股价连续2日异常波动... 德龙汇能公告称,公司股票交易价格连续2个交易日(2026年1月13日、1月14日)收盘价格涨幅累计偏...
*ST张股:预计2025年度归... *ST张股1月14日公告,预计2025年度归属于上市公司股东的净利润亏损45,000万元-55,00...
客流暴涨20%!上海环球美食汇... 夜晚的上海,国际范儿藏在一道道美味佳肴中。正在火热举行的2026上海环球美食汇,让全球风味汇聚申城,...
广期所公告多晶硅期货PS270... 转自:新华财经新华财经北京1月14日电 广期所发布公告,经研究决定,现将多晶硅期货PS2701、碳酸...
中伟股份:1月15日起证券简称... 中伟股份1月14日公告,经董事会审议通过,公司将证券简称由“中伟股份”变更为“中伟新材”,公司名称、...