Java 并发编程:读写锁 ReentrantReadWriteLock(八)

版权声明:本文为博主原创文章,转载请注明出处 https://blog.csdn.net/u010647035/article/details/84930857

1、读写锁(ReadWriteLock)简介

ReentrantReadWriteLock是Lock的另一种实现方式,同一时间允许多个读线程访问,但是在写线程访问时,所有的读写线程都被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁使并发性比一般的排它锁有了很大的提升。

一般情况下,读写锁的性能都会比排它锁好,因为大多场景下读操作要多于写操作。在读操作多于写操作的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。Java并发包中提供的读写锁的实现是ReentrantReadWriteLock,本文就针对该读写锁的实现做一个全面的学习。

2、读写锁(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();
}

ReadWriteLock 仅仅定义了获取读锁(readLock() )和写锁(writeLock() )的两个方法,而该接口的实现ReentrantReadWriteLock,除了实现接口中的方法外,还提供了一些便于监控内部工作状态的方法。

//ReentrantReadWriteLock.java

    /**
     * 获取当前读锁被获取的次数
     */
    public int getReadLockCount() {
        return sync.getReadLockCount();
    }

    /**
     * 获取当前线程获取写锁的次数
     */
    public int getWriteHoldCount() {
        return sync.getWriteHoldCount();
    }
    
    /**
     * 获取当前线程获取读锁的次数
     */
    public int getReadHoldCount() {
        return sync.getReadHoldCount();
    }

    /**
     * 判断当前写锁对象是否被获取
     */
    public boolean isWriteLocked() {
        return sync.isWriteLocked();
    }
    /**
     * 判断当前线程是否获取了写锁
     */
    public boolean isWriteLockedByCurrentThread() {
        return sync.isHeldExclusively();
    }
    /**
     * 返回正在等待获取写锁的线程的集合。返回的线程集合没有特定的顺序
     */
    protected Collection<Thread> getQueuedWriterThreads() {
        return sync.getExclusiveQueuedThreads();
    }
    /**
     * 返回正在等待获取读锁的线程的集合
     */
    protected Collection<Thread> getQueuedReaderThreads() {
        return sync.getSharedQueuedThreads();
    }

    /**
     * 获取正在等待获取读锁或写锁线程的集合
     */
    protected Collection<Thread> getQueuedThreads() {
        return sync.getQueuedThreads();
    }
    /**
     * 获取正在等待获取读锁或写锁线程的数量
     */
    public final int getQueueLength() {
        return sync.getQueueLength();
    }

2.1、读写锁(ReadWriteLock)使用示例

接下来使用一个缓存示例,学习一下读写锁的使用方式。缓存中使用一个非线程安全的HashMap 作为缓存对象的实现,同时使用读写锁来保证对缓存对象cacheMap操作的线程安全。

/**
 *
 *
 * @author kaifeng
 * @date 2018/12/9
 */
public class ReadWriteLockCache {
    private static Map<String, Object> cacheMap = new HashMap<String, Object>();
    private static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private static Lock readLock = rwl.readLock();
    private static Lock writeLock = rwl.writeLock();

    /**
     * 根据指定key获取值
     *
     * @param key 指定key
     */
    public static Object get(String key) {
        readLock.lock();
        try {
            return cacheMap.get(key);
        } finally {
            readLock.unlock();
        }
    }

    /**
     * 更新指定key对应的值,并返回旧值
     */
    public static Object put(String key, Object value) {
        writeLock.lock();
        try {
            return cacheMap.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }

    /**
     * 清空所有缓存内容
     */
    public static void clear() {
        writeLock.lock();
        try {
            cacheMap.clear();
        } finally {
            writeLock.unlock();
        }
    }
}

1、读操作 get(String key) 中,需要获取读锁,使并发访问该方法时不会被阻塞。

2、写操作 put(String key, Object value) 和 clear() 中,在更新 cacheMap之前必须先获取写锁,当获取写锁后,其它线程对读写锁的获取都会被阻塞,只有写锁释放后,其它读写操作才能继续。

3、cacheMap 使用读写锁提高了读操作的并发性,也保证了写操作对所有读写操作的可见性,同时使编程方式更简化了

3、读写锁(ReadWriteLock)特性

1)支持公平和非公平的获取锁的方式;

2)支持可重入。读线程在获取了读锁后还可以获取读锁;写线程在获取了写锁之后既可以再次获取写锁又可以获取读锁;

3)允许锁降级。从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不允许的;

4)支持锁中断。读取锁和写入锁都支持锁获取期间的中断;

5)Condition支持。仅写入锁提供了一个 Conditon 实现;读取锁不支持 Conditon ,当使用 readLock().newCondition() 时会抛出 UnsupportedOperationException。

4、读写锁(ReadWriteLock)实现分析

4.1、读写状态的原理

同步状态在重入锁的实现中是表示被同一个线程重复获取的次数,即一个整形变量来维护,但是之前的那个表示仅仅表示是否锁定,而不用区分是读锁还是写锁。而读写锁需要在同步状态(一个整形变量)上维护多个读线程和一个写线程的状态。

读写锁对于同步状态的实现是在一个整形变量上通过【按位切割使用】:将变量切割成两部分,高16位表示读,低16位表示写

在这里插入图片描述

当前 同步 状态 表示 一个 线程 已经 获取 了 写 锁, 且 重 进入 了 两次, 同时 也 连续 获取 了 两次 读 锁。 读写 锁 是 如何 迅速 确定 读 和 写 各自 的 状态 呢?

当前同步状态表示一个线程获取了写锁,且重入了两次,同时也连续获取了两次读锁。

那么读写锁时如何快速确定读写各自的状态的呢?

假设当前同步状态值为S,get和set的操作如下:

1、获取写状态:

S&0x0000FFFF : 将高16位全部抹去

2、获取读状态:

S>>>16 : 无符号补0,右移16位

3、写状态加1:

S+1

4、读状态加1:

S+(1<<16)即S + 0x00010000

根据状态的划分能得出一个推论:

如果S不等于0,当写状态(S&0x0000FFFF)等于0时,而读状态(S>>>16)大于0,则表示读锁已被获取。

4.2、写锁的获取与释放

写锁是一个支持可重入的排它锁,如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取或另一个线程已经获取了写锁,则当前线程将进入等待状态。

4.2.1、写锁的获取

下面看一下写锁获取的代码实现:

protected final boolean tryAcquire(int acquires) {
    //当前线程
    Thread current = Thread.currentThread();
    //获取同步状态
    int c = getState();
    //写线程数量
    int w = exclusiveCount(c);
    
    //当前同步状态c != 0,说明其他线程获取了读锁或写锁
    if (c != 0) {
        //存在读锁或当前线程不是持有写锁的线程
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        
        //判断同一线程获取写锁是否超过最大次数(65535)
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        
        //此时当前线程已持有写锁,现在是重入,所以只需要修改锁的获取次数即可
        setState(c + acquires);
        return true;
    }
    
    //此时c=0,读锁和写锁都没有被获取,writerShouldBlock表示是否阻塞
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    
    //设置锁为当前线程所有
    setExclusiveOwnerThread(current);
    return true;
}

由源码可知,写锁获取的实现步骤如下:

1、首先获取当前同步状态(c)和获取写锁线程的数量(w)

2、如果同步状态 c != 0,说明已经有其他线程获取了读或写锁,进入判断(3),否则进入(5)

3、如果同步状态 c != 0 并且 w==0 或当前线程不是持有写锁的线程则返回false,当前线程不能获取写锁

4、如果同步状态 c != 0,判断当前线程获取写锁的次数是否超过了最大值,如果超过则抛出异常,否则更新同步状态(累加获取写锁线程的数量),返回 true

5、如果同步状态 c == 0,表示读锁或写锁都没有被获取,则CAS更新同步状态,若CAS成功则返回true,失败则说明锁被别的线程抢去了,返回false。

4.2.1、写锁的释放

protected final boolean tryRelease(int releases) {
    //若锁的持有者不是当前线程,抛出异常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //写锁的新线程数
    int nextc = getState() - releases;
    //如果独占模式重入数为0了,说明独占模式被释放
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        //若写锁的新线程数为0,则将锁的持有者设置为null
        setExclusiveOwnerThread(null);
    //设置写锁的新线程数
    //不管独占模式是否被释放,更新独占重入数
    setState(nextc);
    return free;
}

写锁的释放过程相对而言比较简单,首先查看当前线程是否为写锁的持有者,如果不是抛出异常。然后检查释放后写锁的持有线程数是否为0,如果为0则表示写锁空闲了,释放锁资源将锁的持有线程设置为null,否则仅仅只是一次重入锁而已,并不能将写锁的线程清空。

4.3、读锁的获取与释放

读锁是一个支持可重入的共享锁,它能够被多个线程同时获取,在写状态为0时,读锁总是会被成功获取,需要的操作仅仅是增加读状态。如果当前线程在获取读锁时,写锁已经被其它线程持有,则当前线程进入等待状态。

类似于写锁,读锁的lock和unlock的实际实现对应Sync的 tryAcquireShared 和 tryReleaseShared方法。

4.3.1、读锁的获取

protected final int tryAcquireShared(int unused) {
    // 获取当前线程
    Thread current = Thread.currentThread();
    // 获取同步状态
    int c = getState();
    
    //如果写锁线程数 != 0 ,且独占锁不是当前线程则返回失败
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    // 读锁数量
    int r = sharedCount(c);
    /*
     * readerShouldBlock():读锁是否需要等待(公平锁原则)
     * r < MAX_COUNT:持有线程小于最大数(65535)
     * compareAndSetState(c, c + SHARED_UNIT):设置读取锁状态
     */
     // 读线程是否应该被阻塞、并且小于最大值、并且比较设置成功
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        //r == 0,表示第一个读锁线程,第一个读锁firstRead是不会加入到readHolds中
        if (r == 0) { // 读锁数量为0
            // 设置第一个读线程
            firstReader = current;
            // 读线程占用的资源数为1
            firstReaderHoldCount = 1;
        } else if (firstReader == current) { 
        // 当前线程为第一个读线程,表示第一个读锁线程重入,占用资源数加1
            firstReaderHoldCount++;
        } else { // 读锁数量不为0并且不为当前线程
            // 获取计数器
            HoldCounter rh = cachedHoldCounter;
            // 计数器为空或者计数器的tid不为当前正在运行的线程的tid
            if (rh == null || rh.tid != getThreadId(current)) 
                // 获取当前线程对应的计数器
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0) // 计数为0
                //加入到readHolds中
                readHolds.set(rh);
            //计数+1
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

tryAcquireShared(int unused) 方法中,如果其它线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或写锁未被获取,则当前线程增加读状态,成功获取读锁。读锁的每次释放都是减少读状态的值,减少的值时( 1<< 16)

4.3.1、读锁的释放

protected final boolean tryReleaseShared(int unused) {
    // 获取当前线程
    Thread current = Thread.currentThread();
    if (firstReader == current) { // 当前线程为第一个读线程
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1) // 读线程占用的资源数为1
            firstReader = null;
        else // 减少占用的资源
            firstReaderHoldCount--;
    } else { // 当前线程不为第一个读线程
        // 获取缓存的计数器
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current)) // 计数器为空或者计数器的tid不为当前正在运行的线程的tid
            // 获取当前线程对应的计数器
            rh = readHolds.get();
        // 获取计数
        int count = rh.count;
        if (count <= 1) { // 计数小于等于1
            // 移除
            readHolds.remove();
            if (count <= 0) // 计数小于等于0,抛出异常
                throw unmatchedUnlockException();
        }
        // 减少计数
        --rh.count;
    }
    for (;;) { // 无限循环
        // 获取状态
        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;
    }
}

读锁线程释放锁,首先判断当前线程是否为第一个读线程firstReader,若是,则判断第一个读线程占有的资源数firstReaderHoldCount是否为1,若是,则设置第一个读线程firstReader为空,否则,将第一个读线程占有的资源数firstReaderHoldCount减1;若当前线程不是第一个读线程,那么首先会获取缓存计数器(上一个读锁线程对应的计数器 ),若计数器为空或者tid不等于当前线程的tid值,则获取当前线程的计数器,如果计数器的计数count小于等于1,则移除当前线程对应的计数器,如果计数器的计数count小于等于0,则抛出异常,之后再减少计数即可。无论何种情况,都会进入无限循环,该循环可以确保成功设置状态state

4.4、锁降级

锁降级是指写锁降级为读锁。如果当前线程拥有写锁,然后释放掉,再获取读锁,这种分开完成的过程不能称为锁降级。锁降级是线程持有写锁的情况下再去获取读锁,然后释放掉写锁,仍持有读锁。

public void processData() {
        readLock.lock();
        if (!update) {
            // 必须 先 释放 读 锁 
            readLock.unlock();

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

当数据发生变更后,update 变量被设置为false,此时所有访问 processData() 方法 的线程都能感知到变化,但只有一个线程能够获取到写锁,其他线程会被阻塞在读锁和 写锁的lock() 方法上。当前线程获取写锁完成数据准备之后,再获取读锁,随后释放写 锁,完成锁降级。

总结

1、在线程持有读锁的情况下,该线程不能取得写锁,因为获取写锁的时候,如果发现当前的读锁被占用,立即就会获取失败,不管读锁是不是被当前线程持有。

2、在线程持有写锁的情况下,该线程可以继续获取读锁,获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败。

3、锁降级是合理的,因为当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。

4、一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁。

猜你喜欢

转载自blog.csdn.net/u010647035/article/details/84930857