Java
Java多线程
Java并发
JUC锁

Java多线程 17 - ReentrantReadWriteLock读写锁

简介:ReadWriteLock是一个接口。ReentrantReadWriteLock是它的实现类,ReentrantReadWriteLock包括子类ReadLock和WriteLock。

1. ReadWriteLock和ReentrantReadWriteLock介绍

ReadWriteLock,顾名思义,是读写锁。它维护了一对相关的锁:读取锁和写入锁,一个用于读取操作,另一个用于写入操作。

  • 读取锁用于只读操作,它是共享锁,能同时被多个线程获取。
  • 写入锁用于写入操作,它是独占锁,写入锁只能被一个线程锁获取。

ReadWriteLock是一个接口。ReentrantReadWriteLock是它的实现类,ReentrantReadWriteLock包括子类ReadLock和WriteLock。

ReadWriteLock函数列表如下:

  • // 返回用于读取操作的锁。
  • Lock readLock()
  • // 返回用于写入操作的锁。
  • Lock writeLock()

ReentrantReadWriteLock函数列表如下:

  • // 创建一个新的ReentrantReadWriteLock,默认是采用非公平策略
  • ReentrantReadWriteLock()
  • // 创建一个新的ReentrantReadWriteLock,fair是公平策略;fair为true意味着公平策略,否则意味着非公平策略
  • ReentrantReadWriteLock(boolean fair)
  • // 返回当前拥有写入锁的线程,如果没有这样的线程,则返回null
  • protected Thread getOwner()
  • // 返回一个collection,它包含可能正在等待获取读取锁的线程
  • protected Collection<Thread> getQueuedReaderThreads()
  • // 返回一个collection,它包含可能正在等待获取读取或写入锁的线程
  • protected Collection<Thread> getQueuedThreads()
  • // 返回一个collection,它包含可能正在等待获取写入锁的线程
  • protected Collection<Thread> getQueuedWriterThreads()
  • // 返回等待获取读取或写入锁的线程估计数目
  • int getQueueLength()
  • // 查询当前线程在此锁上保持的重入读取锁数量
  • int getReadHoldCount()
  • // 查询为此锁保持的读取锁数量
  • int getReadLockCount()
  • // 返回一个collection,它包含可能正在等待与写入锁相关的给定条件的那些线程
  • protected Collection<Thread> getWaitingThreads(Condition condition)
  • // 返回正等待与写入锁相关的给定条件的线程估计数目
  • int getWaitQueueLength(Condition condition)
  • // 查询当前线程在此锁上保持的重入写入锁数量
  • int getWriteHoldCount()
  • // 查询是否给定线程正在等待获取读取或写入锁
  • boolean hasQueuedThread(Thread thread)
  • // 查询是否所有的线程正在等待获取读取或写入锁
  • boolean hasQueuedThreads()
  • // 查询是否有些线程正在等待与写入锁有关的给定条件
  • boolean hasWaiters(Condition condition)
  • // 如果此锁将公平性设置为 ture,则返回 true
  • boolean isFair()
  • // 查询是否某个线程保持了写入锁
  • boolean isWriteLocked()
  • // 查询当前线程是否保持了写入锁
  • boolean isWriteLockedByCurrentThread()
  • // 返回用于读取操作的锁
  • ReentrantReadWriteLock.ReadLock readLock()
  • // 返回用于写入操作的锁
  • ReentrantReadWriteLock.WriteLock writeLock()

ReentrantReadWriteLock的类图如下:

1.ReentrantReadWriteLock.png

从中可以看出:

  1. ReentrantReadWriteLock实现了ReadWriteLock接口。ReadWriteLock是一个读写锁的接口,提供了获取读锁的readLock()函数和获取写锁的writeLock()函数。
  2. ReentrantReadWriteLock中包含Sync同步器、读锁ReadLock和写锁ReadLock。读锁ReadLock和写锁WriteLock都实现了Lock接口。读锁ReadLock和写锁WriteLock中也都分别包含了Sync对象,它们的Sync对象和ReentrantReadWriteLock的Sync对象是一样的,通过Sync,读锁和写锁实现了对同一个对象的访问。
  3. 和ReentrantLock一样,Sync也是一个继承于AQS的抽象类,Sync也包括公平同步器FairSync和非公平同步器NonfairSync。sync对象是FairSync和NonfairSync中的一个,默认是NonfairSync。

2. ReentrantReadWriteLock示例

下面是一个ReentrantReadWriteLock的示例,演示了独占写和并发读的操作:

  • package com.coderap.lock;
  • import java.util.HashMap;
  • import java.util.Map;
  • import java.util.Random;
  • import java.util.concurrent.locks.Lock;
  • import java.util.concurrent.locks.ReadWriteLock;
  • import java.util.concurrent.locks.ReentrantReadWriteLock;
  • public class ReadAndWriteLockTest<K, V> {
  • private Map<K, V> map = new HashMap<>();
  • // 创建可重入读写锁
  • private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  • // 读锁
  • private Lock readLock = readWriteLock.readLock();
  • // 写锁
  • private Lock writeLock = readWriteLock.writeLock();
  • public V get(K key) {
  • try {
  • // 读加锁
  • System.out.println(Thread.currentThread().getName() + " begin read");
  • readLock.lock();
  • System.out.println(Thread.currentThread().getName() + " locked read");
  • try {
  • Thread.sleep(1000);
  • } catch (InterruptedException e) {
  • e.printStackTrace();
  • }
  • System.out.println(Thread.currentThread().getName() + " executing read");
  • V v = map.get(key);
  • System.out.println(Thread.currentThread().getName() + " end read");
  • return v;
  • } finally {
  • // 读解锁
  • readLock.unlock();
  • System.out.println(Thread.currentThread().getName() + " unlocked read");
  • }
  • }
  • public void put(K key, V value) {
  • try {
  • // 写加锁
  • System.out.println(Thread.currentThread().getName() + " begin write");
  • writeLock.lock();
  • System.out.println(Thread.currentThread().getName() + " locked write");
  • try {
  • Thread.sleep(1000);
  • } catch (InterruptedException e) {
  • e.printStackTrace();
  • }
  • System.out.println(Thread.currentThread().getName() + " executing write");
  • map.put(key, value);
  • System.out.println(Thread.currentThread().getName() + " end write");
  • } finally {
  • // 写解锁
  • writeLock.unlock();
  • System.out.println(Thread.currentThread().getName() + " unlocked write");
  • }
  • }
  • public static void main(String[] args) {
  • ReadAndWriteLockTest<String, String> readAndWriteLockTest = new ReadAndWriteLockTest<>();
  • // 测试写锁
  • testWrite(readAndWriteLockTest);
  • // 测试读锁
  • testRead(readAndWriteLockTest);
  • }
  • private static void testWrite(ReadAndWriteLockTest readAndWriteLockTest) {
  • new Thread(new Runnable() {
  • @Override
  • public void run() {
  • readAndWriteLockTest.put("key1", "value1");
  • }
  • }, "Write Thread - 1").start();
  • new Thread(new Runnable() {
  • @Override
  • public void run() {
  • readAndWriteLockTest.put("key2", "value2");
  • }
  • }, "Write Thread - 2").start();
  • new Thread(new Runnable() {
  • @Override
  • public void run() {
  • readAndWriteLockTest.put("key3", "value3");
  • }
  • }, "Write Thread - 3").start();
  • }
  • private static void testRead(ReadAndWriteLockTest readAndWriteLockTest) {
  • new Thread(new Runnable() {
  • @Override
  • public void run() {
  • readAndWriteLockTest.get("key1");
  • }
  • }, "Read Thread - 1").start();
  • new Thread(new Runnable() {
  • @Override
  • public void run() {
  • readAndWriteLockTest.get("key1");
  • }
  • }, "Read Thread - 2").start();
  • new Thread(new Runnable() {
  • @Override
  • public void run() {
  • readAndWriteLockTest.get("key1");
  • }
  • }, "Read Thread - 3").start();
  • }
  • }

在上面的示例代码的ReadAndWriteLockTest类中,使用ReentrantReadWriteLock锁分别实现了get()put()方法,其中get()方法模拟的是读操作,而put()方法模拟的是写操作,这两个方法中分别用读锁和写锁进行了控制;同时在main()方法中有testWrite()testRead()两个测试方法,testWrite()方法开启了三个写线程调用传入的readAndWriteLockTest对象的put()方法进行并发写入,testRead()方法则开启了三个读线程调用传入的readAndWriteLockTest对象的get()方法进行并发读取;需要注意的是,传给testWrite()testRead()两个方法的readAndWriteLockTest对象是同一个对象,因此它们存在读写并发的相互制约和控制;运行测试代码,某一次的结果如下:

  • Write Thread - 1 begin write
  • Write Thread - 1 locked write
  • Write Thread - 2 begin write
  • Write Thread - 3 begin write
  • Read Thread - 1 begin read
  • Read Thread - 2 begin read
  • Read Thread - 3 begin read
  • Write Thread - 1 executing write
  • Write Thread - 1 end write
  • Write Thread - 1 unlocked write
  • Write Thread - 2 locked write
  • Write Thread - 2 executing write
  • Write Thread - 2 end write
  • Write Thread - 2 unlocked write
  • Write Thread - 3 locked write
  • Write Thread - 3 executing write
  • Write Thread - 3 end write
  • Write Thread - 3 unlocked write
  • Read Thread - 1 locked read
  • Read Thread - 2 locked read
  • Read Thread - 3 locked read
  • Read Thread - 2 executing read
  • Read Thread - 1 executing read
  • Read Thread - 1 end read
  • Read Thread - 3 executing read
  • Read Thread - 3 end read
  • Read Thread - 1 unlocked read
  • Read Thread - 2 end read
  • Read Thread - 3 unlocked read
  • Read Thread - 2 unlocked read

从运行结果可以看出,一开始程序就开启了三个写线程,但只有Write Thread - 1线程成功获取到了写锁,Write Thread - 2Write Thread - 3两个线程虽然也开启了,但并没有获取到写锁,这里就体现了写锁的独占性;接下来开了三个读线程,但由于此时Write Thread - 1线程已经获取了写锁,所以三个读线程只能等待;Write Thread - 1线程在执行完写操作后释放了锁,然后是Write Thread - 2线程和Write Thread - 3线程依次获取读锁、执行写操作,然后释放锁;在最后,三个写线程都执行完之后,三个读线程同时获取到了读锁,并进行了并发读取,这里就体现了读锁的共享性;当三个读线程完成自己的读取操作之后,分别释放了自己获取的读锁。

3. 锁降级

锁降级指的是写锁降级为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种并不能称之为锁降级,锁降级指的是把持住当前拥有的写锁,再获取到读锁,随后释放先前用到的写锁的过程。重入锁是允许从写锁降级为读锁,其实现方式为先获取写锁,然后获取读锁,最后释放写锁。但是从读取锁升级到写入锁是不可行的。

我们先观察一段存在读写安全性问题的代码:

  • public class ReadAndWriteLockTest<K, V> {
  • private Map<K, V> map = new HashMap<>();
  • // 创建可重入读写锁
  • private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  • // 读锁
  • private Lock readLock = readWriteLock.readLock();
  • // 写锁
  • private Lock writeLock = readWriteLock.writeLock();
  • // 读写线程安全性演示
  • public V updateAndGet(K key, V value) {
  • // 先进行写
  • writeLock.lock();
  • int updateFlag = new Random().nextInt(1000);
  • System.out.println(Thread.currentThread().getName() + " Write Flag: " + updateFlag + ", K -> V: " + key + " -> " + value);
  • map.put(key, value);
  • writeLock.unlock();
  • try {
  • Thread.sleep(3000);
  • } catch (InterruptedException e) {
  • e.printStackTrace();
  • }
  • /**
  • * 然后再进行读
  • * 由于可能存在多个线程在竞争写锁,当上面写操作释放锁还没有运行到这行代码时,
  • * 可能其他的写线程又竞争到了锁,将数据修改了,因此存在线程安全性问题
  • */
  • V result = map.get(key);
  • System.out.println(Thread.currentThread().getName() + " Read Flag: " + updateFlag + ", K -> V: " + key + " -> " + result);
  • return result;
  • }
  • public static void main(String[] args) {
  • ReadAndWriteLockTest<String, Integer> readAndWriteLockTest = new ReadAndWriteLockTest<>();
  • for (int i = 0; i < 10; i++) {
  • final int index = i;
  • new Thread(new Runnable() {
  • @Override
  • public void run() {
  • readAndWriteLockTest.updateAndGet("key" + index % 3, new Random().nextInt(1000));
  • }
  • }).start();
  • }
  • }
  • }

某一次运行的结果如下:

  • Thread-1 Write Flag: 629, K -> V: key1 -> 565
  • Thread-4 Write Flag: 535, K -> V: key1 -> 95
  • Thread-5 Write Flag: 308, K -> V: key2 -> 303
  • Thread-9 Write Flag: 820, K -> V: key0 -> 35
  • Thread-0 Write Flag: 768, K -> V: key0 -> 655
  • Thread-8 Write Flag: 202, K -> V: key2 -> 269
  • Thread-2 Write Flag: 890, K -> V: key2 -> 547
  • Thread-6 Write Flag: 112, K -> V: key0 -> 817
  • Thread-7 Write Flag: 863, K -> V: key1 -> 40
  • Thread-3 Write Flag: 893, K -> V: key0 -> 732
  • Thread-5 Read Flag: 308, K -> V: key2 -> 547
  • Thread-1 Read Flag: 629, K -> V: key1 -> 40
  • Thread-4 Read Flag: 535, K -> V: key1 -> 40
  • Thread-0 Read Flag: 768, K -> V: key0 -> 732
  • Thread-9 Read Flag: 820, K -> V: key0 -> 732
  • Thread-2 Read Flag: 890, K -> V: key2 -> 547
  • Thread-3 Read Flag: 893, K -> V: key0 -> 732
  • Thread-6 Read Flag: 112, K -> V: key0 -> 732
  • Thread-8 Read Flag: 202, K -> V: key2 -> 547
  • Thread-7 Read Flag: 863, K -> V: key1 -> 40

代码的注释详细解释了写与读操作进行降级切换时可能会发生安全性问题的原因,同时运行的结果也演示了安全性问题的产生;要避免这种问题,则需要安全地对锁进行降级操作,锁降级代码如下:

  • public class ReadAndWriteLockTest<K, V> {
  • private Map<K, V> map = new HashMap<>();
  • // 创建可重入读写锁
  • private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  • // 读锁
  • private Lock readLock = readWriteLock.readLock();
  • // 写锁
  • private Lock writeLock = readWriteLock.writeLock();
  • // 锁降级演示
  • public V updateAndGet(K key, V value) {
  • // 先进行写
  • writeLock.lock();
  • int updateFlag = new Random().nextInt(1000);
  • System.out.println(Thread.currentThread().getName() + " Write Flag: " + updateFlag + ", K -> V: " + key + " -> " + value);
  • map.put(key, value);
  • // 此处将锁降级
  • readLock.lock();
  • writeLock.unlock();
  • try {
  • Thread.sleep(3000);
  • } catch (InterruptedException e) {
  • e.printStackTrace();
  • }
  • /**
  • * 然后再进行读
  • * 由于可能存在多个线程在竞争写锁,当上面写操作释放锁还没有运行到这行代码时,
  • * 可能其他的写线程又竞争到了锁,将数据修改了,因此存在线程安全性问题
  • *
  • * 可以使用锁降级来解决这个问题
  • * 使用锁降级后,由于在上面写锁期间以及获取了读锁,因此写操作完毕之后其他的写操作线程是无法获取到写锁的
  • * 因此可以保证读的数据就是写的数据
  • */
  • try {
  • V result = map.get(key);
  • System.out.println(Thread.currentThread().getName() + " Read Flag: " + updateFlag + ", K -> V: " + key + " -> " + result);
  • return result;
  • } finally {
  • // 释放读锁
  • readLock.unlock();
  • }
  • }
  • public static void main(String[] args) {
  • ReadAndWriteLockTest<String, Integer> readAndWriteLockTest = new ReadAndWriteLockTest<>();
  • for (int i = 0; i < 10; i++) {
  • final int index = i;
  • new Thread(new Runnable() {
  • @Override
  • public void run() {
  • readAndWriteLockTest.updateAndGet("key" + index % 3, new Random().nextInt(1000));
  • }
  • }).start();
  • }
  • }
  • }

使用锁降级后的运行结果如下:

  • Thread-4 Write Flag: 262, K -> V: key1 -> 382
  • Thread-4 Read Flag: 262, K -> V: key1 -> 382
  • Thread-0 Write Flag: 543, K -> V: key0 -> 660
  • Thread-0 Read Flag: 543, K -> V: key0 -> 660
  • Thread-7 Write Flag: 806, K -> V: key1 -> 834
  • Thread-7 Read Flag: 806, K -> V: key1 -> 834
  • Thread-8 Write Flag: 636, K -> V: key2 -> 692
  • Thread-8 Read Flag: 636, K -> V: key2 -> 692
  • Thread-9 Write Flag: 483, K -> V: key0 -> 874
  • Thread-9 Read Flag: 483, K -> V: key0 -> 874
  • Thread-6 Write Flag: 490, K -> V: key0 -> 703
  • Thread-6 Read Flag: 490, K -> V: key0 -> 703
  • Thread-5 Write Flag: 901, K -> V: key2 -> 710
  • Thread-5 Read Flag: 901, K -> V: key2 -> 710
  • Thread-3 Write Flag: 224, K -> V: key0 -> 130
  • Thread-3 Read Flag: 224, K -> V: key0 -> 130
  • Thread-2 Write Flag: 918, K -> V: key2 -> 727
  • Thread-2 Read Flag: 918, K -> V: key2 -> 727
  • Thread-1 Write Flag: 357, K -> V: key1 -> 976
  • Thread-1 Read Flag: 357, K -> V: key1 -> 976

从运行结果发现,写和读操作的切换是没有问题的,同时也保证了数据的安全性。关于锁降级的详细讨论,会在后面的内容中分析。

注:ReentrantReadWriteLock对应的源码分析,会在后面与AbstractQueuedSynchronizer一起介绍,这里读者只需要先明白ReentrantReadWriteLock的基本用法即可。