读写锁
ReentrantReadWriteLock(属于悲观锁)
-
一个资源能被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程
-
它只允许读读共存,而读写和写写依然是互斥的,大多数场景是“读/读”线程之前不存在互斥关系,只有“读/写”或“写/写”线程间的操作需要互斥,因此引入
ReentrantReadWriteLock
-
它同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在读锁和写锁(互斥),也即一个资源可以被多个读线程操作或一个写线程操作,不能同时存在
-
只有在读多写少的情景下,读写锁才能有较高的性能体现
ReentrantReadWriteLock锁降级
-
遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级为读锁
-
如果同一个线程持有了写锁,在没释放写锁的情况下,它还可以获取读锁。这就是写锁的降级,降级成为了读锁
-
写锁可以降级为读锁(获得读锁 -> 获得写锁 -> 释放写锁 -> 释放读锁 或者 获得读锁 ->释放读锁 -> 获得读锁 ->释放写锁)
public class LockDownGradingDemo { public static void main(String[] args) { ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock(); ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock(); writeLock.lock(); System.out.println("写入----"); readLock.lock(); System.out.println("读取------"); writeLock.unlock(); readLock.unlock(); } }
StampedLock(属于乐观锁,也叫邮戳锁、票据锁)
- stamp代表锁的状态,当stamp返回0时,表示线程获取锁失败,并且,当释放锁或者锁转换的时候,都要传入最初获取的stamp值
- ReentrantReadWriteLock的读锁被占用时,其他线程获取写锁也会阻塞,但是StampedLock采取乐观锁后,其它线程尝试获取写锁时不会产生阻塞,这其实是对锁的优化,但是在获取乐观读锁后,还需要对结果进行校验
特点
-
所有获取锁的方法,都返回一个邮戳(stamp),stamp为0时,表示获取锁失败,其余全部都成功
-
所有释放锁的方法,都需要一个邮戳,这个stamp必须和成功获取锁时得到的stamp一致
-
StampedLock是不可重入的,如果一个线程已经持有了写锁,再去获取写锁的话,会造成死锁
-
public class StampLockDemo { static int number = 8; static StampedLock stampedLock = new StampedLock(); public void write() { long stamp = stampedLock.writeLock(); System.out.println(Thread.currentThread().getName() + "写线程准备写入"); try { number = number + 5; } finally { stampedLock.unlockWrite(stamp); } System.out.println(Thread.currentThread().getName() + "写线程结束写入"); } //传统悲观读 public void read() { long stamp = stampedLock.readLock(); System.out.println(Thread.currentThread().getName() + " come in readLock code block"); try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } try { int result = number; System.out.println(Thread.currentThread().getName() + "获得成员变量值" + result); System.out.println("写线程没有修改成功,因为读锁时候写锁无法接入,传统的读写互斥"); } finally { stampedLock.unlockRead(stamp); } } //乐观读(读的过程允许写锁进入进行修改) public void tryOptimisticRead() { long stamp = stampedLock.tryOptimisticRead(); int result = number; //故意间隔4秒很乐观认为读取中认为没有其他线程修改过number值,具体靠boolean判断 System.out.println("4秒前stampedLock.validate方法(true值代表无修改,false代表被修改)" + "\t" + stampedLock.validate(stamp)); try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("4秒后stampedLock.validate方法(true值代表无修改,false代表被修改)" + "\t" + stampedLock.validate(stamp)); if (!stampedLock.validate(stamp)) { System.out.println("有人修改过------------"); stamp = stampedLock.readLock(); try { System.out.println("从乐观锁变为悲观锁"); result = number; System.out.println("重新悲观读后result:" + result); } finally { stampedLock.unlockRead(stamp); } } System.out.println(Thread.currentThread().getName() + "\t" + "finally result" + result); } public static void main(String[] args) { StampLockDemo resource = new StampLockDemo(); /*new Thread(() -> { resource.read(); }, "t1").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { resource.write(); }, "t2").start();*/ new Thread(() -> { resource.tryOptimisticRead(); }, "readThread").start(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { resource.write(); }, "writeThread").start(); } } //输出结果 4秒前stampedLock.validate方法(true值代表无修改,false代表被修改) true writeThread写线程准备写入 writeThread写线程结束写入 4秒后stampedLock.validate方法(true值代表无修改,false代表被修改) false 有人修改过------------ 从乐观锁变为悲观锁 重新悲观读后result:13 readThread finally result13
缺点
- 不支持锁重入
- StampedLock的悲观锁和写锁都不支持条件变量
- 使用该锁不要中断操作,即不要调用interrupt方法
锁饥饿问题
- ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了。假如,当前有1000个线程,999个在读,1个写,有可能999个读取线程长时间抢到了锁,那这1个写的线程就悲剧了,因为当前有可能一直存在读锁,而无法获取写锁,根本没机会写