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

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

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

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源码注释
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
ReentrantReadWriteLock是Java中的一个锁实现,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。它是可重入的,也就是说,同一个线程可以多次获取读锁或写锁,而不会出现死锁。 ReentrantReadWriteLock源码实现比较复杂,主要包括两个内部类:Sync和FairSync。Sync是ReentrantReadWriteLock的核心实现,它维护了读锁和写锁的状态,以及等待队列。FairSync是Sync的子类,它实现了公平锁的逻辑。 ReentrantReadWriteLock读写锁是通过Sync类中的state字段实现的。state的高16位表示读锁的持有数量,低16位表示写锁的持有数量。当一个线程获取读锁时,它会增加state的高16位;当一个线程获取写锁时,它会增加state的低16位。当一个线程释放读锁或写锁时,它会减少相应的state值。 ReentrantReadWriteLock的等待队列是通过Sync类中的readQueues和writeQueue字段实现的。当一个线程请求读锁或写锁时,它会被加入到相应的等待队列中。当一个线程释放读锁或写锁时,它会唤醒等待队列中的一个线程。 ReentrantReadWriteLock的公平锁实现是通过FairSync类中的tryAcquireShared和tryAcquire方法实现的。tryAcquireShared方法用于获取读锁,它会先检查等待队列中是否有写锁请求,如果有,则返回失败;否则,它会尝试获取读锁。tryAcquire方法用于获取写锁,它会先检查等待队列中是否有读锁或写锁请求,如果有,则返回失败;否则,它会尝试获取写锁。 总的来说,ReentrantReadWriteLock是一个非常实用的锁实现,它可以提高多线程程序的并发性能。但是,由于它的实现比较复杂,使用时需要注意避免死锁和竞争条件等问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

诗水人间

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值