并发编程之ReentrantReadWriteLock详解

目录

ReentrantReadWriteLock介绍

线程进入读锁的前提条件

线程进入写锁的前提条件

ReentrantReadWriteLock三个重要的特性

ReentrantReadWriteLock类

ReentrantReadWriteLock使用读写锁

锁降级

注意事项

ReentrantReadWriteLock结构

 ReentrantReadWriteLock读写状态设计

HoldCounter 计数器

写锁的获取

写锁的释放

读锁的获取

读锁的释放


ReentrantReadWriteLock介绍

        ReentrantReadWriteLock是Java中的一个读写锁实现,它允许多个线程同时读取共享资源(读读可以并发),但在写入时只允许一个线程独占(读写,写读,写写互斥)。这个锁支持重入,即同一个线程可以多次获取相同类型的锁。这种锁可以提高读操作的并发性,同时保证写操作的独占性。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。

线程进入读锁的前提条件

没有其他线程的写锁

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

线程进入写锁的前提条件

没有其他线程的读锁

没有其他线程的写锁

ReentrantReadWriteLock三个重要的特性

公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。

可重入:读锁和写锁都支持线程重入。以读写线程为例:读线程获取读锁后,能够再次获取读锁。写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁。

锁降级:遵循获取写锁、再获取读锁最后释放写锁的次序,写锁能够降级成为读锁。


ReentrantReadWriteLock类

 ReentrantReadWriteLock实现了ReadWriteLock接口

                  

        ReentrantReadWriteLock是可重入的读写锁实现类。在它内部,维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 Writer 线程,读锁可以由多个 Reader 线程同时持有。也就是说,写锁是独占的,读锁是共享的。

ReentrantReadWriteLock使用读写锁
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class SharedData {
    private int data = 0;
    private ReadWriteLock lock = new ReentrantReadWriteLock();

    public int readData() {
        lock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " is reading data: " + data);
            return data;
        } finally {
            lock.readLock().unlock();
        }
    }

    public void writeData(int newValue) {
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " is writing data: " + newValue);
            data = newValue;
        } finally {
            lock.writeLock().unlock();
        }
    }

    public static void main(String[] args) {
        SharedData sharedData = new SharedData();

        // 创建多个读线程
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                sharedData.readData();
            }, "ReaderThread-" + i).start();
        }

        // 创建一个写线程
        new Thread(() -> {
            sharedData.writeData(42);
        }, "WriterThread").start();
    }
}

       SharedData 类包含一个整数 data,并使用 ReentrantReadWriteLock 对其进行读写保护。多个读线程(ReaderThread)可以同时访问 readData 方法,而写线程(WriterThread)在调用 writeData 方法时会独占写锁,以确保写入的原子性。多个线程能够同时读取数据,但只有一个线程能够写入新的数据。(ReentrantReadWriteLock适合读多写少的场景)

锁降级

        在互斥锁中,锁是排他的,即一旦线程获取了写锁,其他线程就不能获取读锁或写锁,直到写锁被释放。而ReentrantReadWriteLock允许降级,即允许一个线程在持有写锁的情况下获取读锁,并最终释放写锁,而保持读锁的持有。

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class LockDowngradeExample {
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void performWrite() {
        lock.writeLock().lock(); // 获取写锁
        try {
            // 执行写操作
            System.out.println("Performing write operation...");
            
            // 获取读锁(锁降级)
            lock.readLock().lock();
        } finally {
            lock.writeLock().unlock(); // 释放写锁
        }

        try {
            // 执行读操作
            System.out.println("Performing read operation...");
        } finally {
            lock.readLock().unlock(); // 释放读锁
        }
    }

    public static void main(String[] args) {
        LockDowngradeExample example = new LockDowngradeExample();
        example.performWrite();
    }
}

        在以上代码中,线程首先获取写锁,执行写操作,然后将写锁降级为读锁。在读锁的情况下可以执行读操作,而不会阻塞其他线程获取读锁。这个降级的过程就是锁降级。

注意事项

读锁不支持条件变量。

重入时升级不支持:持有读锁的情况下去获取写锁,会导致获取永久等待。

重入时支持降级: 持有写锁的情况下可以去获取读锁。


ReentrantReadWriteLock结构


 ReentrantReadWriteLock读写状态设计

       在 ReentrantLock 中,使用 Sync ( 实际是 AQS )的 int 类型的 state 来表示同步状态,表示锁被一个线程重复获取的次数。但是,读写锁 ReentrantReadWriteLock 内部维护着一对读写锁,如果要用一个变量维护多种状态,需要采用“按位切割使用”的方式来维护这个变量,将其切分为两部分:高16为表示读,低16为表示写。

通过位运算确定读锁和写锁的状态:

写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)。 当写状态加1,等于S+1.

读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。当读状态加1,等于S+(1<<16),也就是S+0x00010000

根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。

 exclusiveCount(int c) 静态方法,获得持有写状态的锁的次数。

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

HoldCounter 计数器

       读锁其实就是一个共享锁。一次共享锁的操作就相当于对HoldCounter 计数器的操作。获取共享锁,则该计数器 + 1,释放共享锁,该计数器 - 1。只有当线程获取共享锁后才能对共享锁进行释放、重入操作。

 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;

writerShouldBlock写锁是否阻塞实现取决公平与非公平的策略(FairSync和NonfairSync)

      

写锁的释放

写锁释放通过重写AQS的tryRelease方法实现

protected final boolean tryRelease(int releases) {
    //若锁的持有者不是当前线程,抛出异常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    //当前写状态是否为0,为0则释放写锁
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;

                                 

读锁的获取

通过重写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);

 readerShouldBlock读锁是否阻塞实现取决公平与非公平的策略(FairSync和NonfairSync)

    

读锁的释放

读锁释放的实现主要通过方法tryReleaseShared:

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    //如果当前线程是第一个获取读锁的线程
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--; //重入次数减1
    } else {  //不是第一个获取读锁的线程
        HoldCounter rh = cachedHoldCounter;  
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;  //重入次数减1
    }
    for (;;) {  //cas更新同步状态
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            // Releasing the read lock has no effect on readers,
            // but it may allow waiting writers to proceed if
            // both read and write locks are now free.
            return nextc == 0;
    }

                                       

 本文参考自图灵学院Fox老师笔记!

  • 24
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值