无锁->独占锁->读写锁->邮戳锁
ReentrantLock、synchronized:有序,数据一致性,每次只能来一个线程,不管什么操作
缺点:读操作效率低
读写互斥,读读可以共享,提升大面积的共享性能,同时多人读。
ReentrantReadWriteLock 实现了 ReadWriteLock ,是一个可重入的读写锁,既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。
读写锁进行并发控制的规则:读读不互斥、读写互斥、写写互斥(只有读读不互斥)
缺点:由于读写互斥,所以容易导致写线程饥饿(大多数情况是读多写少,读的过程中,如果没有释放,写线程不可以获得锁;必须读完后,才能有机会写)
可以锁降级:
遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。
锁降级的目的:锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性(写后立即可以读,把写锁降为读锁,保证其他写进程无法进来写,没有读完,写不进去)
不能锁升级
当读锁被使用时,如果有线程尝试获取写锁,该写线程会被阻塞。所以,需要释放所有读锁,才可获取写锁。还可能会有死锁问题发生。举个例子:假设两个线程的读锁都想升级写锁,则需要对方都释放自己锁,而双方都不释放,就会产生死锁。
总结
一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁。也即一个资源可以被多个读操作访问或一个写操作访问,但两者不能同时进行。
只有在读多写少情境之下,读写锁才具有较高的性能体现。
比读写锁更快的锁,解决了读写锁的饥饿问题
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 的子集,在使用的时候,还是有几个地方需要注意一下。
例如下面的代码中,线程 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()。这个规则一定要记清楚。
个人理解:
正常情况(没有打断)下:线程而执行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));}