保证可见性
不保证原子性
禁止指令重排
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过规范定义了程序中的各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。
JMM的同步规定:
线程解锁前,必须把共享变量的值刷新回主内存
线程加锁前,必须读取主内存的最新值到自己的工作内存
加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存时每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回到主内存,不能直接操作主内存中的变量,各个线程的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要的访问过程如下图:
JMM的三大特性
JMM是线程安全性获得的保证。因为JMM具有如下特点:
可见性:从主内存拷贝变量后,如果某一个线程在自己的工作内存中对变量进行了修改,然后写回了主内存,其它线程能第一时间看到,这就叫作可见性。
原子性:不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割
有序性:禁止指令重排,按照规定的顺序去执行
综上所述,volatile满足JMM三大特性中的两个,即可见性和有序性,volatile并不满足原子性,所以说volatile是轻量级的同步机制。
代码示例:
/**
* Created by salmonzhang on 2020/7/4.
* 可见性代码实例
*/
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in ...");
//暂停一会儿线程
try{ TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
myData.addTo10();
System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);
},"Thread01").start();
while (myData.number == 0) {
//main线程一直在这里等待,直到number的值不再等于零
}
System.out.println(Thread.currentThread().getName()+"\t mission is over , number updated ...");
}
}
class MyData{
// int number = 0; // 这里没有加volatile
volatile int number = 0; // 这里加了volatile
public void addTo10() {
this.number = 10;
}
}
没有加volatile的运行结果:
加了volatile的运行结果:
总结:如果不加volatile关键字,则主线程会进入死循环,加了volatile时主线程运行正常,可以正常退出,说明加了volatile关键字后,当有一个线程修改了变量的值,其它线程会在第一时间知道,当前值作废,重新从主内存中获取值。这种修改变量的值,让其它线程第一时间知道,就叫作可见性。
代码示例:
/**
* Created by salmonzhang on 2020/7/4.
* 验证volatile不保证原子性
* 原子性是什么意思:
* 不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。
* 需要整体完整,要么同时成功,要么同时失败。保证数据的原子一致性
*/
public class VolatileDemo2 {
public static void main(String[] args) {
MyData2 myData2 = new MyData2();
for (int i = 1; i <= 20; i++){
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myData2.addPlusPuls();
}
},String.valueOf(i)).start();
}
//需要等待上面20个线程全部执行完成后,再用main线程取得最终的结果值看看是多少?
while (Thread.activeCount() > 2) { //后台默认有两个线程:GC线程和main线程
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "finally number value = " + myData2.number);
}
}
class MyData2{
volatile int number = 0; // 这里加了volatile
public void addPlusPuls() {
number++;
}
}
运行结果:
从代码的运行结果会发现:会出现number最终的结果有可能出现不是20000的时候,这就证明了volatile不能保证原子性。
为什么volatile不能保证原子性?
由于多线程进程调度的关系,在某一时间段出现了丢失写值的情况。因为线程切换太快,会出现后面的线程会把前面的线程的值刚好覆盖。
例如:Thread1和Thread2同时从主内存中读取number的值1到自己的工作内存,并同时进行了+1的动作,当Thread1将2写会主内存的时候,由于线程的调度原因,Thread2并没有第一时间知道Thread1已经将number的值改为了2,而是直接将Thread1改的number值进行覆盖,这样就会导致数据丢失。
解决方案:
2.1. 直接在addPlusPuls前面加上synchronized
class MyData2{
volatile int number = 0; // 这里加了volatile
public synchronized void addPlusPuls() {
number++;
}
}
但是为了保证一个number++的原子性直接用synchronized,感觉有点重,类似于“杀鸡用牛刀”
2.2 用atomic
class MyData2{
AtomicInteger number = new AtomicInteger();
public void addPlusPuls() {
number.getAndIncrement();
}
}
计算机在执行程序时,为了提高性能,编译器的处理器通常会对指令做重排,一般有三种重排:
编译器的重排
指令并行的重排
内存系统的重排
单线程环境里确保程序最终执行的结果和代码执行的结果一致
处理器在进行重排序时,必须考虑指令之间的数据依懒性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证用的变量能否一致性是无法确定的,结果也是无法预测的
重排案例一:
public void mySort(){
int x=11;//语句1
int y=12;//语句2
x=x+5;//语句3
y=x*x;//语句4
}
计算机执行的顺序可能是:
1234
2134
1324
问题:
请问语句4可以重排后变成第一条码?
存在数据的依赖性,所以没办法排到第一个
指令重排代码示例:
public class ReSortSeqDemo {
int a = 0;
boolean flag = false;
public void method01() {
a = 1; // 这里的a和flag没有禁止指令重排,所以在多线程环境中就有可能出现问题
flag = true;
}
public void method02() {
if (flag) {
a = a + 3;
System.out.println("a = " + a);
}
}
}
这里的a和flag没有禁止指令重排,所以在多线程环境中就有可能出现问题,例如指令重排后,method01中的flag=true先被Thread1执行了,此时Thread2又抢占到了线程资源去执行method02()时,此时的运行结果就是有问题的。运行结果就是a = 3,而不是正常情况下的a = 4
代码示例:
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName() + "\t 我是SingletonDemo的构造方法");
};
//synchronized 解决单例的多线程问题,会显得比较重,整个方法都被锁住了,不建议这么写
public static SingletonDemo getInstance(){
if (instance == null) {
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
//并发多线程后,会出现构造函数多次执行的情况
for (int i = 1; i <= 10; i++){
new Thread(() -> {
SingletonDemo.getInstance();
},String.valueOf(i)).start();
}
}
}
运行结果:
1.代码示例:
public class SingletonDemo {
private static volatile SingletonDemo instance = null; //加上volatile,禁止编译器指令重排
private SingletonDemo(){
System.out.println(Thread.currentThread().getName() + "\t 我是SingletonDemo的构造方法");
};
/**
* DCL (double check Lock 双端检索机制)
*/
public static SingletonDemo getInstance(){
if (instance == null) {
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
//并发多线程后,会出现构造函数多次执行的情况
for (int i = 1; i <= 10; i++){
new Thread(() -> {
SingletonDemo.getInstance();
},String.valueOf(i)).start();
}
}
}
总结:
如果没有加 volatile 就不一定是线程安全的,原因是指令重排序的存在,加入 volatile 可以禁止指令重排。
原因是在于某一个线程执行到第一次检测,读取到的 instance 不为 null 时,instance 的引用对象可能还没有完成初始化。
instance = new Singleton() 可以分为以下三步完成
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.初始化对象
所以不加 volatile 返回的实例不为空,但可能是未初始化的实例