AQS(AbstractQueuedSynchronizer)源码(二)读写锁

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_42011541/article/details/85538124

这一次基于读写锁的讲解

共享模式

这里基于ReentrantReadWriteLock讲解
ReentrantReadWriteLock 管理了一个读锁 和 一个写锁
其中读-读共享, 读-写互斥(独占), 写-写(互斥)
因为在并发读和写的时候容易发生脏读问题
至于为什么使用ReentrantReadWriteLock我们大部分是使用一把锁, 读和写都锁起来
但是 如果只读的话对数据没有什么影响, 读操作并不影响数据的安全性
不可变的状态一定是线程安全的!
而且现在的应用其实80%的时间都是在去读数据, 如果使用读写锁会使性能更好一些.

    public interface ReadWriteLock {
    /**
     * 返回读锁
     */
    Lock readLock();

    /**
     * 返回写锁
     */
    Lock writeLock();
}

ReetrantReadWriteLock实现了ReadWriteLock,并使其可重入

这里主要讲解读写锁的读锁, 因为其实写锁也是独占锁 和上述的锁是差不多的 就是多了一个判断是否有读锁, 也就是读写互斥

读写锁的状态保存也是一个int型的变量 但是不同的是, 他采用分位保存状态, 什么意思呢?
一个int类型32位, 用高16位保存读锁状态, 低16位保存写锁状态.
在<<JAVA并发编程艺术中>>有这样一个图
在这里插入图片描述
https://blog.csdn.net/qq_42011541/article/details/85534378 分析了独占模式的锁, 他保存了独占的线程和他的状态, 以及一个同步队列.
这样就可以对这个绑定的线程和状态进行一系列的操作.
那么读锁其实也是这样的 他绑定很多的线程和他们获取锁的数量
有人肯定会想会不会用容器去保存呢. 其实每个线程都有一个ThreadLocal, 用这个去保存线程

 static final class HoldCounter {
            int count;          // 初始值为0
            final long tid = LockSupport.getThreadId(Thread.currentThread());
        }

        static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }

这样就可以清晰的保存每个获取读锁的状态了
当然还有两个优化手段 就是保存第一个线程的状态 和最后一个线程的状态

		private transient HoldCounter cachedHoldCounter;//最后一个线程的缓存状态

        private transient Thread firstReader;//第一个线程
        private transient int firstReaderHoldCount;//第一个线程获得读锁的数量

下面先记几个内部保存的状态的操作

		static final int SHARED_SHIFT   = 16;
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

       //读锁的数量因为是高16位, 把整体无符号右移就可以把低16位挤出去就获得了高16位
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        //写锁数量因为低16位和 0x0000FFFF 按位&运算就可以得到低16位
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

读锁的获取锁方法

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

其实发现和之前独占锁的流程都差不多. 这里主要是想自定义共享锁重写tryAcquireShared方法, 如果返回>=0的值获取锁成功 如果 返回 < 0 则需要排队申请

protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
            //exclusiveCount(c) != 0代表有写锁占用 并且不是当前线程获取锁失败,返回-1
            //如果是当前线程的话 需要进行锁降级.
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            int r = sharedCount(c);//获得读锁的数量
            //如果不需要挂起 并且读锁的数量小于最大数量(65535) 并且更新读锁数量成功
            //CAS设置读锁数量为什么要加SHARED_UNIT(这个值在上面是65536)因为读锁是高16位
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                if (r == 0) {//这里代表第一个读线程
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {//第一个获取读锁的线程等于当前线程
                    firstReaderHoldCount++;
                } else {
	                //否则的话先拿缓存的最后一个
	                //如果是了 就让缓存里的值++
	                //不的是话再获取ThreadLocal里的值
	                //如果没有则创建, 有的话就把数量++
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null ||
                        rh.tid != LockSupport.getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            //如果上述失败了, 要么是公平的可能需要等待, 要么发生并发CAS操作失败了
            //之前也讲过 如果两个线程同时进行CAS操作就会失败,
            //所以这里也有可能是两个线程同时申请读锁
            return fullTryAcquireShared(current);
        }

锁降级

锁降级也就是说写锁可以降级为读锁的
一个线程先获得了写锁 , 再获取读锁, 最后释放写锁称之为锁降级
这个是非常有必要的, 因为可能会存在一个可见性的问题
如果当前线程获取了写锁, 他能不能拿到读锁呢? 能不能可重入读锁呢?
锁是为了保证操作的原子性和可见性, 如果有很多线程, 但是操作的只有线程A, 其他线程都在等待获取操作权限, 是不是就原子性,可见性了
所以答案也是肯定的, 你单个线程 无论是读还是写都不会存在并发的问题,所以是可以重入的
锁降级一个是为了性能, 一个是为了可见性
假如当前线程 A 不获取读锁而是直接释放了写锁,这个时候另外一个线程 B 获取了写锁,那么这个线程 B 对数据的修改是不会对当前线程 A 可见的。如果获取了读锁,则线程B在获取写锁过程中判断如果有读锁还没有释放则会被阻塞,只有当前线程 A 释放读锁后,线程 B 才会获取写锁成功.
在第一种情况很有可能数据是有问题的, 第二种情况 又使性能变差.

final int fullTryAcquireShared(Thread current) {
          //又是一次自旋尝试
            HoldCounter rh = null;
            for (;;) {
                int c = getState();
                //这里判断是否有写锁, 并且看一下是否要进行锁降级
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                    // 读锁需要阻塞,判断是否当前线程已经获取到读锁
                } else if (readerShouldBlock()) {
                    // Make sure we're not acquiring read lock reentrantly
                    if (firstReader == current) {
                        // assert firstReaderHoldCount > 0;
                    } else {
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null ||
                                rh.tid != LockSupport.getThreadId(current)) {
                                rh = readHolds.get();
                                if (rh.count == 0)//计数器等于0 说明没有获得读锁, 清空缓存
                                    readHolds.remove();
                            }
                        }
                        if (rh.count == 0)
                            return -1;
                    }
                }
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                    //CAS设置成功, 后面的操作和之前一样
                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 != LockSupport.getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
        }

对于公平的锁, 读锁的方法readerShouldBlock 判断线程是否要挂起, 只需要判断队列中是否不为空, 如果有结点,说明需要排队

final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }

对于非公平的锁, 读锁的方法readerShouldBlock 判断线程是否要挂起
该方法如果头节点不为空,并头节点的下一个节点不为空,不是共享模式、线程不为空。则返回true,说明有当前申请读锁的线程占有写锁,并有其他写锁在申请。为什么要判断head节点的下一个节点不为空,或是thread不为空呢?因为第一个节点head节点是当前持有写锁的线程,也就是当前申请读锁的线程,这里,也就是锁降级的关键所在,如果占有的写锁不是当前线程,那线程申请读锁会直接失败

final boolean readerShouldBlock() {
            return apparentlyFirstQueuedIsExclusive();
        }
final boolean apparentlyFirstQueuedIsExclusive() {
        Node h, s;
        return (h = head) != null &&
            (s = h.next)  != null &&
            !s.isShared()         &&
            s.thread != null;
    }

读锁释放锁

public void unlock() {
            sync.releaseShared(1);
        }

释放锁的方法还是比较简单的

protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            //判断第一个是不是当前的线程
            if (firstReader == current) {
                //如果锁状态数量只有1了 要把这个释放了 就设置为null
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else//否则的话让数量-1就可以
                    firstReaderHoldCount--;
            } else {
		        //如果不是当前线程,看是否的缓存的最后一个线程
                HoldCounter rh = cachedHoldCounter;
                //如果不是的话 就从ThreadLocal中获取
                if (rh == null ||
                    rh.tid != LockSupport.getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            //自旋CAS去设置状态
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
展开阅读全文

没有更多推荐了,返回首页