文章40 | 阅读 14793 | 点赞0
读写锁适合使用在读多写少的场景,如果写多读少,反而没有可重入锁的效率高,一般而言,能够使用读写改造的情况下,使用读写锁效率会更高。
下面是一个读写锁的读锁使用案例
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; // 读锁加锁成功
}
}
}
}
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://yumbo.blog.csdn.net/article/details/110037614
内容来源于网络,如有侵权,请联系作者删除!