第一部分:并发理论基础02->java内存模型,看java如何解决可见性和有序性问题
1.前情提要
可见性,原子性,有序性,称为并发编程的bug之源
2.java的内存模型
导致可见性问题是cpu缓存引起
导致有序性问题是编译器优化
那么解决方案是什么?
禁用缓存和禁用编译优化,但是程序性能就下降了
那么如何能保证性能的同时,又解决了可见性及有序性问题?
该禁用缓存和编译优化的时候禁用,其余时候不做限制
java内存模型规范了jvm如何按需禁用缓存和按需禁用编译优化的方法。
主要包含volatile,synchronized,final三个关键字,以及6个happens-before规则
3.volatile
volatile int x = 0
告诉编译器,对这个变量x的读写,不能使用cpu缓存,必须从内存中读取或写入
示例代码思考
// 以下代码来源于【参考1】
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 这里x会是多少呢?
}
}
}
线程A执行write方法,volatile会把变量v=true写入内存,假设线程B执行reader()方法,按照volatile,线程B会从内存中读取变量v,线程看到v==true时,变量x是多少?
jdk1.5之后是42,jdk1.5之后对volatile进行了升级
4.Happens-Before规则
前面一个操作的结果对后续操作是可见的
happens-before规则榆树了编译器的优化行为
4.1程序的顺序性规则
按照程序顺序,前面操作happens-before与后续任意操作。
x=42,happens-before与v=true,符合单线程里面的思维,程序前面对某个变量的修改一定是是对叙操作可见的
4.2 volatile规则
对volatile变量的写操作,happens-before与后续对这个volatile变量的读操作
有点禁用缓存的意思
4.3 传递性
A happens-before B,且B happens-before c,那么A happens-before c
x=42,happens-before与写变量v=true,这是4.1程序顺序性规则的内容
v=true happens-before 读变量v==true,这是4.2 volatile规则
根据传递性原则
x=52 happens-before 读变量vtrue
如果线程B读到了vtrue,那么线程A设置的x=42对线程B是可见的。
4.4 管程中锁的规则
锁的解锁happens-before与后续对这个锁的加锁操作
管程是java中提供的同步原语,就是synchronized关键字
synchronized (this) { //此处自动加锁
// x是共享变量,初始值=10
if (this.x < 12) {
this.x = 12;
}
} //此处自动解锁
结合4.4的规则,我们可以判断出如果x的初始值是10,线程A执行完代码库后x的值会变为12,然后自动释放锁,线程B抢到锁后,能够看到线程A对x的写操作,也就是说线程B可以看到x==12
4.5 线程start()规则
线程A调用线程B的start方法,那么该start操作happens-before与线程B中的任意操作
Thread B = new Thread(()->{
// 主线程调用B.start()之前
// 所有对共享变量的修改,此处皆可见
// 此例中,var==77
});
// 此处对共享变量var修改
var = 77;
// 主线程启动子线程
B.start();
4.6 线程join规则
线程等待,主线程A等待子线程B完成,调用join,子线程B完成后,主线程能够看到子线程的操作
看到的是对共享变量的操作
线程A调用线程B的join()并成功返回,那么线程B的任意操作happens-before 与该join操作的返回
Thread B = new Thread(()->{
// 此处对共享变量var修改
var = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程B可见
// 主线程启动子线程
B.start();
B.join()
// 子线程所有对共享变量的修改
// 在主线程调用B.join()之后皆可见
// 此例中,var==66
5.final
final 关键字可以告诉编译器优化的更好一点
final修饰变量时,告诉编译器,这个变量生而不变,可劲优化
现在final 修饰的变量,对编译重排进行了约束,不会导致有序性问题并产生空指针异常
// 以下代码来源于【参考1】
final int x;
// 错误的构造函数
public FinalFieldExample() {
x = 3;
y = 4;
// 此处就是讲this逸出,
global.obj = this;
}
5.总结
happens-before 本质上是一种可见性
共享变量abc,在一个线程里设置了abc的值=3,那么有什么办法可以让其他线程看到abc==3
1.声明共享变量abc,并使用volatile关键字修饰
2.声明共享变量abc,在synchronized关键字对abc的赋值代码库加锁,由于happen-before管程锁规则,后续线程可以看到abc的值
3.A线程启动后,使用A.JOIN()方法来完成运行,后续线程再启动,就一定可以看到abc==3
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示