JMM模型

指Java内存模型(Java Memory Model,JMM),不是内存布局,不是指所谓的堆、栈、方法区,用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。。
每个Java线程都有自己的工作内存。操作共享数据,首先将数据从主内存拷贝到线程自己的工作内存中,得到一份拷贝,操作完毕后在写会主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。

对volatile的理解

volatile 是 Java虚拟机提供的轻量级同步机制

  • 保证可见性(一个线程对共享变量做了修改之后,其他的线程立即能够看到(感知到)该变量这种修改(变化))
解决缓存一致性方案有两种:
1. 通过在总线加LOCK#锁的方式;
2. 通过缓存一致性协议。
但是方案1存在一个问题,它是采用一种独占的方式来实现的,即总线加LOCK#锁的话,只能有一个CPU能够运行,其他CPU都得阻塞,效率较为低下。
第二种方案,缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。
所以JMM就解决这个问题。
有volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令,该指令在多核处理器下会引发两件事情。将当前处理器缓存行数据刷写到系统主内存。
这个刷写回主内存的操作会使其他CPU缓存的该共享变量内存地址的数据无效。这样就保证了多个处理器的缓存是一致的,对应的处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器缓存行设置无效状态,当处理器对这个数据进行修改操作的时候会重新从主内存中把数据读取到缓存里。
  • 不保证原子性
  • 禁止指令重排

指令重排:

计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种

编译器优化的重排

编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

指令并行的重排

现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序

内存系统的重排

由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。

其中编译器优化的重排属于编译期重排,指令并行的重排和内存系统的重排属于处理器重排,在多线程环境中,这些重排优化可能会导致程序出现内存可见性问题

先了解一个概念,内存屏障又称内存栅栏,是一个CPU指令,它的作用有两个:

一是保证特定操作的执行顺序

二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重新排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

DEMO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

class MyData{
volatile int number = 0;
public void addTo60(){
this.number = 60;
}
> public void addPlusPlus(){
number++;
}

AtomicInteger atomicInteger = new AtomicInteger();
public void addMyAtommic(){
atomicInteger.getAndIncrement();
}
}
/*
1 验证volatile的可见性
1.1 加入int number=0,number变量之前根本没有添加volatile关键字修饰,没有可见性
1.2 添加了volatile,可以解决可见性问题
2 验证volatile不保证原子性

2.1 原子性是不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者分割。
需要整体完成,要么同时成功,要么同时失败。

2.2 volatile不可以保证原子性演示

2.3 如何解决原子性
*加sync
*使用我们的JUC下AtomicInteger

* */
public class VolatileDemo {
public static void main(String[] args){
MyData myData = new MyData();
for (int i = 1; i <= 20 ; i++) {
new Thread(()->{
for (int j = 1; j <= 1000 ; j++) {
myData.addPlusPlus();
myData.addMyAtommic();
}
},String.valueOf(i)).start();
}

//需要等待上述20个线程都计算完成后,再用main线程去的最终的结果是多少?
// try{TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}
while(Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t finnally number value: "+myData.number);
System.out.println(Thread.currentThread().getName()+"\t finnally number value: "+myData.atomicInteger);
}
}

##多线程下单例模式的安全问题
###DCL(Double Check Lock)模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SingletonDemo {
//volatile 添加在singletondemo对象上 防止指令重排
private static SingletonDemo singletonDemo=null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName()+"\t 我是构造方法");
}
//DCL模式 Double Check Lock 双端检索机制:在加锁前后都进行判断
public static SingletonDemo getInstance(){
if (singletonDemo==null){
synchronized (SingletonDemo.class){
if (singletonDemo==null){
singletonDemo=new SingletonDemo();
}
}
}
return singletonDemo;
}

public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
SingletonDemo.getInstance();
},String.valueOf(i+1)).start();
}
}
}

instance=new SingletonDemo();可以大致分为三步

1
2
3
memory = allocate();     //1.分配内存
instance(memory); //2.初始化对象
instance = memory; //3.设置引用地址

其中2、3没有数据依赖关系,可能发生重排。如果发生,此时内存已经分配,那么instance=memory不为null。如果此时线程挂起,instance(memory)还未执行,对象还未初始化。由于instance!=null,所以两次判断都跳过,最后返回的instance没有任何内容,还没初始化。

解决的方法就是对singletondemo对象添加上volatile关键字,禁止指令重排。