【Java并发锁】ReentrantReadWriteLock读写锁

目录

 

1 前言

2 接口与示例

3 实现分析

3.1 读写状态的设计

3.2 写锁的获取与释放

3.2.1 tryAcquire方法

3.2.2 tryRelease方法

3.3 读锁的获取与释放

3.3.1 tryAcquireShared方法

3.3.2 tryReleaseShared

4 公平锁和非公平锁

4.1 使用原理

4.2 公平锁

4.3 非公平锁


1 前言

本人使用jdk8版本

首先,ReentrantReadWriteLock中包含三种锁:读写锁、可重入锁、公平/非公平锁。各种锁的介绍可参考:Java中各种锁介绍

读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

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

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

2 接口与示例

ReadWriteLock仅定义了获取读锁和写锁的两个方法,即ReadLock()方法和writeLock()方法,而其实现:ReentranReadWriteLock,除接口方法之外,还提供了一些便于外界监控其内部工作状态的方法:

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

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

public class Cache {
static Map<String, Object> map = new HashMap<>();
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,并返回旧的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();
    }
}
}

3 实现分析

接下来分析ReentranReadWriteLock的实现,主要包括读写状态的设计、写锁的获取与释放、读锁的获取与释放以及锁降级(以下没有特别说明读写锁均可认为是ReentranReadWriteLock)。

3.1 读写状态的设计

ReentranReadWriteLock中使用一个int state变量来维护读锁和写锁的状态。int类型为4字节32位,用state的高16位来存储读锁的状态,用低16位来存储写锁的状态。

当前同步状态表示一个线程已经获取了写锁,且重进入了两次,同时也连续获取了两次读锁。读写锁是如何迅速的确定读和写各自的状态呢?答案是通过位运算。假设当前同步状态值为S,写状态等于 S & 0x0000FFFF(将高16位全部抹去),读状态等于 S >>> 16(无符号补0右移16位)。当写状态增加1时,等于S + 1,当读状态增加1时,等于S + (1 << 16),也就是S + 0x00010000。

3.2 写锁的获取与释放

ReentranReadWriteLock的写锁的获取与释放实现基于同步器AbstractQueuedSynchronizer,与其它独占锁的差异就在于对同步器中tryAcquire()和tryRelease()的重写上,所以下面仅讨论这两个方法。AbstractQueuedSynchronizer独占锁释放获取实现见:AbstractQueuedSynchronizer独占式同步状态获取与释放

3.2.1 tryAcquire方法

官方给出的注释如下:

  1. 若读锁数或写锁数非空,并且当前线程不是读锁或写锁的持有者,获取失败。
  2. 若当前线程获取写锁的数量已经达到最大值,失败。
  3. 否则,当前线程获得锁。

writerShouldBlock()介绍见第4点。

        protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);    // 写锁数
            if (c != 0) {
                // if c != 0 and w == 0 意味着读锁数大于0
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                // 写锁数超过上限
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // 当前线程持有锁,且在此获得锁
                setState(c + acquires);
                return true;
            }
            // 若c=0,读写锁数量都为0。writerShouldBlock()始终返回false
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }

3.2.2 tryRelease方法

释放方法很简单,就是对锁的状态进行判断,无误则对状态进行修改。

        protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())        // 读锁是否被当前线程持有
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }

3.3 读锁的获取与释放

读锁是一个支持重进入的共享锁,他能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功的获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。

3.3.1 tryAcquireShared方法

当写锁被另外一个线程持有时,获取读锁失败;否则调用readerShouldBlock()来判断这次获取读锁的操作应不应该阻塞,然后判断读锁数是否达到上限,如果上面两次判断都通过,就调用compareAndSetState()来更新锁状态。

readerShouldBlock()方法主要是为了避免尝试获取写锁的线程陷入无限阻塞,如果队列的头结点是一个等待的writer节点,那么该方法就会返回true。

通过下面代码可以看到,在调用compareAndSetState()更新锁状态成功后,会进入if方法体。这段代码的作用主要是:更新第一个获取读锁的线程指针和获取次数(firstReader 和firstReaderHoldCount),更新上一个获取读锁的线程指针(cachedHoldCounter),最后更新当前线程获取读锁的数量(通过ThreadLocal存在自己线程的容器中)。若读锁数量为0,表示当前线程是第一个,执行下面1处代码;若当前线程是第一个,然后现在再次获取了,执行下面2处代码;否则更新当前线程的获取读锁数和cachedHoldCounter,最后获取读锁数都+1。最后返回1。

cachedHoldCounter是一个ThreadLocal的实现类,它将自己作钥匙,把当前线程获取读锁的数量存在了当前线程中,ThreadLocal可以参考:ThreadLocal底层原理实现

readerShouldBlock()见第4点介绍。

        protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)    // 写锁被其它线程持有
                return -1;
            int r = sharedCount(c);                      // 读锁数量
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {    // 读锁+1
                if (r == 0) {
                    // 1.当前线程是第一个获取读锁的线程
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    // 2.当前线程是第一个,然后现在再次获取了
                    firstReaderHoldCount++;
                } else {
                    // 3.更新当前线程的获取读锁数,然后更新cachedHoldCounter
                    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);
        }

3.3.2 tryReleaseShared

首先更新firstReader,然后更新cachedHoldCounter和当前线程的锁数量,最后更新锁的状态。同时若读写锁数量都为0,则返回true,这是为了上层方法唤醒阻塞的writer线程;否则返回false。

 protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            if (firstReader == current) {
                // 当前线程是第一个获取读锁的线程
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;    // 上一个获取读锁的线程
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();        // rh指向当前线程获取读锁的数量对象
                int count = rh.count;            // 当前线程获取读锁的数量
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;        // 若当前线程读锁数>1,则数量-1
            }
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;         // 读写锁的数量   
                if (compareAndSetState(c, nextc))    // 更新锁状态
                    // 若读写锁数量都为0,则返回true,这里是为了唤醒阻塞的writer线程
                    return nextc == 0;
            }
        }

4 公平锁和非公平锁

与ReentrantLock一样,ReentrantReadWriteLock也分为公平锁和非公平锁,可以在构造方法中指定,不指定默认就是非公平锁。公平锁和非公平锁都继承自内部同步器Sync,它们的区别仅在writerShouldBlock()和readerShouldBlock()这两个方法的实现上。writerShouldBlock()在tryAcquire()方法中即获取写锁的时候会调用,readerShouldBlock()在tryAcquireShared()方法中即获取写锁的时候会调用。

    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
    public ReentrantReadWriteLock() {
        this(false);
    }

4.1 使用原理

就是在构造方法的时候给sync属性传入FairLock或NonFairLock对象,然后接下来就用sync来进行锁的操作了。

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    final Sync sync;    // 构造方法中初始化成FairLock或NonFairLock,进行lock和unlock的主要对象

    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

    public ReentrantReadWriteLock() {    this(false);    }

    // 自定义同步器,tryAcquire()、tryRelease()等主要方法都在里面实现了
    abstract static class Sync extends AbstractQueuedSynchronizer {...}

    // 公平锁,就是一个同步器,特别实现readerShouldBlock()和writerShouldBlock()来实现公平锁
    static final class FairSync extends Sync {...}

    // 非公平锁,就是一个同步器,特别实现readerShouldBlock()和writerShouldBlock()来实现非公平锁
    static final class NonfairSync extends Sync {...}
}

4.2 公平锁

writerShouldBlock()和readerShouldBlock()都是判断当前节点有没有不是头结点的前驱节点,即是否有线程比当前线程等待更久的时间,有的话返回false,否则true。

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -2274990926593161451L;
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();    // 判断当前节点有没有不是头结点的前驱节点
        }
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }

    public final boolean hasQueuedPredecessors() {
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

4.3 非公平锁

获取写锁的线程一定不会阻塞,因为始终返回false。获取读锁的线程,若队列中第一个节点是独占节点,则该线程获取读锁失败,这是为了避免获取写锁一直阻塞。

   static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;
        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }
        final boolean readerShouldBlock() {
            return apparentlyFirstQueuedIsExclusive();    // 队列中第一个节点是否是writer节点
        }
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值