1 volatile是Java虚拟机提供的轻量级的同步机制(保证可见性,不保证原子性,禁止指令重排序)
保证可见性
volatile保证可见性Demo(可测试volatile删除与添加的运行差异):
public class VisibleTest {
public static void main(String[] args) {
VisibleData data = new VisibleData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " in...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
data.setNum(77);
System.out.println("Data num changed to " + data.getNum() + " in " + Thread.currentThread().getName());
}, "Thread 666").start();
while (data.num == 0) {
}
System.out.println("Data num is " + data.num + " now in main thread.");
}
static class VisibleData {
private volatile int num = 0;
public void setNum(int num) {
this.num = num;
}
public int getNum() {
return num;
}
}
}
不保证原子性
代码示例(可用AtomicInteger解决原子性问题):
public class AutomaticTest {
public static void main(String[] args) {
VisibleData data = new VisibleData();
for (int i = 0; i < 50; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
data.addNum();
}
}).start();
}
while (Thread.activeCount() > 2) {
//程序启动后,包含main线程,垃圾回收线程,大于2,说明for循环中还有线程未结束
}
System.out.println(data.getNum());
}
static class VisibleData {
private volatile int num = 0;
public void setNum(int num) {
this.num = num;
}
public int getNum() {
return num;
}
public void addNum() {
num++;
}
}
}
禁止指令重排序
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令重排序,一般分为以下3种:
源代码-->编译器优化的重排-->指令并行的重排-->内存系统的重排-->最终执行的指令
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
处理器在进行重排序时必须要考虑指令之间的数据依赖性。
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。
内存屏障(Memory Barrier),又称内存栅栏,是一个CPU指令,它的作用有两个:
-
保证特定操作的顺序执行
-
保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
由于编译器和处理器都能执行指令重排优化,如果在指令间插入一条Memory Barrier会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。
内存屏障的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
2 JMM(Java内存模型)
可见性
原子性
有序性
JMM(Java内存模型 Java Memory Model)本身是一个抽象的概念,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
1 线程解锁前,必须把共享变量的值刷新回主内存
2 线程加锁前,必须读取主内存的最新值到自己的工作内存
3 加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先,要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝。因此,不同的线程间无法访问对方的工作内存。线程间的通信(传值)必须通过主内存来完成,其访问过程如下:
各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存。这可能就存在一个问题,一个线程A修改了共享变量X的值但还未写回到主内存时,另外一个线程B,也对主内存中同一个变量X进行操作,但此时A线程工作内存中共享变量X对线程B并不可见。这种工作内存与主内存同步延迟现象,就造成了可见性问题。
原子性: 不可分割,完整性,即某个线程在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整,要么同时成功,要么同时失败。
3 哪些地方用到过volatile(单例模式双重检查锁DCL为什么要加volatile)
class Singleton{
private static volatile Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton==null){
synchronized(Singleton.class){
if(singleton==null){
singleton=new Singleton();
}
}
}
return singleton;
}
}
DCL(Double Check Lock)机制不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排。
原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。
instance=new SingletonTest();可以分为三步:
memory=allocate(); // 1.分配对象内存空间
instance(memory); //2.初始化对象
instance=memory;// 3.设置instance指向刚分配的内存地址,此时instance!=null
步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中没有改变,因此,这种重排优化是允许的。
memory=allocate(); //1.分配对象内存空间
instance=memory; //3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成
instance(memory); //2.初始化对象
但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。
所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。