1. java并发编程之可见性
可见性的定义是指一个线程对共享变量的修改,另一个线程能够立马看到,但是再任何情况下都是可见的吗?我们来看下面这段代码
public class VolatileDemo {
private static Boolean FLAG = true;
public static void main(String[] args) throws InterruptedException {
System.out.println("main thread start!");
new Thread(() -> {
System.out.println("children thread start!");
while(FLAG) {
};
System.out.println("children thread end!");
}).start();
Thread.sleep(1000L);
FLAG = false;
System.out.println("main thread end!");
}
}
运行结果:
可以看到主线程修改了FLAG的值,按正常逻辑,子线程的会跳出while循环直到停止,但是事实是子线程一直在while循环未停止,为什么会出现这样的情况?这就是java可见性的问题,因为主线程修改了FLAG的值,子线程不能立马看到
2. 可见性产能原因
在单核时代,所有线程都在一个cpu上运行,不存在上面的那种情况,后来到了多核时代,由于cpu的速度越来越快,内存的速度已经远远跟不上了,所以出了cpu缓存,cpu在会先从cpu缓存中读取数据,所以在多核cpu下,缓存带来了可见性问题
如下图:
多线程对共享变量的操作过程:
主线程发起修改共享变量FLAG=flase—>写入主线程对应的高速缓存—>写入共享区域主内存---->写入子线程对应的高速缓存—>子线程读取到修改后的FLAG
在主线程修改了FLAG的值后,没有立即刷新到子线程所对应的缓存中,子线程读取到的FLAG值依然是true,所以子线程还在while循环中
3. volatile关键字保证多线程的可见性
为了解决上面的可见性问题,java提供了volatile,synchronized关键字等来解决可见性问题,这篇文章我们来详细的讲解volatile关键字
volatile关键字的作用:
- 保证不同线程对共享变量修改后,这个修改后的值对于其他线程是可见的,即其他线 程能够立即读 取到修改后的值
- 禁止指令重排序,阻止编译器对代码优化
上面一段代码我们使用valotile关键字如下:
public class VolatileDemo {
private static volatile Boolean FLAG = true;
public static void main(String[] args) throws InterruptedException {
System.out.println("main thread start!");
new Thread(() -> {
System.out.println("children thread start!");
while(FLAG) {
};
System.out.println("children thread end!");
}).start();
Thread.sleep(1000L);
FLAG = false;
System.out.println("main thread end!");
}
}
运行结果:
从运行结果上来看,是得到了正常逻辑的结果。
这是volatile关键字上面第一点的作用,具体变现为:
被valotile修饰的共享变量在某个线程被修改后会立即写入主内存并且强制清空其他线程的高速缓存,也就是其他线程会强制去主内存中读取该共享变量的值,这样读取到的值都是最新的,保证了多线程之间的可见性
4. volatile关键字禁止指令重排序
上述volatile关键字作用第二点禁止指令重排序,这里面涉及到java并发编程的有序性问题,JVM在编译java代码时在不改变程序执行结果的前提下会对指令进行优化,有些指令可能会重新排序以提高运行效率,但是这种不改变执行结果只针对单线程,在多线程下指令重排序可能会带来并发问题,我们以经典的单例模式懒汉式双重锁为例:
public class SimpleInstance {
public static SimpleInstance simpleInstance;
private SimpleInstance() {}
public static SimpleInstance getInstance() {
if (simpleInstance == null) {
synchronized (SimpleInstance.class) {
if (simpleInstance == null) {
simpleInstance = new SimpleInstance();
}
}
}
return simpleInstance;
}
}
这里需要指出的是volatile还有一个特性,那就是不保证原子性
上面代码在new一个SimpleInstance对象,其实整个操作并不是原子性性的,分为以下三步:
- 给SimpleInstance对象分配内存空间
- 初始化SimpleInstance对象
- 将SimpleInstance对象指向其被分配的内存空间
这三条指令其实顺序不一定是按照上述顺序,JVM会对指令重排序,如果上述指令发生了重排序,顺序会变成以下这样
- 给SimpleInstance对象分配内存空间
- 将SimpleInstance对象指向其被分配的内存空间
- 初始化SimpleInstance对象
即第二步和第三步顺序调换,这样在多线程下,可能会发生这样的情况:线程A拿到synchronized同步锁去new一个SimpleInstance对象,执行到完第二步后,线程B抢占到cpu时间片,此时simpleInstance对象已经被分配了内存空间,但对象没有被初始化,而线程B执行第一个 if (simpleInstance == null)语句,由于simpleInstance对象已经被分配了内存空间,得到结果会是flase并返回一个null对象,这明显不是我们想要的结果,这种情况解决办法就是利用volatile禁止指令重排序的作用,用volatile修饰simpleInstance变量,这样new一个SimpleInstance对象的指令就不会重排序,这样即便线程A的new SimpleInstance()操作还没执行完就切换到线程B也不会发生线程B返回一个null对象的现象了,修改后的代码如下:
public class SimpleInstance {
public static volatile SimpleInstance simpleInstance;
private SimpleInstance() {}
public static SimpleInstance getInstance() {
if (simpleInstance == null) {
synchronized (SimpleInstance.class) {
if (simpleInstance == null) {
simpleInstance = new SimpleInstance();
}
}
}
return simpleInstance;
}
}