JUC之 ReentrantLock、ReentrantReadWriteLock、StampedLock

面试题

  1. 你知道Java里面有哪些锁?
  2. 你说你用过读写锁,锁饥饿问题是什么?
  3. 有没有比读写锁更快的锁?
  4. StampedLock知道吗?(邮戳锁/票据锁)
  5. ReentrantReadWriteLock有锁降级机制策略你知道吗?

ReentrantReadWriteLock(读写锁)

  • 一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。
  • 读写锁ReentrantReadWriteLock并不是真正意义上的读写分离,它只允许读读共存,而读写和写写依然是互斥的,大多实际场景是“读/读”线程间并不存在互斥关系,只有"读/写"线程或"写/写"线程间的操作需要互斥的。因此引入ReentrantReadWriteLock。
  • 一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁。
  • 只有在读多写少情境之下,读写锁才具有较高的性能体现。

特点

  1. 可重入
  2. 读写分离

从写锁→读锁,ReentrantReadWriteLock可以降级

锁的严苛程度变强叫做升级,反之叫做降级
JUC之 ReentrantLock、ReentrantReadWriteLock、StampedLock_第1张图片

锁降级

遵循获取写锁→再获取读锁→再释放写锁的次序,写锁能够降级成为读锁。如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。
JUC之 ReentrantLock、ReentrantReadWriteLock、StampedLock_第2张图片

  • 通过获取写入锁定,然后读取锁然后释放写锁从写锁到读取锁, 但是,从读锁定升级到写锁是不可能的
  • 写锁和读锁是互斥的(这里的互斥是指线程间的互斥,当前线程可以获取到写锁又获取到读锁,但是获取到了读锁不能继续获取写锁),这是因为读写锁要保持写操作的可见性。因为,如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作。
锁降级的好处

锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性

  1. 代码中声明了一个volatile类型的cacheValid变量,保证其可见性。
  2. 首先获取读锁,如果cache不可用,则释放读锁,获取写锁,在更改数据之前,再检查一次cacheValid的值,然后修改数据,将cacheValid置为true,然后在释放写锁前获取读锁;此时,cache中数据可用,处理cache中数据,最后释放读锁。这个过程就是一个完整的锁降级的过程,目的是保证数据可见性。
  3. 如果违背锁降级的步骤 如果当前的线程C在修改完cache中的数据后,没有获取读锁而是直接释放了写锁,那么假设此时另一个线程D获取了写锁并修改了数据,那么C线程无法感知到数据已被修改,则数据出现错误。
  4. 如果遵循锁降级的步骤 线程C在释放写锁之前获取读锁,那么线程D在获取写锁时将被阻塞,直到线程C完成数据处理过程,释放读锁。这样可以保证返回的数据是这次更新的数据,该机制是专门为了缓存设计的。

邮戳锁StampedLock

StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化。邮戳锁 也叫 票据锁
stamp(戳记,long类型)代表了锁的状态。当stamp返回零时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,都要传入最初获取的stamp值。

它是由锁饥饿问题引出

ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,假如当前1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那1个写线程就悲剧了 因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写;

如何解决

使用公平锁,代价是牺牲性能;

StampedLock类的乐观读锁

  • ReentrantReadWriteLock允许多个线程同时读,但是只允许一个线程写,在线程获取到写锁的时候,其他写操作和读操作都会处于阻塞状态,读锁和写锁也是互斥的,所以在读的时候是不允许写的,读写锁比传统的synchronized速度要快很多,原因就是在于ReentrantReadWriteLock支持读并发;
  • ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,所以,在获取乐观读锁后,还需要对结果进行校验。

StampedLock的特点

  1. 所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为零表示获取失败,其余都表示成功;
  2. 所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
  3. StampedLock是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)

三种访问模式

  1. Reading(读模式):功能和ReentrantReadWriteLock的读锁类似
  2. Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
  3. Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式

StampedLock的缺点

  1. StampedLock 不支持重入,没有Re开头
  2. StampedLock 的悲观读锁和写锁都不支持条件变量(Condition),这个也需要注意。
  3. 使用 StampedLock一定不要调用中断操作,即不要调用interrupt() 方法

如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly()和写锁writeLockInterruptibly()

你可能感兴趣的:(JUC,JUC,java,性能优化,锁,读写锁,邮戳锁)