再介绍volatile之前,我们了解一下有什么状况,让我们需要用到volatile。
这就要说起导致线程安全的——可见性了。
举个例子
//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;
假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。
此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.
这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。
下面是展现问题的具体代码
package com.imikasa.test1;
public class TestThread2 {
static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while(flag){
// System.out.println("hello");
}
}).start();
System.out.println("1s后停止线程");
Thread.sleep(1000);
flag = false;
}
}
程序运行之后会一直运行是循环,主线程修改的flag的值不会被其他线程知道(cpu缓存引起),可见性问题通常都是由一个线程修改共享变量,其他线程读取那个共享变量引起的...
如何保证可见性
Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时static volatile boolean flag = true;
,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
值得注意的是,在上面我们测试展示可见性问题的代码中,我们把死循环中的打印语句注释去掉,在运行,发现一秒后,死循环结束,保证了可见性,这是为什么呢?
原因很简单,我们看println()的源码就知道了
println()
因为通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。但是缺点是synchronized属于重量级操作,性能相对更低
网友评论