深扒AQS(四)之ReentrantReadWriteLock

前言

上一节我们说了ReentrantLock,这个锁可以很好的保证线程安全,接下来我们考虑对这个锁的优化,实际上很多业务场景都是读多写少的场景,ReentrantLock是个独占锁,所以不能很好的应对并发的读请求,ReentrantReadWriteLock应运而生,可以分离读锁和写锁,其中读锁是共享锁,写锁是独占锁,提高并发性能。

基本介绍

属性
public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
   
    private final ReentrantReadWriteLock.ReadLock readerLock;
   
    private final ReentrantReadWriteLock.WriteLock writerLock;
   
    final Sync sync;
    
    static final class HoldCounter {
            int count = 0;
            // 线程id
            final long tid = getThreadId(Thread.currentThread());
    }
    
    // 这是个包着HoldCounter类的ThreadLocal子类,用来记录每个线程对应的HoldCounter
    static final class ThreadLocalHoldCounter
        extends ThreadLocal<HoldCounter> {
        public HoldCounter initialValue() {
            return new HoldCounter();
        }
    }
    
    // 存放除了第一个获取读锁线程之外的其他线程的读锁重入次数
    private transient ThreadLocalHoldCounter readHolds;
    
    // 记录最后一个获取读锁的线程的获取读锁的的重入次数
    private transient HoldCounter cachedHoldCounter;
    
    // 第一个持有读锁的线程
    private transient Thread firstReader = null;
    
    // 第一个持有读锁的线程获取读锁的可重入次数
    private transient int firstReaderHoldCount;
    
    
    // 写锁的当前持有线程
    private transient Thread exclusiveOwnerThread;
    
   
}    

类似于ReentrantLock,底层依然依靠sync实现,同时还多了两个内部类ReadLock和WriteLock,上一节中AQS维护的state是锁的重入次数,本节中的state则表示读写两种状态。除此之外的其他属性,先有个概念,我们后边用到再细说

公平与非公平
// 默认是非公平锁
public ReentrantReadWriteLock() {
    this(false);
}

// 可以设置公平和非公平锁
public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}
    
static final class NonfairSync extends Sync {
        // 非公平获取写锁时直接获取不需阻塞等待
        final boolean writerShouldBlock() {
            return false; 
        }
        // 非公平锁获取读锁时候,若是首节点是共享节点,不需阻塞等待
        final boolean readerShouldBlock() {
            return apparentlyFirstQueuedIsExclusive();
        }
}

// 这是AQS中的方法,如果当前阻塞队列非空 且 首节点(非哨兵节点)不为空 且 首节点不是共享节点 且 首节点线程不为空 ,返回true
final boolean apparentlyFirstQueuedIsExclusive() {
        Node h, s;
        return (h = head) != null &&
            (s = h.next)  != null &&
            !s.isShared()         &&
            s.thread != null;
}

    // 公平锁不管是获取写锁还是读锁都需要排队等待自己成为首节点
    static final class FairSync extends Sync {
    
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }
state的读写状态
static final int SHARED_SHIFT   = 16;

// 读状态是在高16位操作,加1就是加1 0000 0000 0000 0000,即加SHARED_UNIT;减1亦然,这样方便直接在state上进行操作       
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);

// 线程最大数 65535
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

// 所有线程获取读锁的次数和,即读锁状态
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

// 返回写锁的已重入次数,即写锁状态
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

这里来解释一下ReentrantReadWriteLock中一个state怎么维持两种状态,其实很简单,state是个32位的int变量,那我们就分成两份,高16位表示读,低16位表示写,假设状态为S,读状态为S>>>16,写状态为S&0x0000FFFF。

写锁

写锁的获取
public void lock() {
            sync.acquire(1);
}

写锁是独占锁,我们这里也能看到内部调用的AQS的独占模式,acquire()中的参数1在这里代表的是重入次数.这块儿在上一节中我们说过,最后又会回来调用tryAcquire().

protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            // 获取写状态,写锁的重入次数
            int w = exclusiveCount(c);
            if (c != 0) {
            
                // c!=0且w == 0说明读锁状态不为0,说明有线程持有读锁
                // c!=0且w!=0且当前线程不持有写锁说明写锁被其他线程持有
                //这两种情况均直接返回false,不能获得写锁,返回false
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                
                // 此时w!=0且当前线程持有写锁,检查获取锁后会否溢出    
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
               
                // 不会溢出,获得写锁并更新state
                setState(c + acquires);
                return true;
            }
            // 如果不需等待自己成为首节点
            // CAS设置状态,失败的话直接返回false;成功则设置当前线程为持有锁线程,返回true
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
}

总结一下:

  • 有线程持有读锁时不能获取写锁

  • 其他线程持有写锁时,不能获取写锁

这两点算是获取写锁时的基本限制条件了,如果获取失败别忘了是要进入阻塞队列的。

写锁的释放
public void unlock() {
            sync.release(1);
}

还是一样的步骤,我们直接看tryRelease()

protected final boolean tryRelease(int releases) {
            // 必须持有锁才能释放锁
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            // 这里并没有跟写锁掩码相交,因为获取锁时读锁状态肯定为0
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
}

其实这块儿跟ReentrantLock基本完全一样,不再复述,很简单,就是重入次数减一,如果重入次数为0了,返回true;否则返回false

此外,类似于之前,我们知道还有一些响应中断的、带超市时间的获取写锁的方式,不再一一介绍

读锁

读锁的获取
public void lock() {
            sync.acquireShared(1);
}

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
}

可以看到读锁这里采用了共享模式的获取方法,接着调用tryAcquireShared,

protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
            // 当其他线程持有写锁,获取读锁失败,return -1,接着构造共享节点加入阻塞队列
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            // 此时没有线程持有写锁或本线程持有写锁,可以获取读锁
            // r为持有读锁的线程    
            int r = sharedCount(c);
            
            // 不需要阻等待获取而且r没有溢出且CAS设置state
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                
                // 此时没有线程持有读锁,当前线程将是第一个获取读锁的线程
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                }
                // 此时有线程持有读锁
                // 当前线程是这些线程中第一个获取读锁的线程,更新firstReaderHoldCount
                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);
}

思考一下为什么要记录首个和最后一个获取读锁的线程?
后边会说这个问题

final int fullTryAcquireShared(Thread current) {
            HoldCounter rh = null;
            for (;;) {
                int c = getState();
                // 其他线程持有写锁时直接返回-1
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                // 发生等待            
                } else if (readerShouldBlock()) {
                    if (firstReader == current) {
                    } 
                    // 当前线程不是第一个获得读锁的线程
                    else {
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != getThreadId(current)) {
                                rh = readHolds.get();
                                if (rh.count == 0)
                                    readHolds.remove();
                            }
                        }
                        if (rh.count == 0)
                            return -1;
                    }
                }
                // 此时没有发生等待
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // CAS更新state,更新对应的firstreader或者readHolds或者cachedHoldCounter,CAS成功返回1,否则继续自旋
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (sharedCount(c) == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
}
读锁的释放
public void unlock() {
            sync.releaseShared(1);
}

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
}

老规矩,调用tryReleaseShared。

注意这里doReleaseShared 释放的是因为获取写锁而被阻塞的线程

protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            
            // 如果当前线程是第一个获取读锁的,更新firstReader和firstReaderHoldCount
            if (firstReader == current) {
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } 
            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;
            }
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                // CAS 更新state,最后返回状态是否为0,为 0 的话就说明此时没有线程持有读锁,然后调用doReleaseShared释放一个由于获取写锁而被阻塞的线程
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
}

总结

首先说个问题,有了readHolds为什么还需要firstreader和cachedHoldCounter呢?

还是一句话,性能的优化,不要当然也可以,大不了就都从readHolds中查找嘛,但是,当线程很多时,查找是需要代价的,这时候就远远不如直接通过引用来查找了。

为什么当前线程持有写锁的情况下还能继续获取读锁呢?

其实就是一个可见性的问题,当前线程获取写锁后,其他线程显然不能再获取写锁,所以此时的修改操作只能在当前线程进行,此时完全可以把本线程内的任务看成是顺序执行的,别的线程不会干扰他,自然可以获取读锁进行读取操作

为什么当前线程持有读锁的情况下不能继续获取写锁呢?

如果可以允许读锁升级为写锁,这里面就涉及一个很大的竞争问题,所有的读锁都会去竞争写锁,这样以来必然引起巨大的抢占,这是非常复杂的,因为如果竞争写锁失败,那么这些线程该如何处理?是继续还原成读锁状态,还是升级为竞争写锁状态?这一点是不好处理的,所以Java的api为了让语义更加清晰,所以只支持写锁降级为读锁,不支持读锁升级为写锁。DK8中新增的StampedLock类就可以比较优雅的完成这件事,这个到后面我们再分析。

总结一下锁的获取:

  • 当有线程持有读锁时当前线程不能获取写锁

  • 当前线程持有写锁时可以继续获取读锁,此时再释放写锁继续持有读锁,即锁降级

  • 其他线程持有写锁时不能获取读锁

  • 不支持锁升级



    个人公众号
    在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值