Java内存模型和底层实现原理
Java内存模型把Java虚拟机内部划分为线程栈和堆。这张图演示了Java内存模型的逻辑视图。
每一个运行在Java虚拟机里的线程都拥有自己的线程栈。这个线程栈包含了这个线程调用的方法当前执行点相关的信息。一个线程仅能访问自己的线程栈。
每当我们执行一个方法时,我们都会将该方法打包成一个栈帧,然后将其将栈帧放入我们的线程栈之内,这就是为什么我们在 并发安全(一)—— 如何保证线程安全 中提到说,栈封闭可以保证我们的线程安全。因为每一个线程仅能访问自己的线程栈。
另外我们执行方法都会像上述那种所说,打个栈帧,然后放入栈内存中,执行完后就会从栈内存中取出,这个压栈弹栈的过程,我们在 Spring的AOP底层源码后续——AOP的调用流程及总结图解 中就有深刻的体会。
所有原始类型(boolean、byte、short、char、int、long、float、double)的局部变量都直接保存在线程栈当中,对于它们的值各个线程之间都是独立的。对于原始类型的局部变量,一个线程可以传递一个副本给另一个线程,也就是值传递,当它们之间是无法共享的。
一个局部变量如果是原始类型,那么它会被完全存储到栈区。 一个局部变量也有可能是一个对象的引用,这种情况下,这个本地引用会被存储到栈中,但是对象本身仍然存储在堆区。
所以:存在栈内存中,肯定是独占的,存在堆内存中的对象,因为只有是局部变量,对象的引用只有该方法知道,所以在栈封闭的状态下,即不存在成员变量,我们是无需考虑其线程安全的问题的。
堆区包含了Java应用创建的所有对象信息,不管对象是哪个线程创建的,其中的对象包括原始类型的封装类(如Byte、Integer、Long等等)。不管对象是属于一个成员变量还是方法中的局部变量,它都会被存储在堆区。
一个对象的成员变量,不管它是原始类型还是包装类型,都会被存储到堆区。static 类型的变量以及类本身相关信息都会随着类本身存储在堆区。
成员对象全部存放在堆内存中,并且它们的引用都是可以被其他线程所得到的,所以我们在多线程使用下,就必须考虑其线程安全的问题。
synchronized
既然有线程安全问题,我们就必须要解决,我们之前学过那么多,都知道最简单的方法,就是加锁,如 synchronized 关键字,synchronized 关键字我们都非常熟悉了,这里我们就提一点 synchronized 关键字的实现原理。
synchronized 关键字主要使用 monitorenter 和 monitorexit 指令实现的。
- monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处。
- 每个 monitorenter 必须有对应的 monitorexit 与之配对 。
- 任何对象都有一个 monitor 与之关联,当且一个 monitor 被持有后,它将处于锁定状态 。
我们在使用 synchronized 关键字时,都会存放在这个对象的对象头之中。
volatile
我们在 volatile关键字 —— 可见性及禁止指令重排序 中提到了 volatile 关键字可以帮助我们保证其可见性,以及禁止其指令重排序。
有 volatile 变量修饰的共享变量进行写操作的时候会使用CPU提供的 Lock 前缀指令
- 将当前处理器缓存行的数据写回到系统内存
- 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
可见性原理主要如上,那么是如何禁止指令重排序的呢?这里就需要使用到我们的内存屏障了,那么什么是内存屏障呢?
那么 volatile 是如何实现的呢?
在每个volatile写操作的前面插入一个StoreStore屏障。在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的后面插入一个LoadLoad屏障。在每个volatile读操作的后面插入一个LoadStore屏障。
final
我们在并发安全中提到过,在条件允许的情况下,我们应该将变量加上 final 关键字进行修饰,其实我们的 final 中也是使用到了我们刚刚提到的内存屏障。
- 要求编译器在final域的写之后,构造函数 return 之前插入一个 StoreStore 障屏。
- 读 final 域的重排序规则要求编译器在读 final 域的操作前面插入一个 LoadLoad 屏障。
因为我们在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。我们必须要先将 final 初始化完成后再进行使用。
另外初次读一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操作之间不能重排序。