java memory model :java内存模型

JMM关于同步的规定:

  1. 线程解锁前,必须将共享变量的值刷新回主内存;
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存;
  3. 加锁解锁为同一把锁

配图说明:
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图
image.png

JMM三个属性

  1. 可见性
  2. 原子性
  3. 有序性

volatile三大特性:

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

1.保证可见性(base on 内存屏障)

代码路径:D:\JavaCode\JUC\src\main\java\jmm\JMM01.java

  1. //1.线程操作资源类
  2. class MyClass{
  3. //********************变量的可见性**********************
  4. private volatile int num=0;
  5. public int getNum(){
  6. return this.num;
  7. }
  8. public void changeNum(){
  9. this.num=60;
  10. }
  11. }
  12. //2.入口类
  13. public class JMM01 {
  14. public static void main(String[] args) {
  15. MyClass myClass=new MyClass();
  16. new Thread(()->{
  17. System.out.println(Thread.currentThread().getName()+"\t come in");
  18. try {
  19. Thread.sleep(1);
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. myClass.changeNum();
  24. System.out.println(Thread.currentThread().getName()+"\t change num to "+myClass.getNum());
  25. },"aaa").start();
  26. while(myClass.getNum()==0){
  27. }
  28. System.out.println("主线程已经知道主内存中的共享变量发生了改变!");
  29. }
  30. }

分析:

  1. 当线程操纵资源类中的num没有volatile修饰时,由于main线程无法知晓主内存中num变量的变化,因此会卡在while循环处
  2. 当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. //1.线程操作资源类
  2. class MyClass{
  3. private volatile int num=0;
  4. public int getNum(){
  5. return this.num;
  6. }
  7. public void changeNum(){
  8. this.num=60;
  9. }
  10. public void addPlusPlus(){
  11. num++;
  12. }
  13. }
  14. //2.入口类
  15. public class JMM01 {
  16. public static void main(String[] args) {
  17. MyClass myClass=new MyClass();
  18. for(int i=0;i<20;i++){
  19. new Thread(()->{
  20. for(int j=0;j<10000;j++) {
  21. myClass.addPlusPlus();
  22. }
  23. },String.valueOf(i)).start();
  24. }
  25. //等待上面的20个计算线程结束后就只剩main线程和GC线程
  26. while(Thread.activeCount()>2){
  27. Thread.yield();
  28. }
  29. System.out.println(Thread.currentThread().getName()+"\t final num is: "+myClass.getNum());
  30. }
  31. }

**运行输出:
main final num is: 63638
每次的final num都不一样
但是都不会是理想中的20*10000
说明volatile并不能保证原子性

解决方案1:
addPlusPlus方法使用synchronized修饰
此时final num就会是20*10000
但是这样叫做杀鸡用牛刀

解决方案2:
变量num改用AtomicInteger变量

2.5 使用AtomicInteger变量保证原子性

  1. package jmm;
  2. import java.util.concurrent.atomic.AtomicInteger;
  3. //1.线程操作资源类
  4. class MyClass{
  5. private volatile int num=0;
  6. private AtomicInteger atomicInteger=new AtomicInteger();
  7. public int getNum(){
  8. return this.num;
  9. }
  10. public AtomicInteger getAtomicInteger(){
  11. return atomicInteger;
  12. }
  13. public void changeNum(){
  14. this.num=60;
  15. }
  16. public void addPlusPlus(){
  17. num++;
  18. }
  19. public void addAtomicInteger(){
  20. atomicInteger.getAndIncrement();
  21. }
  22. }
  23. public class JMM01 {
  24. public static void main(String[] args) {
  25. MyClass myClass=new MyClass();
  26. for(int i=0;i<20;i++){
  27. new Thread(()->{
  28. for(int j=0;j<10000;j++) {
  29. myClass.addPlusPlus();
  30. myClass.addAtomicInteger();
  31. }
  32. },String.valueOf(i)).start();
  33. }
  34. //等待上面的20个计算线程结束后就只剩main线程和GC线程
  35. while(Thread.activeCount()>2){
  36. Thread.yield();
  37. }
  38. System.out.println(Thread.currentThread().getName()+"\t final num is: "+myClass.getNum());
  39. System.out.println(Thread.currentThread().getName()+"\t final atomic is: "+myClass.getAtomicInteger());
  40. }
  41. }

输出结果:
main final num is: 114943
main final atomic is: 200000

3.禁止指令重排

单线程

单线程下指令重排之后程序执行的结果跟没有重排的结果是一致的
多线程下指令重排会带来混乱

指令重排遵守的规则:
保证数据依赖性

举例说明:

  1. int x=11
  2. int y=12;
  3. x=x+5;
  4. 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指令
用途有二:

  1. 保证特定操作的执行顺序
  2. 保证某些变量的内存可见性(volatile可见性的基石

image.png

网上的例子

  1. public class JMM02 {
  2. private int a=0;
  3. boolean flag=false;
  4. public void method1(){
  5. a=1;
  6. flag=true;
  7. }
  8. public void method2(){
  9. if(flag){
  10. a=a+5;
  11. if(a==5){
  12. System.out.println("I am 5");
  13. }
  14. }
  15. }
  16. public static void main(String[] args) {
  17. JMM02 jmm02=new JMM02();
  18. for(int i=0;i<1000000;i++){
  19. new Thread(()->{
  20. jmm02.method1();
  21. }).start();
  22. new Thread(()->{
  23. jmm02.method2();
  24. }).start();
  25. }
  26. }
  27. }

分析:
如果方法1先执行,
没有发生指令重排的话:那么a就等于6,
发生指令重排的话:那么a就等于5,
但是我怎么都得不到5。

DCL双重检查例子

双重检查是单例模式中的一种
image.png

**完整的DCL需要对instance进行volatile修饰*
**完整的DCL需要对instance进行volatile修饰*
**完整的DCL需要对instance进行volatile修饰*

synchronized不能禁止指令重排

image.png