JAVA内存模型(JMM)

9 篇文章 0 订阅

java内存模型:

		java虚拟机规范中定义的java内存模型(JMM)是为了 屏蔽各种操作系统 ,硬件等内存的访问差异,以确保Java程序在所有操作系统和平台上能够实现一次编写、到处运行的效果。
		理解java内存模型是学习java并发的基础,JMM定义了java中多线程对共享变量操作的定义
主内存
	  java虚拟机规范规定所有的共享变量存储在主内存中
工作内存
每个线程有自己的独立内存空间--工作内存;

每个线程不能直接操作主内存中的变量,只能从主内存copy到自己的工作内存空间进行操作,然后写入到主内存空间; 各个线程是不可见的数据不共享的, 线程之间的通信需要通过主内存进行交互;所以在多线程进行操作的时候就会出现数据一致性问题

工作内存和主内存交互协议

Java内存模型
lock(锁定):作用于主内存变量,锁定变量为线程独占状态
unlock(解锁): 作用于主内存变量,解锁线程锁定的变量,供其他线程使用
read(读取):作用于主内存变量,把一个变量的值从主内存中传输到线程工作内存中,以便随后的load操作
load(载入): 作用于工作内存变量,把从主内存中读取到的值保存到工作内存的变量副本中
use(使用): 作用于工作内存变量,把工作内存中的变量值传递给执行引擎,每当虚拟机需要使用这个变量的时候,就会执行该操作
assign(赋值):作用于工作内存中的变量,把从执行引擎中获取到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的指令的时候会执行该操作
store(存储):作用于工作内存变量,把工作内存中变量的值传递给主内存,以便后续的write操作
write(写入):作用于主内存变量,把store操作过来的值写入到主内存变量中

这8个步骤必须符合的规则
  1. 不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。
  2. 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存
  3. 一个新的变量只允许在主内存中诞生,不允许工作内存直接使用未初始化的变量
  4. 不允许一个线程回写没有修改的变量到主内存,也就是如果线程工作内存中变量没有发生过任何assign操作,是不允许将该变量的值回写到主内存
  5. 一个变量在同一时刻只能被一个线程进行lock操作,加锁后其他线程不能对该变量进行lock操作,只能等待解锁unlock后其他线程才能操作;但是同一线程对该变量能进行多次lock操作,不过也要相应执行多次的unlock
  6. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
  7. 不允许对没有lock的变量执行unlock操作,如果一个变量没有被lock操作,那也不能对其执行unlock操作,当然一个线程也不能对被其他线程lock的变量执行unlock操作
  8. 对一个变量进行unlock操作的时候,必须把变量值同步回主内存
原子性,可见性 和有序性
  • 原子性:操作是不可中断的,要么成功要么失败;java内存模型中定义了8种操作都是原子操作,不可再分的;
    由Java内存模型来直接保证的原子性变量 操作包括read、load、assign、use、store和write,我们大致可以认为基本数据类型的访问读写是具备原子性的。如果应用场景需要一个更大方位的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式的使用这两个操作,这两个字节码指令反应到Java代码中就是同步块–synchronized关键字,因此在synchronized块之间的操作也具备原子性。
    所以可以这么说JMM中定义的 read,load,assign,use,stoe,write都是原子性操作,lock和unlock是为了在多线程情况下保证原子性
  • 可见性:可见性是指当一个线程修改了变量的值,其他线程能够立即感知到;变量值修改后刷新会主内存,使用变量的值时从主内存刷新回工作内存;volatile修饰的变量在操作的时候是具备可见性的,除了volatile之外,Java还有两个关键字能实现可见性,即synchronized和final;而final关键字的可见性是指:被final修饰的字段在构造器中一旦初始化完成,其他字段就能看见

final字段的值,也就是final域能确保初始化过程的安全性。

  • 有序性:单线程中所有的操作都是有序的;如果在多线程环境下,一个线程观察其他线程的操作都是无序的;前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象; 也可以这么说 单线程中不存在有序和无序,只有在多线程环境下多个线程操作顺序是乱序的

这里引入一个指令重排序的概念; 编译器在不改变程序的语义的情况下对代码指令进行重排序;
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。例如:

	int a = 10;
	int b = 20;

这样的操作 可能会先加载 int b = 20; 两个变量的加载顺序是不关联的

	int a = 10;
	int b = a;	

这样的操作是和顺序由关联的 必须先加载a, 在加载 b

再看下面代码

// 定义两个变量
int a = 0;
boolean flag = false;
public void opration1() {
	a = 2;                     // 1
	flag = true;               // 2
}
public void opration2() {
	if (flag) {                // 3
		System.out.print(a * a);//4
	}
}

现在假设有两个线程,一个线程调用了opration1 方法, 一个线程调用了opration2 方法, 由于指令重排序 线程1 可能是这么执行的

flag = true;  // 先执行了flag = true 并写入了主内存
a = 2;        

然后这个时候 线程而开始执行了,线程2 获取到的flag = true, a = 0;(线程1 的a赋值操作还没写入到主内存) 所以会输出 0 ;

再看一个经典的单例问题, 双重检测机制

//常见的 线程安全的单例模式
public class TestA {
   
   private TestA(){}
   
   public static TestA ta = null;
   
   public static TestA getInstance() {
       if (ta == null) {
           synchronized (TestA.class) {
               if (ta == null) {
                   ta = new TestA();
               }
           }
       }
       return ta;
   }
}

但是他一定安全吗/
ta = new TestA();
这里会做三步操作;

  1. 开辟内存空间,分配内存空间
  2. 初始化对象信息
  3. 句柄引用指向内存空间
    在指令重排序的作用下 原本 1-2-3 的操作 可能会变成1-3-2 也就是分配空间后并没有初始化就进行了 对象的引用指向操作,
    所以这个 单例模式 在极小的概率下并不是安全的
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值