ReentrantReadWriteLock

ReentrantReadWriteLock(读写锁)

什么是读写锁

读写锁是一种特殊的锁,他把对资源的访问分为读访问和写访问,多个线程可以同时对共享资源进行读访问,但是同一时间只能有一个线程对共享资源进行写访问,使用读写锁可以极大地提高并发量。

读写锁特性

是否互斥

类结构

在这里插入图片描述
从类图我们可以看到

1、ReentrantReadWriteLock本身实现了ReadWriteLock接口,这个接口只提供了两个方法readLock()writeLock()
2、主要由AQS(同步器)实现相关功能,包含一个继承了AQS的Sync内部类,以及其两个子类FairSyncNonfairSync
3、ReadLockWriteLock两个内部类实现了Lock接口,它们具有锁的一些特性。

源码分析

主要属性

以下就是读写锁中的几个核心属性,很简单,但是它内部却是十分复杂。

// 读锁
private final ReentrantReadWriteLock.ReadLock readerLock;
// 写锁
private final ReentrantReadWriteLock.WriteLock writerLock;
// 同步器
final Sync sync;

主要构造方法

它提供了两个构造方法,默认构造方法使用的是非公平锁模式,在构造方法中初始化了读锁和写锁。

public ReentrantReadWriteLock() {
    this(false);
}

// 参数为ture创建公平锁,false创建非公平锁
// 并初始化读锁和写锁
public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

获取读锁和写锁的方法

上面我们说过ReentrantReadWriteLock实现了ReadWriteLock接口,这里通过实现readLock()writeLock()两个方法将读锁和写锁暴露给外部。

public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

读锁和写锁

读锁和写锁的代码其实都很简单,主要是实现了Lock的接口,这里就不再详细讲解,因为读锁和写锁的实现的核心代码主要都在Sync和它的两个子类FairSyncNonfairSync中,所以我们要重点分析这三个类。

// 读锁
public static class ReadLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = -5992448646407690164L;
    // 注意此处持有Sync的引用,核心代码都在这个类里面
    private final Sync sync;

    protected ReadLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }
    // ......
}
// 写锁
public static class WriteLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = -4992448646407690164L;
    private final Sync sync;

    protected WriteLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }
}

Sync核心类及其子类

Sync核心属性
abstract static class Sync extends AbstractQueuedSynchronizer {
	// 高16位为读锁,低16位为写锁
    static final int SHARED_SHIFT   = 16;
    // 读锁单位,2^16
    static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
    // 读锁最大持有数量或读锁最大可重入数
    static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
    // 和state执行&操作,使高16抹0
    static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
    // 本地线程计数器
    private transient ThreadLocalHoldCounter readHolds;
    // 缓存计数器
	private transient HoldCounter cachedHoldCounter;
    // 第一个读线程
    private transient Thread firstReader = null;
    // 第一个读线程的计数
    private transient int firstReaderHoldCount;
    // 读锁的数量,持有读锁的线程数
    static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
    // 写锁的数量,即写锁的重入次数
    static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
}
控制的读锁和写锁状态的核心

我们知道在AQS中都是通过state变量来表示锁的状态,但是state是一个整形,只能表示两种状态,但是在读写锁中有读写锁是分离的,需要四种表示状态,为了解决这个问题就将state变量分为了高16位和低16位,高16位表示读锁状态,低16位表示写锁状态。我们知道int是32位的,将其无符号右移16位就得到了高16位(读锁)的值;将其与2^16 - 1做与运算,高16位全部和0相与全为零,低16位全部和1相与全部不变,这样就相当于将高16位的值全部抹零了,得到的就是低16位(写锁)的值。
在这里插入图片描述

Sync内部类

Sync中有两个内部类,分别为HoldCounter和ThreadLocalHoldCounter,其中HoldCounter主要与读锁配套使用。

// 记录线程持有的锁的重入次数
static final class HoldCounter {
    // 计数
    int count = 0;
    // 线程id
    final long tid = getThreadId(Thread.currentThread());
}
// 记录了每个线程对应的HoldCounter
static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    // 重写了ThreadLocal的initialValue方法
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}
读锁和写锁的实现

我们知道读写锁是基于AQS实现的,关于AQS我们在上一篇博客中有详细的介绍,这里就不做重点分析,感兴趣的朋友可以参考这篇博客。我们这里主要分析Sync如何重写AQS的四个钩子方法tryAcquiretryReleasetryAcquireSharedtryReleaseShared

我们从读写锁的读写、写写互斥的特性我们可以看出读锁其实是共享锁,因此主要是通过重写tryAcquireSharedtryReleaseShared两个方法实现,写锁其实是互斥锁,主要通过tryAcquiretryRelease这两个方法实现。下面我们重点分析这几个方法。

读锁的获取
// ReentrantReadWriteLock.ReadLock.lock()
public void lock() {
    sync.acquireShared(1);
}
// AbstractQueuedSynchronizer.acquireShared()
public final void acquireShared(int arg) {
    // 尝试获取共享锁(返回1表示成功,返回-1表示失败)
    if (tryAcquireShared(arg) < 0)
        // 失败了就可能要排队
        doAcquireShared(arg);
}
// ReentrantReadWriteLock.Sync.tryAcquireShared()
protected final int tryAcquireShared(int unused) {    
    Thread current = Thread.currentThread();
    // 当前状态
    int c = getState();
    // 如果写锁的状态不为0并且持有锁的线程和不等于当前线程,说明此时其它的线程获得了写锁,返回-1
    // exclusiveCount(c)互斥锁(写锁)的次数
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    // 读锁被获取的次数
    int r = sharedCount(c);
    // readerShouldBlock:读锁是否阻塞,是返回true,不是false
    // 读锁不阻塞,持有读锁的数量小于最大值,并且CAS修改锁的状态成功,此时表示获取读锁成功。
    // SHARED_UNIT = 2^16,高16位表示读锁,相当于读锁数量加1
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        // 获取读锁成功,如果持有读锁的数量为0,将当前线程设置为第一个持有读锁线程,并设置持有读锁的数量为1
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            // 如果第一持有读锁的线程等于当前线程,那么持有读锁的数量加1
            firstReaderHoldCount++;
        } else {
            // 缓存计数器,我们上面讲过HoldCounter主要用来记录线程持有的锁的重入次数
            HoldCounter rh = cachedHoldCounter;
            // 如果rh等于null或者当前线程id和缓存的计数器中的线程不是同一个线程,那么去本地计数器中获取线程对应的HoldCounter
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            // 缓存的HoldCounter是这个线程的HoldCounter,并且count等于0,将HoldCounter保存在本地计数器中
            else if (rh.count == 0)
                readHolds.set(rh);
           	// 该线程的锁的计数加1
            rh.count++;
        }
        return 1;
    }
    // 获取锁失败重新尝试获取锁
    return fullTryAcquireShared(current);
}

上面我们说了获取锁失败则在fullTryAcquireShared重新尝试获取,其实fullTryAcquireShared的代码和tryAcquireShared差不多。

final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    // 死循环
    for (;;) {
        int c = getState();
        // 此处里面分为好几种情况,十分复杂
        // 如果写锁的状态不为0,说明此时有线程获得了写锁
        if (exclusiveCount(c) != 0) {
            // 1、写锁被获取并且持有锁的线程和不等于当前线程,返回-1,获取锁失败
            if (getExclusiveOwnerThread() != current)
                return -1;
            // 2、写锁被获取并且持有锁的线程等于当前线程
        } else if (readerShouldBlock()) {
            // 3、写锁空闲,且该读线程应该被阻塞
            // readerShouldBlock:读锁是否阻塞,是返回true,不是false。
            // 如果当前线程是持有读锁的第一个线程,确保我们不会在获取到读锁
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
            } else {
                // 当前线程不是持有读锁的第一个线程
                // 如果rh为null,则从缓存计数中获取
                // 如果缓存计数也为空则从本地计数器中获取
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        // 如果rh的count为0则从本地计数器中移除该线程对应的HoldCounter
                        // 4、当前读线程需要阻塞且是非重入(当前线程还未获取读锁的),获取失败。
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                // 如果rh.count等于0,返回-1,表示获取锁失败
                // 4、这里表示的是该读线程需要阻塞,且是未重入的(即该线程之前未获取到读锁),获取失败
                if (rh.count == 0)
                    return -1;
            }
        }
        // 共享锁(读锁)的数量大于最大值,抛异常
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 修改state的值,尝试去获取读锁
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            // 获取读锁成功,且持有读锁的数量为0,初始化
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                // 如果第一持有读锁的线程等于当前线程,那么持有读锁的数量加1
                firstReaderHoldCount++;
            } else {
                // rh为null,从缓存计数器中获取
                if (rh == null)
                    rh = cachedHoldCounter;
                // 仍然为null,从本地计数器中获取
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                // 如果count等于0,将其保存到本地计数器
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
                cachedHoldCounter = rh; // cache for release
            }
            // 获取成功
            return 1;
        }
    }
}
读锁的释放
// ReentrantReadWriteLock.ReadLock.unlock()
public void unlock() {
    sync.releaseShared(1);
}
// AbstractQueuedSynchronizer.releaseShared()
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
// ReentrantReadWriteLock.Sync.tryReleaseShared()
// 此处unused为1
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    // 如果当前线程是第一个持有读锁的线程
    if (firstReader == current) {
        // 如果第一个持有读锁的线程的重入次数为1,则将firstReader设置为null
        if (firstReaderHoldCount == 1)
            firstReader = null;
        // 如果第一个持有读锁的线程的重入次数不为1,将重入次数减1
        else
            firstReaderHoldCount--;
        // 如果当前线程不是第一个持有读锁的线程
    } else {
        HoldCounter rh = cachedHoldCounter;
        // 如果rh为空从本地计数器中获取
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        // 如果count小于等于1,则将其该线程对应的HoldCounter从本地计数器中移除,说明该线程完全释放了锁
        if (count <= 1) {
            readHolds.remove();
            // 如果count小于等于0抛异常
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;
    }
    for (;;) {
        // 死循环,读锁的线程持有数量减1
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            // 释放读锁对其他读线程没有任何影响,
            // 但可以允许等待的写线程继续,如果读锁、写锁都空闲1253254570
            return nextc == 0;
    }
}

注意:在读锁的获取或释放时,锁的重入次数(holdcount)和锁的获取次数(state)都要同步的加1或减1。

写锁的获取
// acquires为1
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    // 写锁的持有数量
    int w = exclusiveCount(c);
    // 如果c不等于0,表示有线程正在持有锁
    if (c != 0) {
        // 如果c != 0和w == 0并且持有锁的线程不等于当前线程,获取锁失败
        // c !=0和w=0,这句表示写锁的数量为0,读锁的数量不为0
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        // 如果c != 0,w!=0,持有锁线程等于当前线程,并且写锁的重入次数大于写锁最大可重入次数,抛出异常
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 写锁的重入次数加1
        // 这里为什么说是写锁的冲入次数而不是数量呢?
        // 因为写锁同一时刻只能被同一线程持有,而写锁是可重入的因此state的低16位表示的写锁的重入次数
        setState(c + acquires);
        // 获取锁成功
        return true;
    }
    // c=0,表示没有线程持有锁
    // writerShouldBlock:写线程是否阻塞,true是,false否
    // compareAndSetState尝试加锁失败,返回false
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    // 加锁成功设置持有锁线程,注意只有写锁才会设置持有锁线程,读锁不会设置
    setExclusiveOwnerThread(current);
    return true;
}
写锁的释放
protected final boolean tryRelease(int releases) {
    // 不是排它锁抛异常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 写锁的重入次数减1
    int nextc = getState() - releases;
    boolean free = exclusiveCount(nextc) == 0;
    // 如果写锁的重入数等于0,将持有锁的线程设置为null
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    // free为true释放成功,false失败
    return free;
}

Sync的钩子方法

在Sync中暴露了两个钩子方法给子类去重写,分别是readerShouldBlockwriterShouldBlock,这两个方法在我们上面分析锁的获取释放时有提到,但没有详细深入,其实这两个方法十分重要。我们知道在Sync有公平(FairSync)和非公平(NonfairSync)两个子类,这两个子类中这两个方法的实现都不相同,公平策略和非公平策略。

公平类(FairSync

我们看到在公平类中这两个方法都是通过调用hasQueuedPredecessors方法实现的,下面我们来进行详细的分析。

static final class FairSync extends Sync {
    // 当AQS队列未初始化或第一个等待节点和当前线程相同时返回false,即不阻塞,不需要排队获取锁
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }
    // 当AQS队列未初始化或第一个等待节点和当前线程相同时返回false,即不阻塞,不需要排队获取锁
    final boolean readerShouldBlock() {
        return hasQueuedPredecessors();
    }
}

public final boolean hasQueuedPredecessors() {
    Node t = tail; 
    Node h = head;
    Node s;
    // 判断队列中是否有等待节点,此处分为三种情况
    // 1、队列未初始化,头节点和尾节点都为null,所以返回fasle,队列中没有等待的节点,当然不需要排队
    // 2、队列以初始化,头节点的后续节点为空,此时s.thread肯定为null,所以s.thread != Thread.currentThread()条件不成立,即表示队列中只有一个节点,此时我们要注意的是我们的头节点是持有锁的节点,如果队列中只有一个头节点,那么表示队列中没有等待的节点,需要排队,返回true
    // 3、队列以初始化,头节点后续节点不为空,即队列中不止一个节点,若后续节点的线程不等于当前线程,即比较排队的第一个节点和当前节点是否相同,是则不需要排队尝试获取锁(返回false),不是需要排队(返回true)
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
非公平(NonfairSync
static final class NonfairSync extends Sync {
    // 返回false,表示写锁永远不阻塞,为什么这么写?因为写锁只有在锁空闲的时候或者是重入的时候才能够加锁成功,没必要阻塞,影响性能
    final boolean writerShouldBlock() {
        return false; 
    }
    // 1、队列未初始化,2、队列中只有一个节点,3、共享锁,4、后继节点线程为空,线程取消,这四种情况不阻塞,不需要排队
    final boolean readerShouldBlock() {
        return apparentlyFirstQueuedIsExclusive();
    }
}

final boolean apparentlyFirstQueuedIsExclusive() {
    Node h, s;
    // 1、队列未初始化,false
    // 2、队列初始化,后继节点为空,即队列中只有一个节点,false
    // 3、队列初始化,后继节点不为空,是共享锁,false
    // 4、队列初始化,后继节点不为空,不是共享锁,后继节点的线程为空,线程取消,false
    // 5、队列初始化,后继节点不为空,不是共享锁,后继节点的线程不为空,true
    return (h = head) != null &&
        (s = h.next)  != null &&
        !s.isShared()         &&
        s.thread != null;
}

参考博客:

https://segmentfault.com/a/1190000015768003

https://blog.51cto.com/14267003/2408707

e
// 3、队列初始化,后继节点不为空,是共享锁,false
// 4、队列初始化,后继节点不为空,不是共享锁,后继节点的线程为空,线程取消,false
// 5、队列初始化,后继节点不为空,不是共享锁,后继节点的线程不为空,true
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}


**参考博客:**

[https://segmentfault.com/a/1190000015768003](https://segmentfault.com/a/1190000015768003)

[https://blog.51cto.com/14267003/2408707](https://blog.51cto.com/14267003/2408707)

[https://www.cnblogs.com/xiaoxi/p/9140541.html](https://www.cnblogs.com/xiaoxi/p/9140541.html)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值