多线程的锁操作——ReentrantReadWriteLock类

1 ReadWriteLock接口

本文接上一篇的ReentrantLock类, 讲一讲 ReadWriteLock 接口及其实现类ReentrantReadWriteLock。

ReadWriteLock 也是一个接口,提供了 readLockwriteLock 两种不同用途锁的操作,该接口定义在java.util.concurrent.locks.ReadWriteLock,定义如下:

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

通过其定义,大概总结如下:

  1. ReadWriteLock 并不是 Lock 的子接口,而是一种新的锁接口,只是用到了Lock接口作为成员方法返回值,实现其特定功能
  2. ReadWriteLock 提供了两个锁——读取锁、写入锁;其中每次读取共享数据时需要使用读取锁,当需要修改共享数据时就需要使用写入锁

2 ReentrantReadWriteLock实现类

ReentrantReadWriteLockReadWriteLock 接口的实现类。从ReentrantReadWriteLock 字面上来看包括了两重含义:一层含义是读写锁;另外一层含义是具有可重入性

  • 读写锁就是每次读取共享数据时使用读取锁,当需要修改共享数据时就使用写入锁。

  • 可重入性就是如果对资源加了写锁,其他线程无法再获得写锁与读锁,但是持有写锁的线程,可以对资源加读锁;如果一个线程对资源加了读锁,其他线程可以继续加读锁。

综上,以下是对ReentrantReadWriteLock的读写锁机制的总结:

  • 读操作-读操作不互斥: 没有发生写操作,当多个线程同时执行读操作,那么这多个线程可以并发执行,不会发生阻塞
  • 写操作-读操作互斥: 当前正在发生写操作,那么此刻到来的读线程就会被阻塞
  • 读操作-写操作互斥: 当前正在发生读操作,那么此刻到来的写线程就会被阻塞
  • 写操作-写操作互斥: 当多个线程同时执行写操作时,某个线程先拿到锁就先执行,其他线程会被阻塞直到之前线程释放锁

3 ReentrantReadWriteLock的应用

关于ReentrantReadWriteLock的典型用法一般有两种。

3.1 读操作与写操作相分离的场景

//读写锁的典型用法
public class ReadAndWrite {
    //定义一个读写锁
    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    //获取一个可以被多个读操作可共享的读取锁,同时互斥所有写操作
    private Lock  readLock = rwLock.readLock();
    //获取一个只能独占的写入锁,同时互斥所有的读操作与写操作
    private Lock writeLock = rwLock.writeLock();
    
    private int cost;
    
    //对所有读操作加读锁
    public int get(){
    //关于锁的用法与Lock的用法类似
    //需要配合try-finally使用
    readLock.lock();
    try{
        //TODO
    }finally{
        readLock.unlock();
    }
    return cost;
    }
    
    //对所有写操作加写锁
    public void set() {
    //关于锁的用法与Lock的用法类似
    //需要配合try-finally使用
    writeLock.lock();
    try {
        //TODO
        } finally {
        // 释放锁
        writeLock.unlock();
    }
    }
}

3.2 读操作与写操作相混合的场景

class CachedData {
    //缓存的数据内容
    Object data;
    //标识缓存数据是否准备好
    volatile boolean cacheValid;
    //读写锁
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
        //为了先从缓存中获取数据,首先申请读锁
    rwl.readLock().lock();
        
    //如果缓冲中的数据还未准备好
    if (!cacheValid) {
        //由于缓冲中的数据还未准备好,我们首先需要从数据库中获取数据源
        //但是为了执行写操作,需要申请写锁,由于读锁与写锁互斥
        //需要首先是否读锁
        rwl.readLock().unlock(); //语句A
            
        //为了写入数据,需要首先申请写锁
        rwl.writeLock().lock(); 
        try {
            //需要再次判断,因为这里存在一种情况:比如之前有两个(甲与乙)读线程都执行到了语句A
        //其中甲线程获取到了写锁,开始执行;而乙线程被阻塞
        //等待甲执行完后,释放了写锁;乙线程获取到了写锁会继续执行
        //但是,此时甲线程已执行了语句B,已经将cacheValid置为了true,
        //下述的if判断可保证乙线程不再重复从数据库中获取数据源
        if (!cacheValid) {
            data = ...
            cacheValid = true;//语句B
        }
        //写操作完成时,必须进行锁降级,为什么是必须?请看下文
        //即:释放写锁之前先获取读锁
        rwl.readLock().lock();//语句C
         } finally {
            //释放写锁,但是仍然持有读锁
            rwl.writeLock().unlock(); 
        }
        }

        //缓冲的中的数据已准备好
        try {
            //用户的业务逻辑,使用数据
        use(data);
        } finally {
        //使用完数据后,最后释放读锁
            rwl.readLock().unlock();
        }
    }
}

如果当前线程不获取读锁而是直接释放写锁,假设此刻另外一个线程获取了写锁并修改了数据,那么当前线程无法感知后者线程的数据更新。如果当前线程获取了读锁,即遵循了锁降级的步骤,由于读-写互斥,后续线程就会被阻塞,直到当前线程使用数据并释放读锁后,后续线程线程才能获取写锁进行数据更新。

你可能感兴趣的:(多线程的锁操作——ReentrantReadWriteLock类)