JMM知识点总结
JMM知识点总结
一、什么是JMM?
不知道大家在学习的过程有没有思考过这两个问题
- 为什么说java是跨平台语言
- 导致并发问题的原因是什么
第一个问题,我是这么理解的,代码运行本质上是将我们写的语言转换为操作系统可运行的指令集,而不同的操作系统可能有不同的cpu,对应了不同的指令集,比如windows就是不开源的,它只能运行在intel 86架构的cpu上,不能运行在ARM上,但linux开源,所有它有不同的版本,运行在不同的架构上,java针对这种情况,自己设计了一套内存模型来屏蔽这种差异,这也是JMM存在的原因之一。
第二个问题,才是JMM发光发热的原因。现代计算机越来越复杂,但也越来越快,处理的东西也越来越多,其原因之一是现代计算机采用了多核结构,也就是存在多个CPU core,cpu多是可以同时处理不同的事情,但硬件发展还根本上这么大的进步,cpu处理数据与内存处理数据不匹配,因此,又弄出了CPU缓存模型,也就是cpu cache,现代计算机将CPU cache又细分为三层,对应了L1,L2,L3,但是这又带来了新问题,不同的cpu core通常有自己的cache,在数据读写的过程中,难免出现数据不一致现象,cpu解决这个问题是制定了MESI协议等缓存一致协议,规定CPU在高速缓存和主内存交互时遵循的规范,而操作系统也要解决这个问题,原因是操作系统虚拟化了底层硬件,不同的cache在它眼里就是一样的,OS的解决办法就是指定内存模型来进行规范,无论是windows还是linux都有自己的内存模型。此外,发生并发安全问题的还有另一个因素,就是指令集重排序,它是编译器和处理器对代码的一种优化,但是又没有那么智能,有时候也会帮了倒忙。
JAVA虽然可以使用操作系统自己的内存模型,但作为跨平台语言,不能依靠操作系统,因此,它制定了自己的内存模型,规范了java源代码转换为CPU指令的过程,同时java模型也进行了优化,原本是java线程操作同一个主存,优化后抽象出一层“本地内存”(硬件上就是寄存器),这样就有了类似CPU缓存一致性问题,所有又赋予JAVA内存模型其他特性(原子性,可见性,有序性)和原则(happens-before原则)
二、JMM的三大特性
- 原子性
原子性存在多个计算机领域中,它的意思就是一组操作要么全部执行,要么全部不执行,众所周知,i++并不是一组原子操作,java中原子操作有基本类型的赋值(有些情况下,double和long类型并不是原子操作),所有reference引用的赋值操作,Atomic中的操作
原子性如何保证:
java中有两种操作保证原子性,一种是加锁如synchronized,另一种就是CAS算法思想设计的类
- 有序性
CPU和编译器会对代码进行一些优化,就是指令重排序优化,表现的现象为指令和java代码的顺序可能不一致,这就导致在并发编程的时候会出现一些稀奇古怪的问题
代码
public class OutOfOrderExecution { private static int x=0,y=0; private static int a=0,b=0; // 禁止重排序 // private volatile static int x=0,y=0; // private volatile static int a=0,b=0; public static void main(String[] args) throws Exception { int i = 0; for (; ; ) { i++; a=0; b=0; x=0; y=0; CountDownLatch latch = new CountDownLatch(1); Thread one = new Thread(new Runnable() { @Override public void run() { a = 1; x = b; } }); Thread two = new Thread(new Runnable() { @Override public void run() { b = 1; y = a; } }); one.start(); two.start(); one.join(); two.join(); String result="第"+i+"次"+"x= " + x + ",y= " + y; if(x==0&&y==0){ System.out.println(result); break; }else{ System.out.println(result); } } } }
代码分析
正常存在三种情况
- a=1,x=b,b=1,y=a --------> x=0,y=1
- b=1,y=a ,a=1,x=b --------> x=1,y=0
- a=1,b=1,y=a,x=b --------> x=1,y=1 (发生线程上下文切换)
异常情况
- y=a,a=1,x=b,b=1(不一定这么排) --------> x=0,y=0 (发生了指令重排序)
如果在本线程中,任何操作都是有序的,这里的有序可以理解为串行,但是从另一个线程观察这个线程,这个线程中所有操作都是无序的,因为存在指令重排序优化
有序性如何保证
volatile,synchronized都可以保证并发的有序性,volatile不解释,自身都有语义禁止重排序,synchronized保证的同一时刻一个变量(或者一段代码)只能由一个线程操作,这保证了as-if-serial语义,as-if-serial语义简单理解就是单线程环境下,重排序不能影响结果(如果单线程下,重排序导致结果错了,cpu和编译器纯纯坏蛋!)
- 可见性
可见性问题的根本原因就是CPU缓存存在多个,如果只有一个就不会发生可见性问题,所谓可见性问题就是别的线程改了某值,但是其他线程不一定知道,然后芭比q了
代码
public class FieldVisibility { int a=1; int b=2; // volatile int a=1; // volatile int b=2; public static void main(String[] args) throws Exception{ while(true){ FieldVisibility test = new FieldVisibility(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } test.change(); } }).start(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } test.print(); } }).start(); } } private void print() { System.out.println("b= "+b+",a="+a); } private void change() { a=3; b=a; } }
结果分析
正常情况
b=2,a=1 先运行print线程,再运行change线程
b=3,a=3 先运行change线程,再运行print线程
b=2,a=3 先运行print线程,然后发生上下文切换运行change线程,再运行print线程
异常情况
b=3,a=1 change运行完了,但是print线程没看到a的变换
如何保证可见性
依旧是volatile,这玩意还有可见性语义,此外还有JMM的灵魂happens-before也是来保证可见性,所谓的happens-before,简单来说这个原则规定动作A发生在动作B之前,那么动作B一定可以看见动作A,当然有着happens-before的synchronized也保证可见性,还有大家容易遗忘的final也能保证可见性,因为final的语义就是只要对象正确构造出来,那么任意线程都可以看见final初始化后的值且不变
4.happens-before原则
有几个规则
- 单线程规则:单线程中先运行的对后运行的是可见的,注意这不与重排序矛盾
- synchronized规则:先上锁的线程的操作结果对后上锁的线程是可见的
- volatile规则
- 线程启动规则:新启动的线程一定可以看见启动前操作的结果
- 线程join规则:想想main函数等待其他函数是为啥,不就是图他们的结果嘛
- happens-before是可以传递的
- 工具类中也有happens-before