AQS 混合模式 之可重入的读写锁ReentrantReadWriteLock

读写锁思想

1、如果有一个线程获取到了读锁,那么其他线程可以获取到读锁,但是不能获取写锁
2、如果一个线程获取到了写锁,则其他线程不能获取读锁或者写锁。

锁降级的过程

一个线程获取到了写锁,还可以继续获取读锁

示例代码:

public class TestReentrantReadWriteLock {
    public static void main(String[] args) {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
        
        new Thread(()->{
            /*获取写锁*/
            writeLock.lock();
            try {
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName() + " 获取读锁");
                /*获取读锁*/
                readLock.lock();
                TimeUnit.SECONDS.sleep(5);
                
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                readLock.unlock();
                System.out.println(Thread.currentThread().getName() + "释放写锁");
                writeLock.unlock();
            }
            
        }, "写-》读").start();

        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                readLock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "获取读锁");
                }finally {
                    readLock.unlock();
                }
            }, "读线程" + i).start();
        }
    }
}

获取读锁之后,别的读写还是不能去执行。。。, 读锁是可重入共享模式, 写锁是可重入独占模式

思考:读锁可重入共享 是不是 semaphore和countdownlatch的结合体???

state结构

呃, ReentrantReadWriteLock 因为是混合模式, 所以 state需要怎么标识 共享锁和独占锁呢。。。。
所以 读锁的共享锁 是state的高16位, 写锁的独占锁是state的低16位, 所以读写的最大数量都是65535。

读锁加一: state + 1<<16 写锁加一: state+1
获取写锁数量 state& (1<<16 - 1)
获取读锁数量 state>>16

读锁写锁分别要实现AQS的模板方法

写锁(独占锁)读锁(共享锁)
尝试获取锁tryAquiretryAquireShared
尝试释放锁tryReleasetryReleaseShared

ReentrantReadWriteLock整体代码框架

思考: AQS作为一个通用的线程同步框架, 如果要实现独占锁,走的模板方法需要子类去实现tryAcquire, 如果要实现共享锁,走的模板方法是tryAcquireShared, 对于公平和非公平来说,独占锁和共享锁都是一样的,公平就是在获取cas需要看队列里有没有元素,但是:有个注意的点就是如果是共享锁读锁,公平就是看队列里有没有节点,而非公平是要看队列里下一个节点是不是写节点。因为读写锁一个大的问题就是写锁饥饿

public class MyReentrantReadWriteLock implements ReadWriteLock {
    
    private MySync mySync;
    private ReadLock readLock;
    private WriteLock writeLock;
    
    
    public MyReentrantReadWriteLock(boolean fair) {
        mySync = fair? new FairSync() : new UnFairSync();
        readLock = new ReadLock(this);
        writeLock = new WriteLock(this);
    }

    /*继承AQS*/
    private abstract static class MySync extends MyAbstractQueueSynchronizer{
        /*state 是32位的 所以我们设计 到16存读锁, 低16位存写锁*/
        private static int SHARED_SHFT = 16; //读状态占用的位数(高16位是读,低16位是写)
        private static int SHARED_UNIT = 1 << SHARED_SHFT; //共享锁加一的单位
        private static int MAX_COUNT = (1 << SHARED_SHFT - 1); //读状态或者写状态的最大数量
        private static int EXCLUSIVE_MASK = (1 << SHARED_SHFT) - 1;


        /*读锁次数*/
        static int sharedCount(int state) {
            return state >> SHARED_SHFT;
        }

        /*写锁次数*/
        static int exclusiveCount(int state) {
            return state & EXCLUSIVE_MASK;
        }
        
        /*共享锁获取读锁*/
        @Override
        protected int tryAquireShared(int premits) {
            return false;
        }

        /*获取写锁 --独占锁*/
        @Override
        protected boolean tryAcquire(int acquire) {
            return false;
        }
        
        /*共享锁释放锁  锁的释放是没有公平和非公平的*/
        @Override
        protected boolean tryReleaseShared(int i) {
            return true;
        }

        /*独占锁释放锁  写锁*/
        @Override
        protected boolean tryRelease(int i) {
            
            return true;
        }
        
        /*读锁是不是应该阻塞  ---实现公平和非公平*/
        protected abstract boolean readShouldBlock();

        /*写锁是不是应该阻塞 --实现公平和非公平*/
        protected abstract boolean writeShouldBlock(); 
    }
    
    /*公平*/
    private static class FairSync extends MySync{
        
        /*公平锁 队列里面又node 当前线程就不应该去获取锁*/
        @Override
        protected boolean readShouldBlock() {
            return hasQueuePred();
        }
        
        @Override
        protected boolean writeShouldBlock() {
            return hasQueuePred();
        }
    }
    
    /*非公平*/
    private static class UnFairSync extends MySync{
        
        /*非公平 为了避免锁饥饿需要判断CLH队列里面里面的下一个节点是不是写线程的(独占模式)*/
        @Override
        protected boolean readShouldBlock() {
            return false;
        }

        /*非公平的写锁(独占模式)其实不需要这个方法,直接cas就行, 所以返回false*/
        @Override
        protected boolean writeShouldBlock() {
            return false;
        }
    }
    
    
    private static class ReadLock  implements Lock{
        private MySync mySync;

        public ReadLock(MyReentrantReadWriteLock myReentrantReadWriteLock) {
            this.mySync = myReentrantReadWriteLock.mySync;
        }

        @Override
        public void lock() {
            /*共享加锁*/
            mySync.acquireShared(1);
        }

        @Override
        public void lockInterruptibly() throws InterruptedException {

        }

        @Override
        public boolean tryLock() {
            return false;
        }

        @Override
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            return false;
        }

        @Override
        public void unlock() {
            mySync.acquireShared(1);
        }

        @Override
        public Condition newCondition() {
            return null;
        }
    }
    
    private static class WriteLock implements Lock{

        private MySync mySync;

        public WriteLock(MyReentrantReadWriteLock myReentrantReadWriteLock) {
            this.mySync = myReentrantReadWriteLock.mySync;
        }

        @Override
        public void lock() {
            /*独占*/
            mySync.acquire(1);
        }

        @Override
        public void lockInterruptibly() throws InterruptedException {

        }

        @Override
        public boolean tryLock() {
            return false;
        }

        @Override
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            return false;
        }

        @Override
        public void unlock() {
            mySync.release(1);
        }

        @Override
        public Condition newCondition() {
            return null;
        }
    }
    
    /*获取读锁*/
    @Override
    public Lock readLock() {
        return null;
    }

    /*获取写锁*/
    @Override
    public Lock writeLock() {
        return null;
    }
}

像是用了外观门面模式? 提供了易用的api给用户。。。。。

写锁获取锁逻辑-- 可重入的独占锁

重入 或者 state为0也就是没有任何线程没有拥有锁, 此时就可以cas去竞争state获取写锁

/*获取写锁 --独占锁*/
        @Override
        protected boolean tryAcquire(int acquire) {
            int state = getState();
            /*判断有没有线程获取了锁*/
            if (state != 0){
                //获取锁的线程必须是写锁, 而且必须是自己
                if (exclusiveCount(state) == 0  //获取到了读锁 
                        || Thread.currentThread() != getOwnThread()){//写锁但是锁的拥有这不是自己
                    return false;
                }
                /*重入*/
                if (exclusiveCount(state) + 1 > MAX_COUNT) throw new IllegalMonitorStateException("重入次数超过限制");
                setState(state + 1);
            }else {
                //说明没有线程获取锁了
                if (!writeShouldBlock() //实现公平的抽象方法,具体由公平锁和非公平锁去实现这个抽象方法
                        && !compareAndSetState(state, state + acquire)){
                    return false;
                }
                /*设置当前线程为写锁获取者*/
                setOwnThread(Thread.currentThread());
            }
            return true;
        }

这里面的两个if条件逻辑值得好好思考,铭记一点||要去到右边,前面的条件是不成立的

写锁释放锁逻辑 -可重入独占锁

 /*独占锁释放锁  写锁*/
        @Override
        protected boolean tryRelease(int acquire) {
            //判断当前线程是不是写锁持有者
            if (Thread.currentThread() != getOwnThread()){
                throw new IllegalMonitorStateException("不是当前锁的持有者不可以释放锁");
            }
            int state = getState();
            state--;
            boolean flag = false;
            if (exclusiveCount(state) < 0){
                throw new IllegalMonitorStateException("释放错误");
            }
            //说明完全释放锁了
            if (exclusiveCount(state) == 0){
                setOwnThread(Thread.currentThread());
                //释放锁
                flag = true;
            }
            setState(state); /*独占锁释放锁不需要cas 不存在竞争*/
            return flag;
        }

代码还是比较直观的。。。

读锁 – 可重入共享锁

为啥要重新起一个字标题, 呃, 读锁真的挺难的。。。很复杂。 还是有需要需要注意的地方

读锁的可重入怎么弄

写锁的话应为是独占锁的,所以state的低16位就只会有一个线程拥有,所以重入直接累加就行。而读锁则不一样, 因为读锁作为共享锁,s获取锁的线程数量以及获取锁的线程重入读锁的次数都会往state的高16位存, 这样就由问题了,无法区分每个读线程锁的重入次数,释放锁的时候就无法做判断什么时候改线程的锁需要释放掉(重入次数为0就释放), 所以我们需要每个线程都能够记住自己的读锁重入次数, 那么就想到可以用## 读锁的可重入怎么标记
呃, state虽然记录了所有线程和重入次数,但是具体是哪个线程重入了几次是不知道的,所以重入次数应该没个入队线程自己保存自己的重入次数,所以使用ThreadLocal。那ThreadLocal里面存的是啥 直接存一个in重入次数吗。。。,
思考: 呃其实代码里面是做了一个优化,就是他会记录第一次获取读锁的线程和重入次数, 以及最近获取读锁的线程,为什么这样,我猜想,如果每次都去ThreadLocal里面取,效率应该比较低,应该Thread对象里面的ThreadLocalMap是一个数组,遍历的实际复杂度就是O(n), 所以记录第一次和最近一次的,算是一种优化吧, 如果都不是那么就去自己的ThreadLocalMap里面遍历找

Thread.java文件的成员属性:

ThreadLocal.ThreadLocalMap threadLocals = null; //ThreadLocalMap 里面有个Entry<ThreadLocal, T> []

所以线程计数类结构如下:

//线程计数器结构
        static final class HoldCounter {
            int count = 0; /*默认重入次数为0*/
            // Use id, not reference, to avoid garbage retention 这里使用tid,不引用线程,是为了避免无法垃圾回收线程对象
            final long tid = getThreadId(Thread.currentThread()); /*获取当前线程的id*/
        }

ThreadLocal
呃,为了初始化方便,继承了ThreadLocal重新了初始化方法,这样get的时候如果ThreadLocalMap里面的Entry数组如果没有找到,就会塞入调用初始化方法得到的值。

//ThreadLocal 重写初始化方法
        static final class ThreadLocalHoldCounter
                extends ThreadLocal<HoldCounter> { /*指定初始化对象的值*/
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }
        private transient ThreadLocalHoldCounter readHolds;/*读锁的threadlcoal*/

记录每次第一次获取读锁的线程 (即把读锁的数量从0 设置为 1的线程,)

private transient Thread firstReader = null; /*把读锁的数量从0 -> 1 的那个线程*/
private transient int firstReaderHoldCount; /*获取锁(重入次数)*/

最近一次获取读锁的Holder

 private transient HoldCounter cachedHoldCounter; /*最近一次获取读锁的线程计数器*/
补:getThreadId 方法说明
/*Thread 类里面的线程id 属性偏移量*/
    private static long tidAddr;
    static {
        try {
            tidAddr = MyJUCUtil.getUnsafe().objectFieldOffset(Thread.class.getDeclaredField("tid"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
    
    private static long getThreadId(Thread thread){
        //tid 不是volatile修饰的, 不具有可见性,使用unsafe魔法的volatile方法,看作是volatile的获取。
        return MyJUCUtil.getUnsafe().getLongVolatile(thread, tidAddr);
    }

Thread.java里面 tid不是volatile修饰 不具有可见性

/*
     * Thread ID
     */
    private long tid;

读锁的获取锁 可重入的共享锁 tryAquireShared

麻了,如果拥有写锁,再来获取读锁这种情况是可以的,也就是开头说的锁降级,所以如果有写线程获取写锁了,并且是写锁线程来获取了读锁,是允许的

protected int tryAquireShared(int premits) {
            /*获取最新的state*/
            int state = getState();
            //读锁数量
            int readCount = sharedCount(state);
            //如果有写锁 ,但是这些写锁拥有者不是自己, 不是锁降级情况
            if (exclusiveCount(state) != 0 && getOwnThread() != Thread.currentThread()){
                return -1;
            }
            
            if (!readShouldBlock() //是否应该阻塞, 公平的话看队列有没有元素,非公平看下一个节点是不是独占
                    &&readCount < MAX_COUNT //读锁数量合法
                    &&compareAndSetState(state, state + SHARED_UNIT)){ //cas 共享锁竞争成功
                //说明读锁数量从 0 -> 1
                if (state == 0){
                    firstReader = Thread.currentThread();
                    firstReaderHoldCount = 1;
                    //firstReader重入
                }else if (firstReader == Thread.currentThread()){
                    cacheHolderCounter.count++;
                    //说明是中间线程重入或者是该线程第一次来获取读锁  怎么理解中间状态:就是firstReader和cacheHolderCounter之间获取读锁的那些线程
                }else {
                    HoldCounter hc = cacheHolderCounter;
                    //这里有个特殊点就是如果是cacheHolderCounter的线程重入, 需要做一个特殊处理,
                    //特殊点就是 最近获取锁的线程完全释放锁之后自己的threadlocal是会进行remove的,但是cacheHolderCounter这个引用是不清除的
                    if (hc == null //说明是第二个线程来获取读锁 
                            || hc.tid != getThreadId(Thread.currentThread())){//说明不是cacherHolderCounter重入
                        cacheHolderCounter = hc = readHolds.get(); //当前线程将要变成最近获取锁的
                    }else if (hc.count == 0){ //说明最近获取锁的线程已经完全释放了锁,自己threalocal已经已移除了,但是现在又重新来获取锁
                        readHolds.set(hc); //重新set回去到threadlocal
                    }
                    //重入次数
                    hc.count++;/*注: 说实话这里有很多地方有些地方需要深思熟虑的地方......*/
                }
                return 1;
            }
            //快速竞争失败,共享锁进入循环一直竞争。
            return fullTryAcquireShared(Thread.currentThread());
        }

注意:
问题1、为啥firstReader、cacheHolderCounter 、count 等等这些东西都不用volatile修饰???,可见性如何保证???
问题2 为啥hc = cacheHolderCounter, 为啥不直接使用cacherHolderCounter?

不知道大家对这两个问题心里没有答案。。。。。。
我就说一些我的理解吧:
首先要知道可见性那些东西可以保证,JMM的happen-before原则可见性都可以保证。
JMM的底层实现就是加屏障。那些可以加屏障呢? 锁、volatile、cas、final 这些都会加入屏障避免编译器和处理器的指令重排导致可见性的问题。
我们可以看tryAcquireShared里面的方法逻辑,从头到尾,在使用没有用volatile修饰变量之前都是要先cas成功之后才会去使用这些变量,这样做就可以保证可见性, cas的底层使用lock原子啥指令的保证内存的一致性。。。 所以问题1可以回答了。。。,

那第二个问题呢?为啥要使用一个hc局部引用呢? 这是为了避免每次读firstReader等都不一样, 整个方法逻辑是没有锁的,所以存在多线程并发,但是cacheHolderCounter只关系谁是最后获取重新给cacheHolderCounter赋值的,所以我要用个局部变量取记录本次线程的最近以此获取锁的。这样多线程并发下就可以竞争,最后赋值的就是最近cacherHolderCounter。。。这里有点难理解,以上是我的思考,慎用。。。

共享锁循环尝试获取锁 fullTryAcquireShared

读锁是可以一直尝试获取锁的,因为读读之间是不互斥的,但是state的修改又同一时刻只能成功一个,所以需要循环保证失败的线程还能再次去竞争。

private  int fullTryAcquireShared(Thread thread){
            //共享锁获取锁
            HoldCounter hc = null; //注意思考定义整个局部变量的含义是什么,卧槽每个线程都会在自己的线程栈创建一个,所以就不需要每次都去从threadlocal数组里面去遍历。。。
            for (;;){
                int c = getState();
                //有写锁
                if (exclusiveCount(c) != 0){
                    //不是锁降级
                    if (getOwnThread() != Thread.currentThread()){
                        return -1;
                    }
                    //是否应该阻塞
                }else if (readShouldBlock()){
                    //应该阻塞,但是阻塞那些呢? 即不是重入的那些线程
                    if (firstReader == Thread.currentThread()){
                        
                    }else {
                        if (hc == null){
                            hc = cacheHolderCounter;
                            if (hc == null //说明是第二个线程来获取读锁
                                    || hc.tid == getThreadId(thread)){
                                hc = readHolds.get();
                                if (hc.count == 0){ /*如果线程是第一次 尝试获取读锁,那他的threadlocalmap里面是没有计数器,但是这里get之后会给他一个初始化的计数器,所以如果不是重入需要给它移除掉*/
                                    readHolds.remove();//移除   
                                }
                            }
                        }
                        //不是重入不允许获取锁了
                        if (hc.count == 0){
                            return -1;
                        }
                    }
                }
                
                if (sharedCount(c) == MAX_COUNT) /*太多了*/
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) {/*开始了cas获取锁*/
                    if (sharedCount(c) == 0) { /*说明是第一次获取读锁*/
                        firstReader = thread;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == thread) {
                        firstReaderHoldCount++;
                    } else {
                        if (hc == null)
                            hc = cacheHolderCounter;
                        if (hc == null || hc.tid != getThreadId(thread))
                            hc = readHolds.get();
                        else if (hc.count == 0)
                            readHolds.set(hc);
                        hc.count++;
                        cacheHolderCounter = hc; // cache for release
                    }
                    return 1;
                }
            }
        }

apparentlyFirstQueuedIsExclusive判断队列里面有没有独占节点写节点

final boolean apparentlyFirstQueuedIsExclusive() {
        Node h, s;
        return (h = head) != null && //队列已经初始化
            (s = h.next)  != null && //头节点的next不为空,说明CLH有入队节点
            !s.isShared()         && //不是共享模式的节点
            s.thread != null; //线程不能为空,该节点没有被唤醒获取锁。。
    }

读线程共享锁释放读锁 tryReleaseShared

protected final boolean tryReleaseShared(int unused) { /*释放读锁有两个流程  一个是重入数 一个是state的高16位*/
            Thread current = Thread.currentThread(); /*获取当前线程*/
            if (firstReader == current) {  /*如果当前线程是把 state  从  0 - > 1 的线程*/
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1) /*获取锁的次数*/
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get(); /*说明当前线程 不是最近一个获取读锁的线程,需要从自己线程空间threadlocalMap里面获取 计数器*/
                int count = rh.count;
                if (count <= 1) { /*说明线程这次释放锁是干干净净的,没有重入*/
                    readHolds.remove(); /*释放了锁 需要把 它从自己的threadlocalmap里面移除*/
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count; /*次数--*/
            }
            for (;;) {  /*cas需改state的值, 因为读锁是共享锁, 会有多个线程同时释放锁*/
                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; /*读锁释放是否完成*/
            }
        }

整体逻辑上不怎么绕, 优化点就是先把线程自身的ThreadLocal进行减-1, 这样每个线程存在一份,是没有线程竞争问题, 最后再来cas 修改state, 如果为空0说明读锁已经没有了,可以释放读锁。 去唤醒CLH队列的节点, 我理解 整个节点只可能是写节点

总结

呃, 读写锁还是很难的,组合了独占模式和共享模式。 在学习读写锁之前都要对独占模式和共享模式的模板方法进行学习。要不然整个逻辑串不起来的。 所以学习读写锁的前提就是先学习ReentractLock(独占)、Semaphore(共享)、CountDownLatch(共享)、以及JUC并发工具包如何实现内存一致性(通过cas和volatile), 所以在学习JUC之前先的把多线程基础学习一下,特别是深入理解JMM内存模型。。。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值