前言

上篇文章分析了AQS的实际应用之一:ReentrantLock 的实现。ReentrantLock 和synchronized 都是独占锁,而AQS还支持共享锁,本篇就来分析AQS 共享锁的实际应用。

文章目录:

1、共享锁、独享锁区别

2、读锁的实现原理

3、写锁的实现原理

4、读写锁 tryLock 原理

5、读写锁的应用

1、共享锁、独享锁区别

基本差别

共享锁、独占锁是在AQS里实现的,核心是"state"的值:

image.png

如上图,对于共享锁来说,允许多个线程对state进行有效修改。

读写锁的引入

根据上面的图,state 同时只能表示一种锁,要么独占锁,要么共享锁。而在实际的应用场景里经常会碰到多个线程读,多个线程写的情况,此时为了能够协同读、写线程,需要将state改造。

先来看AQS state 定义:

#AbstractQueuedSynchronizer.java

private volatile int state;

可以看出是int 类型的(当然也有long 类型的,在AbstractQueuedLongSynchronizer.java 里,本文以int 为例)

image.png

state 被分为两部分,低16位表示写锁(独占锁),高16位表示读锁(共享锁),这样一个32位的state 就可以同时表示共享锁和独占锁了。

2、读锁的实现原理

ReentrantReadWriteLock 的构造

ReentrantReadWriteLock 并没有像ReentrantLock一样直接实现Lock 接口,而是内部分别持有ReadLock、WriteLock类型的成员变量,两者均实现了Lock 接口。

#ReentrantReadWriteLock.java

    public ReentrantReadWriteLock() {

        //默认非公平锁

        this(false);

    }

    public ReentrantReadWriteLock(boolean fair) {

        sync = fair ? new FairSync() : new NonfairSync();

        //构造读锁

        readerLock = new ReadLock(this);

        //构造写锁

        writerLock = new WriteLock(this);

    }

ReentrantReadWriteLock 默认实现非公平锁,读锁、写锁支持非公平锁和公平锁。

读写锁构造之后,将锁暴露出来给外部使用:

#ReentrantReadWriteLock.java

    //获取写锁对象

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

    //获取读锁对象

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

获取锁

在ReentrantLock 分析独占锁时有如下图:

image.png

与独占锁类似,AQS虽然已经实现了共享锁的基本逻辑,但是真正获取锁、释放锁的操作还是需要子类实现,共享锁需要实现方法:

tryAcquireShared & tryReleaseShared

来看看获取锁的过程:

#ReentrantReadWriteLock.ReadLock

    public void lock() {

            //共享锁

            sync.acquireShared(1);

        }

#AbstractQueuedSynchronizer.java

    public final void acquireShared(int arg) {

        if (tryAcquireShared(arg) < 0)

            //doAcquireShared 在AQS里实现

            doAcquireShared(arg);

    }    

重点是tryAcquireShared(xx):

#ReentrantReadWriteLock.java

        protected final int tryAcquireShared(int unused) {

            Thread current = Thread.currentThread();

            //获取同步状态

            int c = getState();

            //此处exclusiveCount作用是取state 低16位,若是不等于0,说明有线程占有了写锁

            //若是有线程占有了写锁,而这个线程不是当前线程,则直接退出------------>(1)

            if (exclusiveCount(c) != 0 &&

                getExclusiveOwnerThread() != current)

                return -1;

            //获取state 高16位,若是大于0,说明有线程占有了读锁

            int r = sharedCount(c);

            //当前线程是否应该阻塞

            if (!readerShouldBlock() &&//------------>(2)

                r < MAX_COUNT &&//若是不该阻塞,则尝试CAS修改state高16位的值

                compareAndSetState(c, c + SHARED_UNIT)) {

                //--------记录线程/重入次数----------->(3)

                //修改state 成功,说明成功占有了读锁

                if (r == 0) {

                    //记录第一个占有读锁的线程

                    firstReader = current;

                    //占有次数为1

                    firstReaderHoldCount = 1;

                } else if (firstReader == current) {

                    //第一个占有读锁的线程重入了该锁

                    firstReaderHoldCount++;

                } else {

                    //是其它线程占有锁

                    //取出缓存的HoldCounter

                    HoldCounter rh = cachedHoldCounter;

                    //若是缓存为空,或是缓存存储的不是当前的线程

                    if (rh == null || rh.tid != getThreadId(current))

                        //从threadLocal里获取

                        //readHolds 为ThreadLocalHoldCounter 类型,继承自ThreadLocal

                        cachedHoldCounter = rh = readHolds.get();

                    else if (rh.count == 0)

                        //说明cachedHoldCounter 已经被移出threadLocal,

                        //重新加入即可------------>(4)

                        readHolds.set(rh);

                    //记录重入次数

                    rh.count++;

                    //--------记录线程/重入次数-----------

                }

                return 1;

            }

            //------------>(5)

            return fullTryAcquireShared(current);

        }

以上是获取读锁的核心代码,标注了5个重点,分别来分析。

(1)

此处表明了一个信息:

若是当前线程已经获取了写锁,那么它可以继续尝试获得读锁。

当它把写锁释放后,只剩读锁了。这个过程可以理解为锁的降级。

(2)

线程能否有机会获取读锁,还需要经过两个判断:

1、判定readerShouldBlock()。

2、判定读锁个数用完了没,阈值是2^16-1。

而读锁公平与否就体现在readerShouldBlock()的实现上。

先来看非公平读锁:

#ReentrantReadWriteLock.java

        final boolean readerShouldBlock() {

            return apparentlyFirstQueuedIsExclusive();

        }

#AbstractQueuedSynchronizer.java

       final boolean apparentlyFirstQueuedIsExclusive() {

           //判断等待队列里的第二个节点是否在等待写锁

        Node h, s;

        return (h = head) != null &&

            (s = h.next)  != null &&

            !s.isShared()         &&

            s.thread != null;

    }

若等待队列里的第二个节点是在等待写锁,那么此时不能去获取读锁。

这与ReentrantLock不一样,ReentrantLock 非公平锁的实现是不管等待队列里有没有节点,都会去尝试获取锁。

再来看公平读锁

#ReentrantReadWriteLock.java

        final boolean readerShouldBlock() {

            return hasQueuedPredecessors();

        }

判断队列里是否有更早于当前线程排队的节点,该方法在上篇分析ReentrantLock 时有深入分析,此处不再赘述。

(3)

这部分代码看起来多,实际上就是为了记录重入次数以及为了效率考虑引入了一些缓存。

考虑到有可能始终只有一个线程获取读锁,因此定义了两个变量还记录重入次数:

#ReentrantReadWriteLock.java

        //记录第一个获取读锁的线程

        private transient Thread firstReader = null;

        //第一个获取读锁的线程获取读锁的个数

        private transient int firstReaderHoldCount;

再考虑到有多个线程获取锁,它们也需要记录获取锁的个数,与线程绑定的数据我们想到了ThreadLocal,于是定义了:

private transient ThreadLocalHoldCounter readHolds;

来记录HoldCounter(存储获取锁的个数及绑定的线程id)。

最后为了不用每次都去ThreadLocal里查询数据,再定义了变量来缓存HoldCounter:

#ReentrantReadWriteLock.java

private transient HoldCounter cachedHoldCounter;

(4)

cachedHoldCounter.count == 0,是在tryReleaseShared(xx)里操作的,并且判断当线程已经彻底释放了读锁后,将HoldCounter 从ThreadLocal里移除,因此此处需要加回来。

(5)

走到这一步,说明之前获取锁的操作失败了,原因有三点:

1、readerShouldBlock() == true。

2、r >= MAX_COUNT。

3、中途有其它线程修改了state。

fullTryAcquireShared(xx)与tryAcquireShared(xx)很类似,目的就是为了获取锁。

针对第三点,fullTryAcquireShared(xx)里有个死循环,不断获取state值,若是符合1、2点,则退出循环,否则尝试CAS修改state,若是失败,则继续循环获取state值。

小结一下:

1、fullTryAcquireShared(xx) 获取锁失败返回-1,接下来的处理逻辑流转到AQS里,线程可能会被挂起。

2、fullTryAcquireShared(xx) 获取锁成功则返回1。

释放锁

释放锁的逻辑比较简单:

#ReentrantReadWriteLock.ReadLock

    public void lock() {

            sync.acquireShared(1);

        }

#AbstractQueuedSynchronizer.java

    public final boolean releaseShared(int arg) {

        if (tryReleaseShared(arg)) {

            //在AQS里实现

            doReleaseShared();

            return true;

        }

        return false;

    }

重点是tryReleaseShared(xx):

#ReentrantReadWriteLock.java

        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))

                    //取不到,则需要从ThreadLocal里取

                    rh = readHolds.get();

                int count = rh.count;

                if (count <= 1) {

                    //若是当前线程不再占有锁,则清除对应的ThreadLocal变量

                    readHolds.remove();

                    if (count <= 0)

                        throw unmatchedUnlockException();

                }

                --rh.count;

            }

            for (;;) {

                //修改state

                int c = getState();

                int nextc = c - SHARED_UNIT;

                if (compareAndSetState(c, nextc))

                    //若是state值变为0,说明读锁、写锁都释放完了

                    return nextc == 0;

            }

        }

此处需要注意的是:

tryReleaseShared(xx)释放读锁时候,若是没有完全释放读锁、写锁,那么将会返回false。

而在AQS里释放共享锁流程如下:

#AbstractQueuedSynchronizer.java

    public final boolean releaseShared(int arg) {

        if (tryReleaseShared(arg)) {

            doReleaseShared();

            return true;

        }

        return false;

    }

也就是说此种情况下,doReleaseShared() 将不会被调用,也就不会唤醒同步队列里的节点。

这么做的原因是:

若只释放完读锁,还剩写锁被占用。而因为写锁是独占锁,其它线程无法获取锁,那么即使唤醒了它们也没有用。

3、写锁的实现原理

获取锁

写锁是独占锁,因此重点关注tryAcquire(xx):

#ReentrantReadWriteLock.java

        protected final boolean tryAcquire(int acquires) {

            Thread current = Thread.currentThread();

            //获取同步状态

            int c = getState();

            //获取当前写锁个数

            int w = exclusiveCount(c);

            if (c != 0) {

                //1、若是w==0,而c!= 0,说明有线程占有了读锁,不能再获取写锁了

                //2、若是写锁被占用,但是不是当前线程,则不能再获取写锁了

                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,此时读锁、写锁都没线程占用

               //判断线程是否应该被阻塞,否则尝试获取写锁------->(1)

            if (writerShouldBlock() ||

                !compareAndSetState(c, c + acquires))

                return false;

            //独占锁需要关联线程

            setExclusiveOwnerThread(current);

            return true;

        }

来看看writerShouldBlock(),写锁公平/非公平就在此处实现的。

先来看非公平写锁:

#ReentrantReadWriteLock.java

        final boolean writerShouldBlock() {

            //不阻塞

            return false; // writers can always barge

        }

非公平写锁不应该阻塞。

再来看公平写锁:

#ReentrantReadWriteLock.java

        final boolean writerShouldBlock() {

            //判断队列是否有有效节点等待

            return hasQueuedPredecessors();

        }

和公平读锁一样的判断条件。

小结

1、读锁/写锁 已被其它线程占用,那么新来的线程将无法获取写锁。

2、写锁可重入。

释放锁

释放锁重点关注tryRelease(xx):

##ReentrantReadWriteLock.java

        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;

        }

若tryRelease(xx)返回true,则AQS里会唤醒等待队列的线程。

4、读写锁 tryLock 原理

读锁tryLock

#ReentrantReadWriteLock.java

        public boolean tryLock() {

            return sync.tryReadLock();

        }

        final boolean tryReadLock() {

            Thread current = Thread.currentThread();

            for (;;) {

            //for 循环为了检测最新的state

                int c = getState();

                if (exclusiveCount(c) != 0 &&

                    getExclusiveOwnerThread() != current)

                    return false;

                int r = sharedCount(c);

                if (r == MAX_COUNT)

                    throw new Error("Maximum lock count exceeded");

                if (compareAndSetState(c, c + SHARED_UNIT)) {

                //记录次数

                    if (r == 0) {

                        firstReader = current;

                        firstReaderHoldCount = 1;

                    } 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 true;

                }

            }

        }

可以看出tryReadLock(xx)里:只要不是别的线程占有写锁并且读锁个数没超出限制,那么它将一直尝试获取读锁,直到得到为止。

写锁tryLock

        public boolean tryLock() {

            return sync.tryWriteLock();

        }

        final boolean tryWriteLock() {

            Thread current = Thread.currentThread();

            int c = getState();

            if (c != 0) {

                int w = exclusiveCount(c);

                if (w == 0 || current != getExclusiveOwnerThread())

                    return false;

                if (w == MAX_COUNT)

                    throw new Error("Maximum lock count exceeded");

            }

            if (!compareAndSetState(c, c + 1))

                return false;

            setExclusiveOwnerThread(current);

            return true;

        }

写锁只尝试一次CAS,失败就返回。

最终,用图表示读锁、写锁实现的功能:

image.png

读锁与写锁关系:

image.png

5、读写锁的应用

分析完原理,来看看简单应用。

public class TestThread {

    static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();

    static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

    public static void main(String args[]) {

        //读

        for (int i = 0; i < 10; i++) {

            new Thread(new Runnable() {

                @Override

                public void run() {

                    String threadName = Thread.currentThread().getName();

                    try {

                        System.out.println("thread " + threadName + " acquire read lock");

                        readLock.lock();

                        System.out.println("thread " + threadName + " read locking");

                        Thread.sleep(1000);

                    } catch (InterruptedException e) {

                        e.printStackTrace();

                    } finally {

                        readLock.unlock();

                        System.out.println("thread " + threadName + " release read lock remain read count:" + readWriteLock.getReadLockCount());

                    }

                }

            }, "" + i).start();

        }

        //写

        for (int i = 0; i < 10; i++) {

            new Thread(new Runnable() {

                @Override

                public void run() {

                    String threadName = Thread.currentThread().getName();

                    try {

                        System.out.println("thread " + threadName + " acquire write lock");

                        writeLock.lock();

                        System.out.println("thread " + threadName + " write locking");

                        Thread.sleep(1000);

                    } catch (InterruptedException e) {

                        e.printStackTrace();

                    } finally {

                        writeLock.unlock();

                        System.out.println("thread " + threadName + " release write lock remain write count:" + readWriteLock.getWriteHoldCount());

                    }

                }

            }, "" + i).start();

        }

    }

}

10个线程获取读锁,10个线程获取写锁。

读写锁应用场景:

ReentrantReadWriteLock 适用于读多写少的场景,提高多线程读的效率、吞吐量。

同一线程读锁、写锁关系:

public class TestThread {

    static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();

    static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

    public static void main(String args[]) {

//        new TestThread().testReadWriteLock();------>1、先读锁,后写锁

//        new TestThread().testWriteReadLock();------>2、先写锁、后读锁

    }

    private void testReadWriteLock() {

        System.out.println("before read lock");

        readLock.lock();

        System.out.println("before write lock");

        writeLock.lock();

        System.out.println("after write lock");

    }

    private void testWriteReadLock() {

        System.out.println("before write lock");

        writeLock.lock();

        System.out.println("before read lock");

        readLock.lock();

        System.out.println("after read lock");

    }

}

分别打开1、2 注释,发现:

1、先获取读锁,再获取写锁,则线程在写锁处挂起。

2、先获取写锁,再获取读锁,则都能正常获取锁。

这与我们上述的理论分析一致。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

热门产品

php编程基础教程.pptx|php编程培训,php,编程,基础,教程,pptx
php编程基础教程.pptx

历史上的今天:04月29日

热门专题

云南高职单招|云南单招,云南单招网,云南高职单招网,云南高职单招,云南单招学校,云南单招培训
云南高职单招
卓越综合高中|卓越综合高中
卓越综合高中
APP开发|app开发_app开发公司_app软件开发_专业app开发_云南app开发公司_app定制_原生app开发定制
APP开发
小程序开发|微信小程序,小程序开发,小程序,小程序制作,微信小程序开发,小程序公司,小程序开发公司,分销,三级分销系统,分销系统
小程序开发
云南巨榕教育投资集团有限公司|云南巨榕教育投资集团有限公司,巨榕教育集团,巨榕教育
云南巨榕教育投资集团有限公司
外贸网站建设|外贸网站建设,英文网站制作,英文网站设计,美国主机空间,外贸建站平台,多语言网站制作
外贸网站建设
大理科技管理学校|大理科技管理学校,大理科技,大理科技中等职业技术学校,大理科技管理中等职业技术学校,大理科技学校
大理科技管理学校
安徽开放大学|安徽开放大学报名,安徽开放大学报考,安徽开放大学,什么是安徽开放大学,安徽开放大学学历,安徽开放大学学费,安徽开放大学报名条件,安徽开放大学报名时间,安徽开放大学学历,安徽开放大学专业
安徽开放大学

微信小程序

微信扫一扫体验

立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部