共享模型之管程

共享带来的问题

上下文切换问题

image-20220314234314311

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
+ 共享带来的问题
+ 上下文切换分析
+ 单线程
+ 多线程情况
+ 线程的 cpu 时间片用完 -> 线程挂起,运行到就绪 -> 线程上下文切换 -> 字节码指令交错运行 -> 共享变量counter读写冲突
+ 根源:指令交错
+ 临界区与竞态条件
+ 临界区
+ 存在对共享资源的多线程读写区域
+ 竞态条件
+ 结果无法预测
+ 避免竞态条件
+ 非阻塞式
+ 原子变量
+ 阻塞式
+ synchronize、lock
+ synchronize
+ 对象锁
+ 临界区代码变成串行
+ 对象,多个线程共享
+ 理解
+ 比喻
+ synchronize中即使时间片用完,下次获取时间片仍进入(上下文切换)
+ 只有执行完其中代码,从synchronize出来解锁
+ 时序图
+ 执行中,若本线程上下文切换
+ 其他线程获取锁被阻塞blocked
+ 其他线程直接上下文切换,本线程继续执行
+ 释放锁,唤醒阻塞线程
+ 思考
+ 利用对象锁保证临界区中代码的原子性
+ 原子性:原子操作,不可分割
+ 面向对象改进
+ 互斥等逻辑封装在room类内部
+ 对共享资源保护,由内部实现
+ 对外调用即可
+ synchronize加在方法上
+ 成员方法上
+ 相当于锁this对象
+ static方法上
+ 相当于锁类对象
+ jvm中只有一个类对象,但是有多个实例对象。这就是区别
+ 习题:线程八锁
+ sleep即抱着锁睡觉
+ 锁类对象只有一个

线程安全分析

image-20220314234413264

1
2
3
4
5
6
7
8
9
10
11
+ 变量的线程安全分析
+ 成员变量、静态变量被共享
+ 读操作,线程安全
+ 读写操作,临界区,考虑线程安全
+ 局部变量
+ 局部变量在线程之内,不共享
+ 线程安全
+ 局部变量引用的对象,可能被共享
+ 如果引用对象逃离方法的作用范围,考虑线程安全
+ 暴露引用
+ private、final,防止子类影响

常见线程安全类

image-20220314234431459

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
+ 线程安全类
+ 多个线程调用他们同一实例时是安全的
+ hashtable put
+ 多个方法组合不安全:非原子
+ 还需在外部上锁
+ 不可变类
+ 直接创建新的实例,不在原有实例上改变
+ 线程安全
+ 实例分析
+ final:对象的引用不可变不代表对象的状态不可变
+ 还可以使用ThreadLocal给每个线程存储一个私有start变量
+ 我觉得主要还是看变量是否1、能逃离,2、共享
+ 先看有木有多个线程同时访问共享变量,再看该共享变量是否可被修改
+ 逃逸分析,要符合闭合原则
+ 习题
+ 售票
+ sell读写操作,不安全
+ add线程安全,有synchronize
+ sell和add
+ 安全, 不是同一共享变量
+ 概括来说就是:下边的原子操作不依赖上边原子操作的结果的话,就不用考虑两个原子操作合在一起的安全性
+ threadlist安全,不被多个线程共享
+ sell加锁
+ 保证原子性
+ 转账
+ 加锁:类对象

Monitor

加锁实现原理

image-20220314234449143

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ 对象头
+ klass word
+ 找到的类对象
+ mark word
+ monitor(锁)
+ 加锁实现原理
+ 对象与monitor相关联
+ 每个synchronize(obj)对象关联一个monitor
+ 只能有一个owner
+ 其他线程执行synchronize(obj)
+ 进入entrylist blocked
+ 执行完同步代码块内容
+ 唤醒entrylist中等待线程
+ 线程相互竞争owner
+ 原理(字节码)
+ 如果出现异常,帮我们正确解锁
+ 重置mark word,唤醒entrylist
+ 抛出异常

synchronize优化原理

对象头格式:正常、偏向锁、轻量级锁、重量级锁、GC

image-20220314234523762

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
+ synchronize优化原理
+ 小故事
+ 轻量级锁
+ 创建锁记录(Lock Record)对象
+ 线程栈帧中包含锁的结构
+ 锁记录中object reference指向锁对象
+ 用cas替换lock record和锁的mark word
+ 用于解锁的恢复和锁记录地址
+ cas替换成功
+ 锁中存储了锁记录地址(哪个线程)和轻量级锁
+ 表示该线程给对象加锁
+ cas替换失败
+ 如果其他线程持有了该obj的轻量级锁
+ 进入锁膨胀过程
+ 如果自己synchronize锁重入
+ 添加lock record,知道是自己线程的锁,作为重入的计数
+ 解锁时如果取值为null,表示锁重入,清除锁记录
+ 解锁时如果取值不为null
+ cas将mark word恢复给锁对象头
+ 变为无锁状态
+ 成功,解锁成功
+ 失败
+ 说明轻量级锁进入了锁膨胀或一级升级为重量级锁
+ 进入重量级锁解锁流程
+ 锁膨胀
+ 将轻量级锁升级为重量级锁
+ 流程
+ 为object对象申请monitor锁
+ mark word指向重量级锁地址,后两位10
+ 其他线程进入monitor的entrylist阻塞
+ 线程执行完,解锁
+ cas将mark word恢复,失败
+ 进入重量级解锁流程
+ 根据对象头找到monitor
+ 设置owner为null
+ 唤醒entrylist
+ 自旋优化
+ 重量级锁竞争时,自旋优化(重试)
+ 减少阻塞发生
+ 阻塞要进行上下文切换
+ 自旋失败,进入阻塞
+ 自旋锁是自适应的,正反馈调节
+ java7以后无法控制是否开启自旋
+ 偏向锁
+ 轻量级锁,锁重入时仍需CAS操作
+ 每一次都要cas操作(翻书包)
+ 偏向锁 (刻名字)
+ 第一次cas时将Thread id设置到锁对象头
+ 重入时判断为本线程,无需cas
+ 偏向状态
+ 默认开启偏向锁,mark word中biased_lock为1
+ 默认延迟开启
+ 获取锁对象
+ 设置线程id到锁的mark word
+ synchronize结束后
+ 锁的mark word不变
+ 其他线程获取锁对象
+ 撤销偏向锁,变成轻量级锁
+ 竞争
+ 膨胀变成重量级锁
+ 使用偏向锁之前获取hashcode
+ 会禁用偏向锁
+ 哈希码用的时候才产生,填到mark word中
+ 轻量级锁hashcode存在线程栈帧的锁记录里
+ 重量级锁hashcode存在monitor对象里
+ 解锁时都会还原回来
+ 偏向锁没有存储的地方
+ 撤销
+ 调用hashcode
+ 其他线程使用对象
+ 偏向锁升级为轻量级锁
+ 测试
+ 轻量级锁、偏向锁前提
+ 线程错开,否则升级重量级锁
+ wait会释放CPU和锁资源
+ 多个线程获取偏向锁
+ 撤销偏向锁
+ 调用wait/notify
+ wait/notify只有重量级锁有
+ 自动升级为重量级锁
+ 批量重偏向
+ 虽然被多个线程访问,没有竞争
+ 让偏向锁重新偏向,而不升级轻量级锁
+ 撤销偏向次数过多:超过20次
+ 重新偏向新线程
+ 总结就是没超过阈值,先把偏向锁变成轻量锁,然后再偏向锁
+ 批量撤销
+ 撤销偏向次数过多:超过40次
+ 整个类所有对象不可偏向
+ 测试
+ t1
+ 39个偏向t1(偏向锁)
+t2
+ 前19个撤销,变成轻量级锁,解锁不可偏向,(轻量级锁)
+ 19以后批量重偏向优化,重新偏向于t2,(偏向锁)
+ t3
+ 前19个撤销,不可偏向
+ t3前19个已被t2设为不可偏向,为轻量级锁
+ t3前十九个已经被t2设置为不可偏向锁,故t3初始为不可偏向锁,加锁成为轻量级锁
+ T2前十九个没有重偏向,解锁后就变为不可偏向,后二十是偏向t2
+ 19以后,
+ 原本偏向t2线程
+ 被撤销,升级为轻量级锁
+ 解锁之后不可偏向
+ 重偏向是针对单个线程来讲的,一个线程撤销20个锁才进行重偏向,这时另一个线程还是要撤销20个
+ t1:全部偏向t1;t2:一半撤销t1的偏向锁,一半偏向t2;t3:一半轻量级锁,一半撤销t2的偏向锁;总共撤销了20次t1的偏向锁,20次t2的偏向锁
+ 批量重偏向和批量撤销是针对类的优化,和对象无关。偏向锁重偏向一次之后不可再次重偏向。当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利
+ 锁消除
+ JIT优化
+ 分析局部变量,synchronize中不会被共享
+ 把synchronize优化掉

wait、notify

image-20220314234608743

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
+ wait、notify
+ 为什么需要wait
+ 进入休息室wait
+ 烟到了notify,唤醒wait线程
+ 工作原理
+ owner线程发现条件不满足,放弃锁
+ 调用wait方法进入waitset
+ WAITING:没有时限
+ blocked和waiting都处于阻塞
+ 不占用cpu时间片
+ owner调用notify或者notifyall唤醒
+ 进入entrylist重新竞争
+ api
+ 都需要获得锁成为owner才能调用
+ 同步代码块中
+ notify挑一个唤醒
+ 有参wait
+ 最大时限等待
+ sleep 和 wait
+ sleep是thread方法,wait是object所有对象方法
+ sleep不需要和synchronize使用,wait需要获取锁
+ sleep睡眠不会释放锁,wait等待时会释放锁
+ wait和notify使用
+ wait时其他线程可以继续工作
+ notify错误唤醒:虚假唤醒
+ notifyall全部唤醒
+ 指定的话,用park,unpark
+ notifyall + while
+ if条件改为while,
+ 只有条件正确才能唤醒出循环
+ 正确使用

设计模式

保护性暂停

image-20220314234708945

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
+ 设计模式:同步模式之保护性暂停
+ 定义
+ 一个线程等待另一个线程执行结果
+ 一个结果从一个线程传递到另一个线程,关联同一个GuardedObject
+ 结果不断传输,需要消息队列(生产者、消费者)
+ join、future实现,采用保护性暂停
+ 等待结果,同步模式
+ 实现
+ guardedobject
+ 优点
+ 相比join,不需要等线程结束,线程还可以继续运行
+ 等待结果变量无需设置全局,直接获取
+ 拓展-增加超时
+ wait time
+ 测试
+ join原理
+ 记录经历时间,等待
+ 保护性暂停
+ 拓展-解耦等待和生产
+ 解耦,生产者和消费者
+ 维护一个集合,guardedobject有唯一id
+ 实现
+ mailbox
+ 线程安全的map hashtable
+ 自增id方法加锁
+ 获取完邮件直接删除,所以remove
+ postman、people
+ 继承Thread
+ people
+ postman
+ 测试
+ //确保生成居民先于快递员,但是快递员发信要早于居民收信
+ 解耦了生产者和消费者
+ 生产者和消费者必须一一对应

生产者消费者

生产者消费者-消息队列

image-20220314234728830

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
+ 异步模式之生产者消费者
+ 定义
+ 不需要生产者和消费者一一对应
+ 消息队列
+ 有容量限制
+ 各种阻塞队列,就是这种模式
+ 异步模式
+ 等待通知 同步,等待需要 通知 之后的结果
+ 实现
+ message
+ messageQueue
+ LinkedList 双向队列
+ 获取消息
+ 队列为空wait等待
+ 不为空,取出队列头部消息
this的话锁粒度太大了,直接就相当于单线程操作了……
+ 取完消息,notify唤醒生产者,队列不满
+ 存入消息
+ 判断队列否已满
+ 生产者wait等待
+ 尾部加入消息
+ notify唤醒消费者,接收消息
+ 测试
+ 生产者
+ 消费者

Park&unpark

先调用unpark原理

image-20220314234756570

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
+ park&unpark
+ 基本使用
+ 暂停当前线程
+ wait状态,无时限等待
+ 若先调用unpark
+ 无法park住
+ 特点
+ 无需配置monitor
+ 以线程为单位阻塞、唤醒线程,精准唤醒
+ 可以先unpark
+ 原理
+ 每个线程都有parker对象(背包)
+ counter(0、1)
+ 干粮
+ cond
+ 帐篷
+ mutex
+ unpark:令干粮充足
+ 调用park
+ 调用unpark
+ 先调用unpark
+ 说白了就是当count为0时调用park才会停
+ 总结
+ park消费counter
+ 若无counter进入cond,counter为0
+ 若unpark,counter为1
+ 唤醒cond中线程,消费counter为0,继续运行
+ 若有counter,counter为0,继续运行

线程状态转换

线程状态

image-20220314234813808

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
+ 线程状态转换

+ new
+ 创建java对象,还没和操作系统线程对象关联
+ runnable
+ 关联操作系统线程,cpu调度执行
+ 包括是否被cpu调度
+ 调用操作系统阻塞io相关的api,java层面都是runnable
+ runnable和waiting
+ 使用synchronize获取对象锁后,wait(),进入waitset
+ notify、interrupt,进入entryset()
+ 竞争锁成功,runnable
+ 竞争锁失败,blocked
+ 调用t线程的join(),当前线程变成waiting
+ t线程结束、当前线程interrupt,变成runnable
+ park
+ unpark、interrupt
+ runnable和timed_waiting(有时限waiting)
+ 获取了synchronize对象锁之后,wait(n)
+ 等待超时、notify、interrupt,进入entryset
+ 竞争锁成功,runnable
+ 竞争锁失败,blocked
+ join(n)
+ 等待超时、运行结束、interrupt,runnable
+ parkNacos(n)、parkUntil(n)
+ 超时、unpark、interrupt,runnable
+ sleep(n)
+ 超时,runnable
+ runnable和blocked
+ 竞争锁失败,blocked
+ 竞争锁成功,runnable
+ terminated
+ 当前线程所有代码执行完毕

多把锁

JUC-可重入锁

image-20220314234844081

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
+ 多把锁
+ 一把锁,并发度低
+ 粒度细分,准备多个对象锁
+ 业务不能有关联
+ 增加并发度
+ 可能造成死锁
+ 活跃性
+ 死锁现象
+ 一个线程需要同时获取多把锁
+ 都在等待对方释放锁
+ 定位死锁
+ jconsole、jstack
+ 哲学家就餐问题
+ 活锁
+ 两个线程互相改变,无法达到结束条件
+ 没有阻塞
+ 执行时间交错开,随机睡眠时间
+ 饥饿
+ 线程优先级太低,始终无法得到cpu调度
+ 顺序加锁的解决方案,都按A、B顺序加锁
+ 产生饥饿问题
+ 在争夺左筷子(最外层锁)中,苏和阿同时争夺c1,所以概率倒数,在 第二轮争夺右筷子时,c5没有人跟赫争,故赫能吃到饭的概率最高
+ ReentrantLock 可重入锁
+ 特点
+ 可中断(破坏锁)
+ 可设置超时时间(争抢锁)
+ 可设置为公平锁(block先进先出)
+ 支持多个条件变量(不同条件对应多个waitset,精准唤醒)
+ 与synchronize一样,支持可重入(同一个线程可以重复获取锁)
+ 可重入
+ 同一个线程获取锁后,可再次获取锁(lock多次)
+ 可打断
+ 等待锁过程中,可以被interrupt打断终止等待
+ lockInterruptibly方法获取锁(lock无法打断等待)
+ 其他线程interrupt()打断
+ 捕捉到InterruptException异常
+ 直接返回
+ 被动,避免死等
+ 锁超时
+ 主动,设置超时避免死等
+ trylock(),尝试获得锁,返回布尔值
+ 获取不到锁直接返回
+ 返回为真,获得到了锁,向下执行代码
+ trylock()带参数
+ 等待获取锁时间,超时返回false
+ 可被打断,进入打断异常,直接返回
+ 解决哲学家就餐
+ 筷子继承可重入锁
+ trylock()
+ 拿不到锁就结束,释放所有锁
+ 左手拿不到继续获取,右手拿不到,释放左手锁
+ 公平锁
+ 默认不公平
+ 公平锁:先入先得锁
+ 为了解决饥饿问题
+ trylock也可以解决
+ 没有必要,降低并发度
+ 非公平锁:没进入等待队列也有机会获得锁
+ 条件变量
+ 支持多个条件变量(休息室),可以精准唤醒
+ 操作
+ 创建条件变量(休息室)
+ 指定条件变量执行await,等待
+ 时间参数
+ 指定条件变量执行signal,唤醒
+ 唤醒之后重新竞争lock锁
+ 使用
+ 不看源码,自己把源码敲了一遍,的确有些自己一直没注意到的地方

设计模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
+ 同步模式之顺序控制(控制线程的运行顺序)
+ 固定运行顺序(先执行2,后执行1)
+ wait&notify
+ while条件判断 + wait、notify
+ await&signal
+ join()
+ park&unpark
+ 精准暂停,唤醒
+ 背包干粮
+ 若t2先运行,t1可正常继续执行
+ 交替输出(按abc顺序交替输出)
+ wait&notify
+ 等待标记:用整数1、2、3标记条件,代表某个数字允许打印
+ 面向对象code,很强
+ await&signal
+ 三个不同休息室(条件变量)
+ 进入当前休息室
+ 打印
+ 唤醒下一间休息室
+ 执行流程
+ 不管谁抢到锁,都进入休息室
+ 只有a休息室解锁,a打印,解锁b休息室
+ 只有b会继续运行,打印,解锁c
+ 之前已经说过了,可以先唤醒,后等待,不存在全部等待的问题
+ park&unpark
+ 以线程为单位停止、恢复
+ 我一直以为我很懂面向对象,写的太烂了
+ 面向对象编程,确实牛逼
+ 流程
+ 无论谁抢到时间片,全部线程park
+ 主线程唤醒t1,t1线程打印,唤醒t2
+ t2打印,唤醒t3

小结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
+ 知识点
+ 共享资源、临界区
+ synchronize
+ 保证临界区代码线程安全
+ 原子性
+ 不至于上下文切换产生交错
+ 锁对象语法
+ 成员、静态方法
+ wait、notify同步方法
+ 条件不满足等待,条件满足恢复运行
+ reentrantlock
+ 可打断、锁超时trylock、公平锁、条件变量
+ 分析变量线程安全性,常见线程安全类
+ 线程活跃性
+ 死锁、活锁、饥饿
+ 应用
+ 互斥
+ synchronize和lock共享资源互斥
+ 同步
+ wait/notify、await/signal线程间通信效果
+ 原理
+ monitor、synchronize、wait/notify
+ synchronize
+ 轻量级锁、偏向锁、重量级锁
+ 锁膨胀、锁消除等
+ park&unpark
+ synchronize实现monitor,JVM层面,c++
+ lock实现monitor,java级别
+ 设计模式
+ 同步模式之保护性暂停
+ 线程之间获得结果,一一对应
+ 异步模式之生产者消费者
+ 生产消费不是一一对应关系
+ 同步模式之顺序控制
+ 控制线程执行先后次序、交替运行