Lock: 读写锁ReentrantReadWriteLock

之前提到的ReentrantLock是排他锁,也就是同一时刻只允许一个线程进行访问。而读写锁在同一时刻可以允许多个读线程进行访问,但是在写线程访问时,所有的读线程和写线程均会被阻塞。

读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大的提升。

读写锁保证写操作对读操作的可见性以及并发性的提升,还简化了读写交互场景的编程方式。
在没有读写锁支持的时候(JDK 1.5之前),如果需要完成写操作对读操作的可见性,就要使用Java的等待/通知机制,当开始写操作时,后续的所有读操作进入等待状态,当写操作完成后并进行通知,所有等待的读操作才能继续执行(写操作之间依靠synchronized关键字进行同步),这样才能保证读操作读取到正确的数据,不会出现脏读。

读写锁ReentrantReadWriteLock的特性

Java并发包提供读写锁的实现是ReentrantReadWriteLock,它的特性如下:

  • 公平性选择
    支持非公平(默认的)和公平的锁获取方式。
  • 重进入
    该锁支持重进入:读线程在获取了读锁后,能够再次获取读锁。写线程在获取了写锁之后能够再次获取写锁和读锁。
  • 锁降级
    遵循获取写锁、获取读锁再释放写锁的顺序。写锁降级为读锁。

读写锁ReentrantReadWriteLock实现分析

读写状态的设计

读写锁同样依赖于自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。
读写锁需要在同步状态(一个整型变量)上维护多种状态,需要“按位切割使用”这个变量,将变量切分成两个部分,高16位表示读,低16位表示写。

读写状态的确定:位运算。
假设当前同步状态为S,写状态等于S & 0x0000FFFF(将高16位抹去),读状态等于S >>> 16(无符号补0右移16位)。写状态增加1,等于S + 1;读状态增加1,等于S + (1<<16)

写锁的获取与释放

写锁是一个支持重进入排他锁
如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。

如果读锁存在,则写锁不能被获取,原因:读写锁要确保写锁的操作对于读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知当前写线程的操作。

写锁的释放与ReentrantLock的释放过程类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放。

读锁的获取与释放

读锁是一个支持重进入共享锁
在没有其他写线程访问时,读锁总会被成功地获取,而所做的也只是(线程安全地,依靠CAS保证)增加读状态。

锁降级

锁降级指的是写锁降级为读锁

整个过程是:1)把持住当前拥有的写锁,2)再获取读锁,3)然后释放先前拥有的写锁。

锁降级中读锁获取的必要性主要是为了保证数据的可见性。如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程T获取了写锁并进行了修改数据,那么当前线程无法感知线程T对数据的更新(当前线程没有被阻塞住,导致此刻的数据更新无法感知)。如果当前线程遵循锁降级的步骤,则线程T会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。