Jdk源码分析

文章40 |   阅读 14793 |   点赞0

来源:https://yumbo.blog.csdn.net/category_10384063.html

以读写锁ReentrantReadWriteLock的读锁为例追踪源码

x33g5p2x  于2021-12-18 转载在 其他  
字(5.9k)|赞(0)|评价(0)|浏览(366)

读写锁适合使用在读多写少的场景,如果写多读少,反而没有可重入锁的效率高,一般而言,能够使用读写改造的情况下,使用读写锁效率会更高。

下面是一个读写锁的读锁使用案例

class ShareData {
    private Integer num = 0;
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    /** * get方法 */
    public Integer getNum() {
        return num;
    }

    public void increment() {
    	lock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 当前值 " + num);
            num++;
            System.out.println(Thread.currentThread().getName() + "\t 更新后的值 " + num);
        } finally {
            lock.readLock().unlock();
        }
    }
}

在构造ReentrantReadWriteLock时,底层会默认创建非公平同步器、读锁、写锁
如下:

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
    private static final long serialVersionUID = -6992448646407690164L;
    private final ReentrantReadWriteLock.ReadLock readerLock; // 读锁
    private final ReentrantReadWriteLock.WriteLock writerLock;// 写锁
    final Sync sync;   // 同步器
    
    /** * 创建默认的非公平锁 */
    public ReentrantReadWriteLock() {
        this(false); // 调用带参数的构造方法
    }
    /** * 创建公平锁/非公平锁、读锁、写锁 */
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync(); // true则是公平锁,false则是非公平锁
        readerLock = new ReadLock(this); // 读锁
        writerLock = new WriteLock(this);// 写锁
    }
}

当我们通过lock.readLock().lock();我们看下他到底做了哪些动作
通过lock.readLock()得到读锁对象,也就是我们构造的时候内部创建的那个对象
写锁也和下面的差不多,都是直接返回对象

public ReentrantReadWriteLock.ReadLock readLock() {
    return readerLock;// 返回读锁
}

然后通过改对象调用lock()对应源码

/** * ReentrantReadWriteLock.readLock()返回读锁的实例 */
public static class ReadLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = -5992448646407690164L;
    private final Sync sync;

    /** * 构造方法 */
    protected ReadLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }

    /** * 尝试获取读锁 */
    public void lock() {
        sync.acquireShared(1); // 调用的是Sync类父类AQS的方法
    }
}

AbstractQueuedSynchronizer 类,尝试抢占读锁,失败则通过acquire进入队列中,acquire方法的讲解可以看:
以ReentrantLock的非公平锁为例深入解读AbstractQueuedSynchronizer源码

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
    /** * 获取共享锁(读锁的lock方法会调用这个方法) */
    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0) // 尝试获取读锁,失败则需要将节点加入aqs队列
            acquire(null, arg, true, false, false, 0L);
    }
}

判断中调用的就是Sync类的方法,如下

读写锁的设计中利用aqs的state(int型数据32位),前面16位用来表示读锁、后面16位表示写锁,如果遇到重入锁就是这各自的16位累加。这也是为什么源码会有下面这些定义

abstract static class Sync extends AbstractQueuedSynchronizer {
       private static final long serialVersionUID = 6317671515068378041L;//序列化版本号
       static final int SHARED_SHIFT = 16;                         // 常量16,目的是将state按位右移16位得到的>值就是读锁的个数
       static final int SHARED_UNIT = (1 << SHARED_SHIFT);         // 2的16次方,实际上表示读锁加的>锁次数是1
       static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;  // 2的16次方再减1,前面16位全0后面16位就是全1,目的就是通过&运算得到写锁的个数
       static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;       // 2的16次方再减1,表示加锁(读/写)最大的计数超过了则抛异常
       private transient Thread firstReader;                       // 第一个获取到读锁的线程
       private transient int firstReaderHoldCount;                 // 第一个线程重入锁的次数计数
       private transient HoldCounter cachedHoldCounter;            // 读锁计数器对象
       private transient ThreadLocalHoldCounter readHolds;         // 在构造Sync的时候就会被赋值,重入>读锁的计数器保持对象(对象中存了获取读锁的次数)
}

读写锁又分读读、读写、写写。后面两种和读排他 是会产生阻塞的情况

abstract static class Sync extends AbstractQueuedSynchronizer {
	/** * 读锁才调用的方法,当前线程尝试获取读锁 */
    @ReservedStackAccess
    protected final int tryAcquireShared(int unused) {
        Thread current = Thread.currentThread(); // 获取当前线程
        int c = getState();// 获取存有读和写锁次数的state值
        /** * 是写锁则进入 */
        // 通过exclusiveCount(c)得到写锁次数,如果不为0则说明加了写锁。加了写锁需要判断当前线程是否是持有写锁的线程,是则不返回-1,不是则说明是写读状态需要进行阻塞当前线程
        if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
            return -1; // 说明是写读状态、返回-1,抢占读锁失败
        // 执行到这里说明前面没有加过写锁,可能加过读锁
        int r = sharedCount(c); // 获取加的读锁次数,r就是read,实际就是将state右移16位得到
        // 到这里说明没有加过锁,到这里c是0,因此进行加锁操作将state更新为读锁的1 实际二进制是:0000 0000 0000 0001 0000 0000 0000 0000
        /** * 是读锁, * 一、读是共享的情况直接执行if内 */
        if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
            if (r == 0) { // 第一次进入,因为能到达这里就说明没有写锁,有判断r==0则说明读锁也为0,则说明是第一次调用
                firstReader = current; // 将第一个线程存起来
                firstReaderHoldCount = 1;// 计数为1
            } else if (firstReader == current) {
                firstReaderHoldCount++; // 读重入,读锁计数进行累加
            } else {
                // 说明不是获得读锁的线程进来了
                // tid 为key ,value为读锁次数
                HoldCounter rh = cachedHoldCounter;// 将当前线程初始值是null
                // 第一次null直接创建一个
                if (rh == null || rh.tid != LockSupport.getThreadId(current))
                    cachedHoldCounter = rh = readHolds.get();// 通过ThreadLocal得到HoldCounter(计数保持器,内部存了加锁计数)
                else if (rh.count == 0) // 如果锁计数为0
                    readHolds.set(rh); // 更新锁计数保持器对象
                rh.count++; // 计数累加
            }
            return 1;// 表示抢占读锁成功
        }
        /** * 二、读是排他的情况,调用下面这个方法 */
        return fullTryAcquireShared(current);
    }
    /** * 读是排他的情况采用自旋方式 * 完整版本的获取读,可处理CAS错误和tryAcquireShared中未处理的可重入读。 */
    final int fullTryAcquireShared(Thread current) {
        /** * 该代码与tryAcquireShared中的代码部分冗余,但由于不使tryAcquireShared与重试和延迟读取保持计数之间的交互复杂化,因此整体代码更简单。 */
        HoldCounter rh = null;
        for (; ; ) {// 自旋
            int c = getState(); // 获取读写锁计数
            /** * 如果存在写锁 */
            if (exclusiveCount(c) != 0) {
                if (getExclusiveOwnerThread() != current)// 判断当前线程是否是持有同一把写锁的线程
                    return -1;// 加锁失败,当前线程不是持有写锁线程
            }
            /** * 不存在写的情况 */
            // 1.判断读是否是排他的,如果是则进入
            else if (readerShouldBlock()) {
                // 当前线程是不是第一个读锁线程,是则说明当前线程是重入的读锁线程
                if (firstReader == current) {
                    // 什么也没有
                } else {
                    // 如果当前线程不是第一个抢占到读锁的线程,如果锁计数存在
                    if (rh == null) {
                        rh = cachedHoldCounter;  // 得到锁计数保持器
                        if (rh == null || rh.tid != LockSupport.getThreadId(current)) {
                            rh = readHolds.get(); // 得到锁计数保持器
                            if (rh.count == 0) // 如果计数为0
                                readHolds.remove(); // 清除保持器
                        }
                    }
                    // 读锁计数保持器存在,如果等于0则抢占读锁失败,因为这个计数器在tryAcquireShared方法已经被赋值了,所以不会为0,为0说明cas操作失败了
                    if (rh.count == 0)
                        return -1; // 加锁失败,当前线程
                }
            }
            // 2.到这里说明是共享的读
            /** * 注意: * 如果是tryAcquireShared方法过来的其实下面不会执行到的, * 因为在tryAcquireShared方法中已经走过一遍这个逻辑了, * 这里加上这个逻辑只是处于对当前方法的封装,这样当前方法可以不用依赖tryAcquireShared方法 */
            if (sharedCount(c) == MAX_COUNT) // 判断读锁是否超过最大值
                throw new Error("Maximum lock count exceeded");
            // 读共享,因此只需要通过cas将读锁计数累加1即可,因为CAS操作多以是单线程所以是加1
            if (compareAndSetState(c, c + SHARED_UNIT)) {// 更新state值
                // c 一开始是0,因为上面更新的不是c而是state值,如果c是0说明是第一个线程调用了这个方法,执行到了这里
                if (sharedCount(c) == 0) {
                    firstReader = current; // 保存当前的第一个线程
                    firstReaderHoldCount = 1;// 保存计数(因为是第一次进入所以是1)
                } else if (firstReader == current) {
                    firstReaderHoldCount++; // 持锁的同一个线程重入读锁
                } else {
                    if (rh == null)
                        rh = cachedHoldCounter; // 其它线程尝试获取读锁,获取第一个线程产生的HoldCounter对象
                    if (rh == null || rh.tid != LockSupport.getThreadId(current))
                        rh = readHolds.get(); // 从ThreadLocal中获取HoldCounter对象
                    else if (rh.count == 0)
                        readHolds.set(rh); // 如果锁计数为0更新锁计数保持其对象
                    rh.count++; // 读锁计数累加
                    cachedHoldCounter = rh; // 保存读锁计数器对象
                }
                return 1; // 读锁加锁成功
            }
        }
    }
}

相关文章推荐

  1. ReentrantReadWriteLock常见问题,源码级别的讲解
  2. ReentrantReadWriteLock源码注释

相关文章