Java并发-ReentrantReadWriteLock


  • 写锁是独占的,读锁是共享的:读读可以并发;读写,写读,写写互斥。在读多写少的场景中,读写锁能够提供比排它锁更好的并发性和吞吐量
    • 读锁不支持条件变量
    • 重入时升级不支持:持有读锁的情况下去获取写锁,会导致获取永久等待
    • 重入时支持降级: 持有写锁的情况下可以去获取读锁
  • 线程进入读锁的前提条件
    • 没有其他线程的写锁
    • 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个
  • 线程进入写锁的前提条件
    • 没有其他线程的读锁
    • 没有其他线程的写锁
  • 读写锁有以下三个重要的特性
    • 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
    • 可重入:读锁和写锁都支持线程重入。以读写线程为例:读线程获取读锁后,能够再次获取读锁。写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁
    • 锁降级:遵循获取写锁、再获取读锁最后释放写锁的次序,写锁能够降级成为读锁
  • 锁降级:锁降级指的是写锁降级成为读锁
    • 锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程
    • 如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级
    • 锁降级可以帮助我们拿到当前线程修改后的结果而不被其他线程所破坏,防止更新丢失
  • 锁降级中为什么要获取读锁:主要是为了保证数据的可见性
    • 如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新
    • 如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新
  • RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程):目的也是保证数据可见性
    • 如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的
  • 用一个变量如何维护多种状态:高16为表示读,低16为表示写
    • 通过位运算确定读锁和写锁的状态
      • 写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)。 当写状态加1,等于S+1
      • 读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。当读状态加1,等于 S +(1<<16),也就是 S+0x00010000
    • S不等于0时,当写状态(S&0x0000FFFF)等于0时,读状态(S>>>16)大于0,即读锁已被获取
  • exclusiveCount(int c) 静态方法,获得持有写状态的锁的次数
  • sharedCount(int c) 静态方法,获得持有读状态的锁的线程数量。
  • HoldCounter 计数器:不同于写锁,读锁可以同时被多个线程持有。而每个线程持有的读锁支持重入的特性,所以需要对每个线程持有的读锁的数量单独计数,这就需要用到 HoldCounter 计数器
    • 读锁的内在机制其实就是一个共享锁。一次共享锁的操作就相当于对HoldCounter 计数器的操作。获取共享锁,则该计数器 + 1,释放共享锁,该计数器 - 1。只有当线程获取共享锁后才能对共享锁进行释放、重入操作
    • 通过 ThreadLocalHoldCounter 类,HoldCounter 与线程进行绑定。HoldCounter 是绑定线程的一个计数器,而 ThreadLocalHoldCounter 则是线程绑定的 ThreadLocal
      • HoldCounter是用来记录读锁重入数的对象
      • ThreadLocalHoldCounter是ThreadLocal变量,用来存放不是第一个获取读锁的线程的其他线程的读锁重入数对象
  • 写锁的获取:写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程, 则当前线程进入等待状态

写锁获取
写锁释放
读锁获取
读锁释放


文章作者: 钱不寒
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 钱不寒 !
  目录