并发编程-ReentrantReadWriteLock读写锁详解

static final class HoldCounter {

// 计数

int count = 0;

// 获取当前线程的TID属性的值

final long tid = getThreadId(Thread.currentThread());

}

说明: HoldCounter主要有两个属性,count和tid,其中count表示某个读线程重入的次数,tid表示该线程的tid字段的值,该字段可以用来唯一标识一个线程 。ThreadLocalHoldCounter的源码如下

// 本地线程计数器

static final class ThreadLocalHoldCounter extends ThreadLocal {

// 重写初始化方法,在没有进行set的情况下,获取的都是该HoldCounter值

public HoldCounter initialValue() {

return new HoldCounter();

}

}

说明:ThreadLocalHoldCounter重写了ThreadLocal的initialValue方法,ThreadLocal类可以将线程与对象相关联。在没有进行set的情况下,get到的均是initialValue方法里面生成的那个HolderCounter对象。

(3)类的属性

abstract static class Sync extends AbstractQueuedSynchronizer {

// 版本序列号

private static final long serialVersionUID = 6317671515068378041L;

// 高16位为读锁,低16位为写锁

static final int SHARED_SHIFT = 16;

// 读锁单位,读锁数量加1,即为state+SHARED_UNIT

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;

// 本地线程计数器

private transient ThreadLocalHoldCounter readHolds;

// 缓存的计数器

private transient HoldCounter cachedHoldCounter;

// 第一个读线程

private transient Thread firstReader = null;

// 第一个读线程的计数

private transient int firstReaderHoldCount;

}

说明:该属性中包括了读锁、写锁线程的最大量。本地线程计数器等。

(4)类的构造函数

// 构造函数

Sync() {

// 本地线程计数器

readHolds = new ThreadLocalHoldCounter();

// 设置AQS的状态

setState(getState()); // ensures visibility of readHolds

}

说明:在Sync的构造函数中设置了本地线程计数器和AQS的状态state。

3、读写状态的设计

同步状态在重入锁的实现中是表示被同一个线程重复获取的次数,即一个整形变量来维护,但是之前的那个表示仅仅表示是否锁定,而不用区分是读锁还是写锁。而读写锁需要在同步状态(一个整形变量)上维护多个读线程和一个写线程的状态。

读写锁对于同步状态的实现是在一个整形变量上通过“按位切割使用”:将变量切割成两部分,高16位表示读,低16位表示写。

http://static.open-open.com/lib/uploadImg/20151031/20151031223319_397.png

假设当前同步状态值为S,get和set的操作如下:

(1)获取写状态:

S&0x0000FFFF:将高16位全部抹去

(2)获取读状态:

S>>>16:无符号补0,右移16位

(3)写状态加1:

S+1

(4)读状态加1:

S+(1<<16)即S + 0x00010000

在代码层的判断中,如果S不等于0,当写状态(S&0x0000FFFF),而读状态(S>>>16)大于0,则表示该读写锁的读锁已被获取。

4、写锁的获取与释放

看下WriteLock类中的lock和unlock方法:

public void lock() {

sync.acquire(1);

}

public void unlock() {

sync.release(1);

}

可以看到就是调用的独占式同步状态的获取与释放,因此真实的实现就是Sync的 tryAcquire和 tryRelease。

写锁的获取,看下tryAcquire:

1 protected final boolean tryAcquire(int acquires) {

2 //当前线程

3 Thread current = Thread.currentThread();

4 //获取状态

5 int c = getState();

6 //写线程数量(即获取独占锁的重入数)

7 int w = exclusiveCount©;

8

9 //当前同步状态state != 0,说明已经有其他线程获取了读锁或写锁

10 if (c != 0) {

11 // 当前state不为0且写锁状态为0说明读锁此时被占用返回false;

12 // 如果写锁状态不为0且写锁没有被当前线程持有返回false

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

14 return false;

15

16 //判断同一线程获取写锁是否超过最大次数(65535),支持可重入

17 if (w + exclusiveCount(acquires) > MAX_COUNT)

18 throw new Error(“Maximum lock count exceeded”);

19 //更新状态

20 //此时当前线程已持有写锁,现在是重入,所以只需要修改锁的数量即可。

21 setState(c + acquires);

22 return true;

23 }

24

25 //到这里说明此时c=0,读锁和写锁都没有被获取

26 //writerShouldBlock表示是否阻塞

27 if (writerShouldBlock() || !compareAndSetState(c, c + acquires))

29 return false;

30

31 //设置锁为当前线程所有

32 setExclusiveOwnerThread(current);

33 return true;

34 }

其中exclusiveCount方法表示占有写锁的线程数量,源码如下:

static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

说明:直接将状态state和(2^16 - 1)做与运算,其等效于将state模上2^16。写锁数量由state的低十六位表示,相当于把高位全部抹掉。

从源代码可以看出,获取写锁的步骤如下:

(1)首先获取c、w。c表示当前锁状态;w表示写线程数量。然后判断同步状态state是否为0。如果state!=0,说明已经有其他线程获取了读锁或写锁,执行(2);否则执行(5)。

(2)如果锁状态不为零(c != 0),而写锁的状态为0(w = 0),说明读锁此时被其他线程占用,所以当前线程不能获取写锁,自然返回false。或者锁状态不为零,而写锁的状态也不为0,但是获取写锁的线程不是当前线程,则当前线程也不能获取写锁。

(3)判断当前线程获取写锁是否超过最大次数,若超过,抛异常,反之更新同步状态(此时当前线程已获取写锁,更新是线程安全的),返回true。

(4)如果state为0,此时读锁或写锁都没有被获取,判断是否需要阻塞(公平和非公平方式实现不同),在非公平策略下总是不会被阻塞,在公平策略下会进行判断(判断同步队列中是否有等待时间更长的线程,若存在,则需要被阻塞,否则,无需阻塞),如果不需要阻塞,则CAS更新同步状态,若CAS成功则返回true,失败则说明锁被别的线程抢去了,返回false。如果需要阻塞则也返回false。

(5)成功获取写锁后,将当前线程设置为占有写锁的线程,返回true。

方法流程图如下:

img

写锁的释放,tryRelease方法:

1 protected final boolean tryRelease(int releases) {

2 //若锁的持有者不是当前线程,抛出异常

3 if (!isHeldExclusively())

4 throw new IllegalMonitorStateException();

5 //写锁的新线程数

6 int nextc = getState() - releases;

7 //如果独占模式重入数为0了,说明独占模式被释放

8 boolean free = exclusiveCount(nextc) == 0;

9 if (free)

10 //若写锁的新线程数为0,则将锁的持有者设置为null

11 setExclusiveOwnerThread(null);

12 //设置写锁的新线程数

13 //不管独占模式是否被释放,更新独占重入数

14 setState(nextc);

15 return free;

16 }

写锁的释放过程还是相对而言比较简单的:首先查看当前线程是否为写锁的持有者,如果不是抛出异常。然后检查释放后写锁的线程数是否为0,如果为0则表示写锁空闲了,释放锁资源将锁的持有线程设置为null,否则释放仅仅只是一次重入锁而已,并不能将写锁的线程清空。

说明:此方法用于释放写锁资源,首先会判断该线程是否为独占线程,若不为独占线程,则抛出异常,否则,计算释放资源后的写锁的数量,若为0,表示成功释放,资源不将被占用,否则,表示资源还被占用。其方法流程图如下。

img

5、读锁的获取与释放

类似于写锁,读锁的lock和unlock的实际实现对应Sync的 tryAcquireShared 和 tryReleaseShared方法。

读锁的获取,看下tryAcquireShared方法

1 protected final int tryAcquireShared(int unused) {

2 // 获取当前线程

3 Thread current = Thread.currentThread();

4 // 获取状态

5 int c = getState();

6

7 //如果写锁线程数 != 0 ,且独占锁不是当前线程则返回失败,因为存在锁降级

8 if (exclusiveCount© != 0 && getExclusiveOwnerThread() != current)

10 return -1;

11 // 读锁数量

12 int r = sharedCount©;

13 /*

14 * readerShouldBlock():读锁是否需要等待(公平锁原则)

15 * r < MAX_COUNT:持有线程小于最大数(65535)

16 * compareAndSetState(c, c + SHARED_UNIT):设置读取锁状态

17 */

18 // 读线程是否应该被阻塞、并且小于最大值、并且比较设置成功

19 if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {

22 //r == 0,表示第一个读锁线程,第一个读锁firstRead是不会加入到readHolds中

23 if (r == 0) { // 读锁数量为0

24 // 设置第一个读线程

25 firstReader = current;

26 // 读线程占用的资源数为1

27 firstReaderHoldCount = 1;

28 } else if (firstReader == current) { // 当前线程为第一个读线程,表示第一个读锁线程重入

29 // 占用资源数加1

30 firstReaderHoldCount++;

31 } else { // 读锁数量不为0并且不为当前线程

32 // 获取计数器

33 HoldCounter rh = cachedHoldCounter;

34 // 计数器为空或者计数器的tid不为当前正在运行的线程的tid

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

36 // 获取当前线程对应的计数器(cachedHoldCounter的存在是为了提供性能,比如第一次是A线程,

// readHolds.get()了一次然后赋值给了cachedHoldCounter,在第二次的时候cachedHoldCounter

// 存的就是当前线程,就不用再次readHolds.get(),因为readHolds.get()是一个耗时的操作。)

37 cachedHoldCounter = rh = readHolds.get();

38 else if (rh.count == 0) // 计数为0

39 //加入到readHolds中

40 readHolds.set(rh);

41 //计数+1

42 rh.count++;

43 }

44 return 1;

45 }

46 return fullTryAcquireShared(current);

47 }

其中sharedCount方法表示占有读锁的线程数量,源码如下:

static int sharedCount(int c) { return c >>> SHARED_SHIFT; }

说明:直接将state右移16位,就可以得到读锁的线程数量,因为state的高16位表示读锁,对应的第十六位表示写锁数量。

读锁获取锁的过程比写锁稍微复杂些,首先判断写锁是否为0并且当前线程不占有独占锁,直接返回;否则,判断读线程是否需要被阻塞并且读锁数量是否小于最大值并且比较设置状态成功,若当前没有读锁,则设置第一个读线程firstReader和firstReaderHoldCount;若当前线程线程为第一个读线程,则增加firstReaderHoldCount;否则,将设置当前线程对应的HoldCounter对象的值。流程图如下。

img

注意:更新成功后会在firstReaderHoldCount中或readHolds(ThreadLocal类型的)的本线程副本中记录当前线程重入数(23行至43行代码),这是为了实现jdk1.6中加入的getReadHoldCount()方法的,这个方法能获取当前线程重入共享锁的次数(state中记录的是多个线程的总重入次数),加入了这个方法让代码复杂了不少,但是其原理还是很简单的: 如果当前只有一个线程的话,还不需要动用ThreadLocal,直接往firstReaderHoldCount这个成员变量里存重入数,当有第二个线程来的时候,就要动用ThreadLocal变量readHolds了,每个线程拥有自己的副本,用来保存自己的重入数。

fullTryAcquireShared方法:

final int fullTryAcquireShared(Thread current) {

HoldCounter rh = null;

for (;😉 { // 无限循环

// 获取状态

int c = getState();

if (exclusiveCount© != 0) { // 写线程数量不为0

if (getExclusiveOwnerThread() != current) // 不为当前线程

return -1;

} else if (readerShouldBlock()) { // 写线程数量为0并且读线程被阻塞

// 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 != getThreadId(current)) { // 计数器为空或者计数器的tid不为当前正在运行的线程的tid

rh = readHolds.get();

if (rh.count == 0)

readHolds.remove();

}

}

if (rh.count == 0)

return -1;

}

}

if (sharedCount© == MAX_COUNT) // 读锁数量为最大值,抛出异常

throw new Error(“Maximum lock count exceeded”);

if (compareAndSetState(c, c + SHARED_UNIT)) { // 比较并且设置成功

if (sharedCount© == 0) { // 读线程数量为0

// 设置第一个读线程

firstReader = current;

//

firstReaderHoldCount = 1;

} else if (firstReader == current) {

firstReaderHoldCount++;

} else {

if (rh == null)

rh = cachedHoldCounter;

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

rh = readHolds.get();

else if (rh.count == 0)

readHolds.set(rh);

rh.count++;

cachedHoldCounter = rh; // cache for release

}

return 1;

}

}

}

说明:在tryAcquireShared函数中,如果下列三个条件不满足(读线程是否应该被阻塞、小于最大值、比较设置成功)则会进行fullTryAcquireShared函数中,它用来保证相关操作可以成功。其逻辑与tryAcquireShared逻辑类似,不再累赘。

读锁的释放,tryReleaseShared方法

1 protected final boolean tryReleaseShared(int unused) {

2 // 获取当前线程

3 Thread current = Thread.currentThread();

4 if (firstReader == current) { // 当前线程为第一个读线程

6 if (firstReaderHoldCount == 1) // 读线程占用的资源数为1

7 firstReader = null;

8 else // 减少占用的资源

9 firstReaderHoldCount–;

10 } else { // 当前线程不为第一个读线程

11 // 获取缓存的计数器

12 HoldCounter rh = cachedHoldCounter;

// 计数器为空或者计数器的tid不为当前正在运行的线程的tid

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

14 // 获取当前线程对应的计数器

15 rh = readHolds.get();

16 // 获取计数

17 int count = rh.count;

18 if (count <= 1) { // 计数小于等于1

19 // 移除

20 readHolds.remove();

21 if (count <= 0) // 计数小于等于0,抛出异常

22 throw unmatchedUnlockException();

23 }

24 // 减少计数

25 --rh.count;

26 }

27 for (;😉 { // 无限循环

28 // 获取状态

29 int c = getState();

30 // 获取状态

31 int nextc = c - SHARED_UNIT;

32 if (compareAndSetState(c, nextc)) // 比较并进行设置

36 return nextc == 0;

37 }

38 }

说明:此方法表示读锁线程释放锁。首先判断当前线程是否为第一个读线程firstReader,若是,则判断第一个读线程占有的资源数firstReaderHoldCount是否为1,若是,则设置第一个读线程firstReader为空,否则,将第一个读线程占有的资源数firstReaderHoldCount减1;若当前线程不是第一个读线程,那么首先会获取缓存计数器(上一个读锁线程对应的计数器 ),若计数器为空或者tid不等于当前线程的tid值,则获取当前线程的计数器,如果计数器的计数count小于等于1,则移除当前线程对应的计数器,如果计数器的计数count小于等于0,则抛出异常,之后再减少计数即可。无论何种情况,都会进入无限循环,该循环可以确保成功设置状态state。其流程图如下。

img

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后我们该如何学习?

1、看视频进行系统学习

这几年的Crud经历,让我明白自己真的算是菜鸡中的战斗机,也正因为Crud,导致自己技术比较零散,也不够深入不够系统,所以重新进行学习是很有必要的。我差的是系统知识,差的结构框架和思路,所以通过视频来学习,效果更好,也更全面。关于视频学习,个人可以推荐去B站进行学习,B站上有很多学习视频,唯一的缺点就是免费的容易过时。

另外,我自己也珍藏了好几套视频资料躺在网盘里,有需要的我也可以分享给你:

1年半经验,2本学历,Curd背景,竟给30K,我的美团Offer终于来了

2、读源码,看实战笔记,学习大神思路

“编程语言是程序员的表达的方式,而架构是程序员对世界的认知”。所以,程序员要想快速认知并学习架构,读源码是必不可少的。阅读源码,是解决问题 + 理解事物,更重要的:看到源码背后的想法;程序员说:读万行源码,行万种实践。

Spring源码深度解析:

1年半经验,2本学历,Curd背景,竟给30K,我的美团Offer终于来了

Mybatis 3源码深度解析:

1年半经验,2本学历,Curd背景,竟给30K,我的美团Offer终于来了

Redis学习笔记:

1年半经验,2本学历,Curd背景,竟给30K,我的美团Offer终于来了

Spring Boot核心技术-笔记:

1年半经验,2本学历,Curd背景,竟给30K,我的美团Offer终于来了

3、面试前夕,刷题冲刺

面试的前一周时间内,就可以开始刷题冲刺了。请记住,刷题的时候,技术的优先,算法的看些基本的,比如排序等即可,而智力题,除非是校招,否则一般不怎么会问。

关于面试刷题,我个人也准备了一套系统的面试题,帮助你举一反三:

1年半经验,2本学历,Curd背景,竟给30K,我的美团Offer终于来了

只有技术过硬,在哪儿都不愁就业,“万般带不去,唯有业随身”学习本来就不是在课堂那几年说了算,而是在人生的旅途中不间断的事情。

人生短暂,别稀里糊涂的活一辈子,不要将就。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
的我也可以分享给你:

[外链图片转存中…(img-TxotgHSl-1713670570497)]

2、读源码,看实战笔记,学习大神思路

“编程语言是程序员的表达的方式,而架构是程序员对世界的认知”。所以,程序员要想快速认知并学习架构,读源码是必不可少的。阅读源码,是解决问题 + 理解事物,更重要的:看到源码背后的想法;程序员说:读万行源码,行万种实践。

Spring源码深度解析:

[外链图片转存中…(img-ryaG5HMa-1713670570498)]

Mybatis 3源码深度解析:

[外链图片转存中…(img-Sm0DdDwJ-1713670570498)]

Redis学习笔记:

[外链图片转存中…(img-thah9kXz-1713670570498)]

Spring Boot核心技术-笔记:

[外链图片转存中…(img-dvqaOqM9-1713670570498)]

3、面试前夕,刷题冲刺

面试的前一周时间内,就可以开始刷题冲刺了。请记住,刷题的时候,技术的优先,算法的看些基本的,比如排序等即可,而智力题,除非是校招,否则一般不怎么会问。

关于面试刷题,我个人也准备了一套系统的面试题,帮助你举一反三:

[外链图片转存中…(img-37PSdCpJ-1713670570499)]

只有技术过硬,在哪儿都不愁就业,“万般带不去,唯有业随身”学习本来就不是在课堂那几年说了算,而是在人生的旅途中不间断的事情。

人生短暂,别稀里糊涂的活一辈子,不要将就。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 28
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值