java memory model :java内存模型
JMM关于同步的规定:
- 线程解锁前,必须将共享变量的值刷新回主内存;
- 线程加锁前,必须读取主内存的最新值到自己的工作内存;
- 加锁解锁为同一把锁
配图说明:
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图
JMM三个属性
- 可见性
- 原子性
- 有序性
volatile三大特性:
- 保证可见性
- 不保证原子性
- 禁止指令重排
1.保证可见性(base on 内存屏障)
代码路径:D:\JavaCode\JUC\src\main\java\jmm\JMM01.java
//1.线程操作资源类class MyClass{//********************变量的可见性**********************private volatile int num=0;public int getNum(){return this.num;}public void changeNum(){this.num=60;}}//2.入口类public class JMM01 {public static void main(String[] args) {MyClass myClass=new MyClass();new Thread(()->{System.out.println(Thread.currentThread().getName()+"\t come in");try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}myClass.changeNum();System.out.println(Thread.currentThread().getName()+"\t change num to "+myClass.getNum());},"aaa").start();while(myClass.getNum()==0){}System.out.println("主线程已经知道主内存中的共享变量发生了改变!");}}
分析:
- 当线程操纵资源类中的num没有volatile修饰时,由于main线程无法知晓主内存中num变量的变化,因此会卡在while循环处
- 当volatile修饰num时,当aaa线程修改完num的值并将该值刷新回主内存后,主线程能够立马知晓该变化,从而退出while循环
1的输出:
aaa come in
aaa change num to 60
程序尚未结束
2的输出:
aaa come in
主线程已经知道主内存中的共享变量发生了改变!
aaa change num to 60
程序已经结束
2.不保证原子性
//1.线程操作资源类class MyClass{private volatile int num=0;public int getNum(){return this.num;}public void changeNum(){this.num=60;}public void addPlusPlus(){num++;}}//2.入口类public class JMM01 {public static void main(String[] args) {MyClass myClass=new MyClass();for(int i=0;i<20;i++){new Thread(()->{for(int j=0;j<10000;j++) {myClass.addPlusPlus();}},String.valueOf(i)).start();}//等待上面的20个计算线程结束后就只剩main线程和GC线程while(Thread.activeCount()>2){Thread.yield();}System.out.println(Thread.currentThread().getName()+"\t final num is: "+myClass.getNum());}}
**运行输出:
main final num is: 63638
每次的final num都不一样
但是都不会是理想中的20*10000
说明volatile并不能保证原子性
解决方案1:
addPlusPlus方法使用synchronized修饰
此时final num就会是20*10000
但是这样叫做杀鸡用牛刀
解决方案2:
变量num改用AtomicInteger变量
2.5 使用AtomicInteger变量保证原子性
package jmm;import java.util.concurrent.atomic.AtomicInteger;//1.线程操作资源类class MyClass{private volatile int num=0;private AtomicInteger atomicInteger=new AtomicInteger();public int getNum(){return this.num;}public AtomicInteger getAtomicInteger(){return atomicInteger;}public void changeNum(){this.num=60;}public void addPlusPlus(){num++;}public void addAtomicInteger(){atomicInteger.getAndIncrement();}}public class JMM01 {public static void main(String[] args) {MyClass myClass=new MyClass();for(int i=0;i<20;i++){new Thread(()->{for(int j=0;j<10000;j++) {myClass.addPlusPlus();myClass.addAtomicInteger();}},String.valueOf(i)).start();}//等待上面的20个计算线程结束后就只剩main线程和GC线程while(Thread.activeCount()>2){Thread.yield();}System.out.println(Thread.currentThread().getName()+"\t final num is: "+myClass.getNum());System.out.println(Thread.currentThread().getName()+"\t final atomic is: "+myClass.getAtomicInteger());}}
输出结果:
main final num is: 114943
main final atomic is: 200000
3.禁止指令重排
单线程
单线程下指令重排之后程序执行的结果跟没有重排的结果是一致的
多线程下指令重排会带来混乱
指令重排遵守的规则:
保证数据依赖性
举例说明:
- int x=11
- int y=12;
- x=x+5;
- y=x*x;
上面依次有四个语句
顺序执行:1234,x=16,y=256
重排1:1324,x=16,y=256
重排2:2134,x=16,y=256
即单线程下顺序执行和指令重排的结果是一致的
而指令4不可以排在第一条,因为其依赖x,这就是数据依赖性
多线程
int a,b,x,y=0;
| 线程1 | 线程2 |
|---|---|
| x=a | y=b |
| b=1 | a=2 |
| 最终:x=0,y=0 | |
| 指令重排 | 指令重排 |
| b=1 | a=2 |
| x=a | y=b |
| 最终:x=2,y=1 |
两个线程各自内部进行指令重排
对原有的x和y的结果产生了影响
内存屏障
内存屏障:memory barrier
本身为一个cpu指令
用途有二:
- 保证特定操作的执行顺序
- 保证某些变量的内存可见性(volatile可见性的基石)
网上的例子
public class JMM02 {private int a=0;boolean flag=false;public void method1(){a=1;flag=true;}public void method2(){if(flag){a=a+5;if(a==5){System.out.println("I am 5");}}}public static void main(String[] args) {JMM02 jmm02=new JMM02();for(int i=0;i<1000000;i++){new Thread(()->{jmm02.method1();}).start();new Thread(()->{jmm02.method2();}).start();}}}
分析:
如果方法1先执行,
没有发生指令重排的话:那么a就等于6,
发生指令重排的话:那么a就等于5,
但是我怎么都得不到5。
DCL双重检查例子
双重检查是单例模式中的一种
**完整的DCL需要对instance进行volatile修饰*
**完整的DCL需要对instance进行volatile修饰*
**完整的DCL需要对instance进行volatile修饰*
synchronized不能禁止指令重排

