JMM-java memory model-java内存模型
1.原子性,保证指令不受线程上下文切换的影响。
2.可见性,保证指令不会受cpu缓存的影响。
3.有序性,保证指令不会受cpu指令并行优化的影响。
@Slf4j
public class ThreadSee {
static boolean flag = true;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(flag) {
}
}, "t1");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("退出t1线程");
flag =false;
}
}
主线程执行flag=false后,子线程t1并没有停下来,为啥?
因为t1线程要频繁的从主内存中读取flag的值,JIT编译器会将flag的值缓存到自己工作内存中的高速缓存中,减少对主内存flag的访问,提高效率。1秒之后,main线程修改了flag的值并同步至主存,而t1线程永远从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值。
一个线程对主存中的数据进行了修改,而其他线程不可见,导致了问题。
解决办法:是给变量flag添加volatile修饰。volatile表示易变的关键字。
volatile static boolean flag = true;
问题:如果不使用volatile关键字,然后循环中添加日志打印代码while(flag){log.info("111")},这样也能让t1线程停下来。使用System.out.println()方法也可以让t1线程停下来。
原因:是因为println方法内部其实使用了Synchronized关键字,所以保证的线程的可见性。
使用volatile关键字后,t1线程每循环一次就会从主内存中获取最新的值。
volatile可以用来修饰成员变量和静态成员变量。避免线程从自己的工作缓存中查找变量的值,线程操作volatile变量都是直接操作主存。
volatile修饰的都是多个线程共享的变量,不会修饰局部变量,因为局部变量每个线程都有一份。
volatile可以保证变量的可见性。
Synchronized也可以保证共享变量的可见性
@Slf4j
public class ThreadSee {
static Object lock = new Object();
static boolean flag = true;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(true) {
synchronized (lock) {
if(!flag) {
break;
}
}
}
}, "t1");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("退出t1线程");
synchronized (lock) {
flag = false;
}
}
}
synchronized要创建monitor,属于比较重量级的操作,volatile相比较就更轻量,所以在解决可见性方面推荐使用volatile。
volatile只能保证可见性,不能保证原子性。适合用于一个线程修改,多个线程读的情况。
synchronized既可以保证原子性又可以保证可见性。但是属于重量级操作,性能相对更低。
volatile可见性、有序性的原理
底层实现原理是内存屏障,memory barrier
对volatile变量的写指令后会加入写屏障,写屏障之前的所有赋值操作都会同步到主内存中,哪怕变量没有被volatile修饰,也会同步到主内存中。保证写屏障之前的代码不会发生指令重排。
对volatile变量的读指令后会加入读屏障,读屏障之后对共享变量的读取,加载的是主存中最新数据。保证读屏障之后的代码不会发生指令重排序。
volatile不能解决指令交错,有序性是保证了本线程内相关代码不被重排序。
volatile只能保证可见性、有序性。
synchronized保证原子性、可见性和有序性。