ReentrantReadWriteLock

  • ReentrantReadWriteLock实现了ReadWriteLock接口。ReadWriteLock是一个读写锁的接口,提供了"获取读锁的readLock()函数" 和 "获取写锁的writeLock()函数"
  • ReentrantReadWriteLock中包含:sync对象,读锁readerLock和写锁writerLock。读锁ReadLock和写锁WriteLock都实现了Lock接口。读锁ReadLock和写锁WriteLock中也都分别包含了"Sync对象",它们的Sync对象和ReentrantReadWriteLock的Sync对象 是一样的,就是通过sync,读锁和写锁实现了对同一个对象的访问
  • 和"ReentrantLock"一样,sync是Sync类型;而且,Sync也是一个继承于AQS的抽象类。Sync也包括"公平锁"FairSync和"非公平锁"NonfairSync。sync对象是"FairSync"和"NonfairSync"中的一个,默认是"NonfairSync"

锁降级

  • 锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性
  • 锁降级:遵循获取写锁→再获取读锁→再释放写锁→再释放读锁的次序,写锁能够降级成为读锁
  • 如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁
  • 线程获取读锁是不能直接升级为写入锁的
  • 在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会被阻塞所以,需要释放所有读锁,才可获取写锁

写锁和读锁是互斥的

  • 写锁和读锁是互斥的,这里的互斥是指线程间的互斥,当前线程可以获取到写锁又获取到读锁,但是获取到了读锁不能继续获取写锁,这是因为读写锁要保持写操作的可见性
  • 因为,如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作,因此,分析读写锁ReentrantReadWriteLock,会发现它有个潜在的问题:
    • 读锁全完,写锁有望
    • 写锁独占,读写全堵
  • 如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即ReadWriteLock读的过程中不允许写,只有等待线程都释放了读锁,当前线程才能获取写锁,也就是写入必须等待,这是一种悲观的读锁

步骤

  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实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,使用“公平”策略可以一定程度上缓解这个问题,但是“公平”策略是以牺牲系统吞吐量为代价的
  • ReentrantReadWriteLock 允许多个线程同时读,但是只允许一个线程写,在线程获取到写锁的时候,其他写操作和读操作都会处于阻塞状态,读锁和写锁也是互斥的,所以在读的时候是不允许写的,读写锁比传统的synchronized速度要快很多,原因就是在于ReentrantReadWriteLock支持读并发
  • ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞,但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,所以,在获取乐观读锁后,还需要对结果进行校验

特点

  • 所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为零表示获取失败,其余都表示成功
  • 所有释放锁的方法,都需要一个邮戳(Stamp),这个 Stamp 必须是和成功获取锁时得到的 Stamp 一致
  • StampedLock 是不可重入的,如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁
  • 三种模式
    • Reading:功能和 ReentrantReadWriteLock 的读锁类似
    • Writing:功能和 ReentrantReadWriteLock 的写锁类似
    • Optimistic reading:无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式

缺点

StampedLock 不支持重入,没有Re开头

StampedLock 的悲观读锁和写锁都不支持条件变量(Condition),这个也需要注意

使用 StampedLock一定不要调用中断操作,即不要调用interrupt() 方法