并发编程十:ReentrantReadWriteLock详解

ReentrantReadWriteLock详解

ReentrantReadWriteLock简介

现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁(读多写少)。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个 线程同时读取共享资源(读读可以并发);但是如果一个线程想去写这些共享资源,就不应该 允许其他线程对该资源进行读和写操作了(读写,写读,写写互斥)。在读多于写的情况下, 读写锁能够提供比排它锁更好的并发性和吞吐量。
针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它内部,维护了 一对相关的锁,一个用于只读操作,称为读锁;一个用于写入操作,称为写锁。
描述如下:
线程进入读锁的前提条件:

  • 没有其他线程的写锁
  • 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。

线程进入写锁的前提条件:

  • 没有其他线程的读锁
  • 没有其他线程的写锁

而读写锁有以下三个重要的特性:

  • 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公 平。
  • 可重入:读锁和写锁都支持线程重入。以读写线程为例:读线程获取读锁后,能够 再次获取读锁。写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁。
  • 锁降级:遵循获取写锁、再获取读锁最后释放写锁的次序,写锁能够降级成为读锁。

ReentrantReadWriteLock使用

ReentrantReadWriteLock的实现关系
在这里插入图片描述
ReentrantReadWriteLock实现了ReadWriteLock接口,该接口有一对方法,分别获得读锁和写锁 Lock 对象。
在这里插入图片描述
来看下读写锁怎么使用
在这里插入图片描述
这部分摘自ReentrantReadWriteLock源码的注释,上面的例子很详细了。在举个例子

public class Cache {
    static Map<String, Object> map = new HashMap<String, Object>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();
    // 获取一个key对应的value
    public static final Object get(String key) {
        r.lock();
        try {
            return map.get(key);
        } finally {
            r.unlock();
        }
    }
    // 设置key对应的value
    public static final Object put(String key, Object value) {
        w.lock();
        try {
            return map.put(key, value);
        } finally {
            w.unlock();
        }
    }
    // 清空所有的内容
    public static final void clear() {
        w.lock();
        try {
            map.clear();
        } finally {
            w.unlock();
        }
    }

上述示例中,Cache组合一个非线程安全的HashMap作为缓存的实现,同时使用读写锁的 读锁和写锁来保证Cache是线程安全的。在读操作get(String key)方法中,需要获取读锁,这 使得并发访问该方法时不会被阻塞。写操作put(String key,Object value)方法和clear()方法, 在更新 HashMap时必须提前获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被 阻塞,而 只有写锁被释放之后,其他读写操作才能继续。Cache使用读写锁提升读操作的并发 性,也保证每次写操作对所有的读写操作的可见性,同时简化了编程方式

在读写锁中存在这么一种情况,有些缓存数据不常变化,所以多个线程可以并发地进行数据处理(读锁),当数据变更后(写锁),如果当前线程感知到数据变化,则进行数据的准备工作,同时其他处理线程被阻塞,直到当前线程完成数据的准备工作。像这种情况可以使用到锁降级

在这里插入图片描述
这部分是ReentrantReadWriteLock源码中锁降级的写法草图,可以点进ReentrantReadWriteLock中查看。根据上面草图写了这么个例子

private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
private volatile boolean update = false;

public void processData() {
    readLock.lock();
    if (!update) {
        // 必须先释放读锁
        readLock.unlock();
        // 锁降级从写锁获取到开始
        writeLock.lock();
        try {
            if (!update) {
                // TODO 准备数据的流程(略)  
                update = true;
            }
            readLock.lock();
        } finally {
            writeLock.unlock();
        }
        // 锁降级完成,写锁降级为读锁
    }
    try {
        //TODO  使用数据的流程(略)
    } finally {
        readLock.unlock();
    }
}

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

ReentrantReadWriteLock源码分析

在这里插入图片描述
在这里插入图片描述

ReentrantReadWriteLock的内部Sync继承了AbstractQueuedSynchronizer。底层也是基于AQS框架
读写状态的设计:
设计的精髓:用一个变量如何维护多种状态
在 ReentrantLock 中,使用 Sync ( 实际是 AQS )的 int 类型的 state 来表示同步状态,表示锁 被一个线程重复获取的次数。但是,读写锁 ReentrantReadWriteLock 内部维护着一对读写锁,如果要用一个变量维护多种状态,需要采用按位切割使用的方式来维护这个变量,将其切分为两部分:高16为表示读,低16为表示写。
分割之后,读写锁是如何迅速确定读锁和写锁的状态呢?通过位运算。假如当前同步状态为S, 那么:

  • 写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)。 当写状态加1,等于S+1.
  • 读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。当读状态加1,等于 S+(1<<16),也就是S+0x00010000

根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状 态(S>>>16)大于0,即读锁已被获取
在这里插入图片描述
具体的代码实现:java.util.concurrent.locks.ReentrantReadWriteLock.Sync
在这里插入图片描述

  • exclusiveCount(int c) 静态方法,获得持有写状态的锁的次数。
  • sharedCount(int c) 静态方法,获得持有读状态的锁的线程数量。不同于写锁,读 锁可以同时被多个线程持有。而每个线程持有的读锁支持重入的特性,所以需要对每个 线程持有的读锁的数量单独计数,这就需要用到 HoldCounter 计数器

HoldCounter 计数器

读锁的内在机制其实就是一个共享锁。一次共享锁的操作就相当于对HoldCounter计数器 的操作。获取共享锁,则该计数器 + 1,释放共享锁,该计数器 - 1。只有当线程获取共享锁后 才能对共享锁进行释放、重入操作。
在这里插入图片描述
通过 ThreadLocalHoldCounter 类,HoldCounter 与线程进行绑定。HoldCounter 是绑定线程的一个计数器,而 ThreadLocalHoldCounter 则是线程绑定的 ThreadLocal。

  • HoldCounter是用来记录读锁重入数的对象
  • ThreadLocalHoldCounter是ThreadLocal变量,用来存放不是第一个获取读锁的线 程的其他线程的读锁重入数对象

写锁的获取
写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程, 则当前线程进入等待状态。 写锁的获取是通过重写AQS中的tryAcquire方法实现的。

 protected final boolean tryAcquire(int acquires) {
    //当前线程
    Thread current = Thread.currentThread();
    //获取state状态   存在读锁或者写锁,状态就不为0
    int c = getState();
    //获取写锁的重入数
    int w = exclusiveCount(c);
    //当前同步状态state != 0,说明已经有其他线程获取了读锁或写锁
    if (c != 0) {
        // c!=0 && w==0 表示存在读锁
        // 当前存在读锁或者写锁已经被其他写线程获取,则写锁获取失败
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        // 超出最大范围  65535
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        //同步state状态
        setState(c + acquires);
        return true;
    }
    // writerShouldBlock有公平与非公平的实现, 非公平返回false,会尝试通过cas加锁
    //c==0 写锁未被任何线程获取,当前线程是否阻塞或者cas尝试获取锁 
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;

    //设置写锁为当前线程所有
    setExclusiveOwnerThread(current);
    return true;

这写锁时独占锁加锁和解锁逻辑可以参考ReentrantLock,之前都有分析过不在深入分析。

读锁的获取
实现共享式同步组件的同步语义需要通过重写AQS的tryAcquireShared方法和 tryReleaseShared方法。读锁的获取实现方法为:

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    // 如果写锁已经被获取并且获取写锁的线程不是当前线程,当前线程获取读锁失败返回-1   判断锁降级
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    //计算出读锁的数量
    int r = sharedCount(c);
    /**
    * 读锁是否阻塞    readerShouldBlock()公平与非公平的实现
    * r < MAX_COUNT: 持有读锁的线程小于最大数(65535)
    *  compareAndSetState(c, c + SHARED_UNIT) cas设置获取读锁线程的数量
    */
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {   //当前线程获取读锁
        
        if (r == 0) {  //设置第一个获取读锁的线程
            firstReader = current; 
            firstReaderHoldCount = 1;  //设置第一个获取读锁线程的重入数
        } else if (firstReader == current) { // 表示第一个获取读锁的线程重入
            firstReaderHoldCount++;
        } else { // 非第一个获取读锁的线程
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;  //记录其他获取读锁的线程的重入次数
        }
        return 1;
    }
    // 尝试通过自旋的方式获取读锁,实现了重入逻辑
    return fullTryAcquireShared(current);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值