可见性问题
- 代码
// 关闭jit优化:-server -Djava.compiler=NONE
//将运行模式设置为- server服务器端,就会变成死循环,默认idea运行时-client模式不会进行jvm层次的指令重排,也就是JIT时期的重排
//通过设置JVM的参数,打印JIT编译的内容(这里说的编译非class文件,是底层汇编内容),通过可视化工具jitwatch查看
//-server -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation -XX:LogFile=jit.log
public class VisibilityDemo {
private boolean flag=true;
public static void main(String[] args) throws InterruptedException {
VisibilityDemo demo=new VisibilityDemo();
new Thread(()->{
int i=0;
//class->运行时jit编译->汇编指令->重排序
while (demo.flag) {//指令重排序导致死循环
i++;
}
// if (demo.flag) {
// while (true) {
// i++;
// }
// }
System.out.println(i);
}).start();
TimeUnit.SECONDS.sleep(2);
demo.flag=false;
System.out.println("重置了");
}
}
说明:在java运行参数加上server时候,为提高性能,会执行jit然后指令重排,导致实际上执行的是注释的代码,从而导致死循环
- 工作内存缓存
其实也就是cpu缓存,但是实际上该缓存只会让另外线程晚一点发现变量已经改为false了,而不是导致死循环
1.png
-
指令重排
出了CPU会进行指令重排,Java编程语言的语义允许编译器和微处理器执行优化,这些优化可以与不正确的同步代码交互,从而产生看似矛盾的行为。
2.png
从上图可知,左边的重排就会导致结果的未可知。

- 共享变量描述
可以在线程之间共享的内存称为共享内存或堆内存。例如:实例字段,静态字段和数组元素都存储在堆内存中。
- Happens-before先行发生原则:下列的时候jit不进行指令重排,同时jit会调用cpu的内存屏障禁止cpu级别的指令重排
happens-before关系主要用于强调两个有冲突的动作之间的顺序,以及定义数据争用的发生时机。
具体虚拟机的实现,有必要确保以下原则的成立
- 某个线程中的每个动作都happens-before该线程中该动作后面的动作
- 某个管程(监视器)上的unlock动作happens-before同一个管程上后续的lock动作()
- 对某个volatile字段的写操作happens-before每个后续对该volatile字段的读操作
- 对某个线程对象上调用start()方法happens-before该启动了的线程中的任意动作
- 某个线程中的所有动作happens-before任意其他线程成功从该线程对象上的join()返回
- 如果某个动作a happens-before动作 b,且b happens-before动作c,则有 a happens-before c
管程概念补充
1. 管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
2. 进程只能互斥得使用管程,即当一个进程使用管程时,另一个进程必须等待。当一个进程使用完管程后,它必须释放管程并唤醒等待管程的某一个进程。
3. 在管程入口处的等待队列称为入口等待队列,由于进程会执行唤醒操作,因此可能有多个等待使用管程的队列,这样的队列称为紧急队列,它的优先级高于等待队列。
针对第二条:如下加上同步关键字则jit就不会指令重排,不会死循环
public static void main(String[] args) throws InterruptedException {
VisibilityDemo demo=new VisibilityDemo();
new Thread(()->{
int i=0;
//class->运行时jit编译->汇编指令->重排序
while (demo.flag) {//指令重排序导致死循环
synchronized (this) {
i++;
}
}
System.out.println(i);
}).start();
TimeUnit.SECONDS.sleep(2);
demo.flag=false;
System.out.println("重置了");
}
- volatile关键字
可见性问题:让一个线程对共享变量的修改,能够及时的被其他线程看到
根据JMM中规定的happen before和同步原则:
对某个volatile字段的写操作happens-before每个后续对该volatile字段的读操作。
对volatile变量的v的写入,与所有其他线程后续对v的读同步
- 要满足这些条件,所以volatile关键字有以下功能
- 禁止缓存(所有缓存,不但指cpu缓存,甚至包含jvm的缓存等等):volatile变量的访问控制符会加个ACC_VOLATILE(禁止缓存can not be cache)
- 对volatile变量相关的指令不做重排序
针对第三条:加上volatile关键字也禁止了指令重排,所以也可以避免死循环
public class VisibilityDemo {
private volatile boolean flag=true;
public static void main(String[] args) throws InterruptedException {
VisibilityDemo demo=new VisibilityDemo();
new Thread(()->{
int i=0;
//class->运行时jit编译->汇编指令->重排序
while (demo.flag) {//指令重排序导致死循环
i++;
}
System.out.println(i);
}).start();
TimeUnit.SECONDS.sleep(2);
demo.flag=false;
System.out.println("重置了");
}
}
说明:volatile修饰的关键字,则没有缓存的说法了,每次其他线程或者本线程读取的时候都会从主内存读取,而且修饰之后,jit也不会对这部分进行指令重排,从而正常结束
-
word tearing字节处理
4.png
-
double和long的特殊处理
5.png
- final在JMM中处理
- final在该对象的构造函数中设置对象的字段,当线程看到该对象时,将始终看到该对象的final字段的正确构造版本。伪代码示例:f=new finalDemo();读取到的f.x一定时最新的,x为final字段。如果不是final修饰的话,则不能保证多线程读取时候初始一定是初始化之后的值,偶尔可以看到默认值
- 如果在构造函数中设置字段后发生读取,则会看到该final字段分配的值,否则它将看到默认值
public class Final {
final int x;
int y;
public Final(){
x=1;
y=x;
System.out.println(y);
}
public static void main(String[] args) {
Final f=new Final();
}
}
如果没设置final修饰x,则可能出现y是默认值的情况,如果加了final修饰那就肯定是1
- 读取该共享对象的final成员变量之前,先要读取共享对象。
伪代码:r=new ReferenceObj();k=r.f;这两个操作不能重排序 - 通常static final是不可以修改的字段,然后System.in,System.out和System.err是static final字段,历史遗留原因,可以通过set方法改变,称这些字段为写保护,以区别于普通final字段
网友评论