ReentrantReadWriteLock 读写锁

之前提到锁(如Mutex和ReentrantLock)基本都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

除了保证写操作对读操作的可见性以及并发性的提升之外,读写锁能够简化读写交互场景的编程方式。假设在程序中定义一个共享的用作缓存数据结构,它大部分时间提供读服务(例如查询和搜索),而写操作占有的时间很少,但是写操作完成之后的更新需要对后续的读服务可见。

在没有读写锁支持的(Java 5之前)时候,如果需要完成上述工作就要使用Java的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行通知之后,所有等待的读操作才能继续执行(写操作之间依靠synchronized关键进行同步),这样做的目的是使读操作能读取到正确的数据,不会出现脏读。改用读写锁实现上述功能,只需要在读操作时获取读锁,写操作时获取写锁即可。当写锁被获取到时,后续(非当前写操作线程)的读写操作都会被阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使用等待通知机制的实现方式而言,变得简单明了。

一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量

ReentrantReadWriteLock其实实现的是ReadWriteLock接口

ReadWriteLock接口

public interface ReadWriteLock {
    //获取读锁
    Lock readLock();
	//获取写锁
    Lock writeLock();
}

ReentrantReadWriteLock类

构造方法
//创建一个ReentrantReadWriteLock实例.
ReentrantReadWriteLock()        

//创建一个具有给定公平策略的ReentrantReadWriteLock实例.
ReentrantReadWriteLock(boolean fair)
常用方法摘要
//返回用于读取操作的锁.
Lock ReentrantReadWriteLock.ReadLock.readLock()   

//返回用于写入操作的锁.
Lock ReentrantReadWriteLock.WriteLock.writeLock()

//返回等待获取读取或写入锁的线程估计数目.
int getQueueLength()

//如果此锁的公平设置为 true,则返回 true.
boolean isFair()

//返回标识此锁及其锁状态的字符串.
String toString()
ReadLock/WriteLock静态内部类
//试图获取锁.
void lock() 

//如果当前线程未被中断,则获取锁.
void lockInterruptibly()        

//返回绑定到此 Lock 实例的新 Condition 实例.
Condition newCondition()         

//仅在调用时锁为空闲状态才获取该锁.
boolean tryLock()          

//如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁.
boolean tryLock(long time, TimeUnit unit)        

//试图释放锁.
void unlock()

//返回标识此锁及其锁状态的字符串.
String toString()

因为ReadLock不支持条件,因此当调用了ReadLock的newCondition()方法时将会抛出UnsupportedOperationException异常。

使用ReentrantReadWriteLock的读锁以及写锁,将会遵循读读共享写写互斥读写互斥

使用示例

public class ReentrantReadWriteLockTest {
    /**
     * 创建线程池
     */
    private static ExecutorService executorService = Executors.newCachedThreadPool();
    /**
     * 创建读写锁
     */
    private static ReadWriteLock lock = new ReentrantReadWriteLock();

    /**
     * 读锁
     */
    private static Lock readLock = lock.readLock();

    /**
     * 获取写锁
     */
    private static Lock writeLock = lock.writeLock();


    /**
     * 读操作
     */
    public static void reading() {
        System.out.println("尝试获取读锁:" + Thread.currentThread().getId());
        readLock.lock();
        System.out.println("获取读锁成功:" + Thread.currentThread().getId());
        try {
            System.out.println("开始进行读操作:" + Thread.currentThread().getId());
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("释放读锁成功:" + Thread.currentThread().getId());
            readLock.unlock();
        }
    }


    /**
     * 写操作
     */
    public static void writing() {
        System.out.println("尝试获取写锁:" + Thread.currentThread().getId());
        writeLock.lock();
        System.out.println("获取写锁成功:" + Thread.currentThread().getId());
        try {
            System.out.println("开始进行写操作:" + Thread.currentThread().getId());
        } finally {
            System.out.println("释放写锁成功:" + Thread.currentThread().getId());
            writeLock.unlock();
        }
    }
}

读读共享

public static void main(String[] args) {
       //读读共享
       for (int i = 0; i < 3; i++) {
           executorService.submit(() -> reading());
       }
       executorService.shutdown();
   }

输出

尝试获取读锁:12
尝试获取读锁:14
尝试获取读锁:13
获取读锁成功:14
开始进行读操作:14
获取读锁成功:12
开始进行读操作:12
获取读锁成功:13
开始进行读操作:13
释放读锁成功:12
释放读锁成功:14
释放读锁成功:13

读锁能被多个线程同时获取,能提高读取的效率 (虽然只用读锁时可以不进行释放,但会影响写锁的获取)

写写互斥

public static void main(String[] args) {
       //读读共享
       for (int i = 0; i < 3; i++) {
           executorService.submit(() -> writing());
       }
       executorService.shutdown();
   }

输出

尝试获取写锁:12
尝试获取写锁:13
尝试获取写锁:14
获取写锁成功:12
开始进行写操作:12
释放写锁成功:12
获取写锁成功:13
开始进行写操作:13
释放写锁成功:13
获取写锁成功:14
开始进行写操作:14
释放写锁成功:14

写锁同一时刻只能被一个线程获取。

读写互斥

 public static void main(String[] args) {
    //读写互斥
    for (int i = 0; i < 3; i++) {
        executorService.submit(() -> {
            reading();
        });
        executorService.submit(() -> {
            writing();
        });
    }
    executorService.shutdown();
}

输出

尝试获取读锁:12
获取读锁成功:12
开始进行读操作:12
尝试获取写锁:13
尝试获取读锁:14
尝试获取写锁:15
尝试获取读锁:16
尝试获取写锁:17
释放读锁成功:12
获取写锁成功:13
开始进行写操作:13
释放写锁成功:13
获取读锁成功:14
开始进行读操作:14
释放读锁成功:14
获取写锁成功:15
开始进行写操作:15
释放写锁成功:15
获取读锁成功:16
开始进行读操作:16
释放读锁成功:16
获取写锁成功:17
开始进行写操作:17
释放写锁成功:17

读的时候不能写,写的时候不能读,即获取读锁时如果写锁此时被线程持有则将等待写锁被释放,获取写锁时如果读锁此时有被线程持有则将等待读锁被释放且写锁未被持有。