JMM(Java Memory Model,Java内存模型),是一种抽象的概念 并不真实存在
,它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式.
JMM关于同步规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
由于JVM运行程序的实体是线程,
而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间)
,工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域
,所有线程都可访问
,但线程对变量的操作(读取赋值等)必须在工作内存中进行
,首先要将变量从主内存拷贝到自己的工作空间
,然后对变量进行操作,操作完成再将变量写回主内存
,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝
,因此不同的线程无法访问对方的工作内存,此案成间的通讯(传值) 必须通过主内存来完成,其简要访问过程如下图:
综上,JMM内部就要保持程序的可见性、原子性和有序性
1 可见性(Visibility)
1.1 描述
通过前面对JMM的介绍,我们知道各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存操作后再写回主内存中的.这就可能存在一个线程AAA修改了共享变量X的值还未写回主内存中时 ,另外一个线程BBB又对内存中的一个共享变量X进行操作,但此时A线程工作内存中的共享比那里X对线程B来说并不不可见.这种工作内存与主内存同步延迟现象就造成了可见性问题.
1.2 实现可见性代码
不加volatile情况下
public class Test01 { int i = 100; void methodA() { this.i = 0; } public static void main(String[] args) { Test01 data = new Test01(); new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } data.methodA(); }).start(); while (data.i == 100) { //while里不能写代码,因为可能会造成线程调度,main线程由等待变为执行时就会主动获取修改后的值 } System.out.println("0"); } } 复制代码
运行结果:
加上volatile情况下:
public class Test01 { volatile int i = 100; void methodA() { this.i = 0; } public static void main(String[] args) { Test01 data = new Test01(); new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } data.methodA(); }).start(); while (data.i == 100) { //while里不能写代码,因为可能会造成线程调度,main线程由等待变为执行时就会主动获取修改后的值 } System.out.println("0"); } } 复制代码
运行结果:
2 原子性(Atomicity)
volatile不保证原子性,synchronized既保证可见性又保证原子性
2.1 描述
原子性是指不可分割,完整性,也即某个线程正在做某个具体业务时,中问不可以被加塞或者被分割。需要整体完整要么同时成功,要么同时失败。
2.2 实现原子性代码
验证volatile不保证原子性
public class Test01 { volatile int i = 0; void add() { this.i++; } public static void main(String[] args) { Test01 data = new Test01(); for (int i = 0; i < 100; i++) { new Thread(() -> { for (int j = 0; j < 100; j++) { data.add(); } }).start(); new Thread(() -> { for (int j = 0; j < 100; j++) { data.add(); } }).start(); } while (Thread.activeCount() > 2) { Thread.yield();//线程等待 } System.out.println(data.i); } } 复制代码
结果:
使用synchronized保证原子性
public class Test01 { volatile int i = 0; synchronized void add() { this.i++; } public static void main(String[] args) { Test01 data = new Test01(); for (int i = 0; i < 100; i++) { new Thread(() -> { for (int j = 0; j < 100; j++) { data.add(); } }).start(); new Thread(() -> { for (int j = 0; j < 100; j++) { data.add(); } }).start(); } while (Thread.activeCount() > 2) { Thread.yield();//线程等待 } System.out.println(data.i); } } 复制代码
结果:
使用AtomicInteger保证原子性
public class Test01 { AtomicInteger atomicInteger=new AtomicInteger(); void add() { atomicInteger.getAndIncrement(); } public static void main(String[] args) { Test01 data = new Test01(); for (int i = 0; i < 100; i++) { new Thread(() -> { for (int j = 0; j < 100; j++) { data.add(); } }).start(); new Thread(() -> { for (int j = 0; j < 100; j++) { data.add(); } }).start(); } while (Thread.activeCount() > 2) { Thread.yield();//线程等待 } System.out.println(data.atomicInteger); } } 复制代码
结果:
3 有序性(Ordering)
计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排,一把分为以下几种
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致.
处理器在进行重新排序是必须要考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测
3.1 指令重排情况
情况一:
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 Test{ int i = 0; boolean isOk = false; public void methodA(){ i = 1; //语句1 isOk = true; //语句2 } public void methodB(){ if(isOk){ i++; System.out.print(i); } } } 复制代码
这种情况下,由于语句1 和语句2 没有依赖性,如果语句2在语句1之前执行,则会导致methodB方法提前执行
3.2 禁止指令重排
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象,先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
- 一是保证特定操作的执行顺序,
- 二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插久内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。