Java内存模型(Java Memory Model,JMM)

为什么要有Java内存模型

  • 可见性,由缓存导致的可见性问题。
  • 有序性,由编译优化导致的有序性问题。
  • 原子性,由线程切换导致的原子性问题。

Java内存模型就是为了解决可见性和有序性问题。

什么是Java内存模型(JMM)

注意:JVM内存模型与Java内存模型是两个不一样的东西。

  • JVM内存模型:具体指的是JVM中运行时数据区的分区。
  • JMM是一种规范,是抽象的概念,目的是解决由于多线程并发编程通过内存共享进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题,即保证内存共享的正确性(可见性、有序性、原子性)。

JMM是一种抽象的概念,它是一种规范,定义了程序中各个变量访问的方式。JVM运行程序的实体是线程,每个线程创建时JVM会为其创建相应的工作内存(空栈间),用于储存线程私有数据,JMM中规定所有变量都存储在主内存上,所有线程都可访问,线程对于变量的操作(赋值、读取等)必须在工作内存进行,操作完成首在写回主内存,这样个线程之间就无法相互访问,线程间的通信(传值)必须通过主内存来完成。

  • 主内存(堆内存):主要存储实例对象,所有线程创建的实例对象(成员、局部、静态、常量等)都放在主内存中。存在线程安全问题(造成主内存与工作内存间数据存在一致性问题)。
  • 工作内存(私有线程域):主要存储当前方法的所有本地变量信息(主内存中变量的复制,也包含字节码行号指示器、相关Native方法信息)。线程中的本地变量对其他线程不可见,不存在线程安全问题。

缓存会导致可见性问题,编译优化会导致有序性问题。如果要避免这两个问题,最简单的方法就是禁用缓存和编译优化。但是这样就丢掉了优化程序性能的有利武器,显然是不可取的。

合理的方案应该是按需禁用缓存以及编译优化。什么叫按需禁用缓存以及编译优化呢?指的就是程序员在写代码的过程中,对有可能出现并发问题的代码禁用缓存和编译优化。

Java内存模型就是禁用缓存和编译优化的一种规范,它规范了 JVM 如何提供按需禁用缓存和编译优化的方法。现在提到的 Java 内存模型,一般指的是 JDK 5 开始使用内存模型,遵循的是 JSR-133 描述的规范。

Java内存模型主要分成两部分,一部分面向并发编程的开发人员,一部分面向JVM开发人员,我们需要关注的是前者。前者主要包括 volatile、synchronized 和 final 三个关键字,以及六项 Happens-Before 规则

主内存与工作内存的数据存储类型、操作方式及与硬件的关系

如果方法中的数据是基本数据类型,将直接存储在栈帧结构中;如果本地变量是引用类型,那么该引用会存储在工作内存的栈帧中,而对象实例还是会存在主内存(堆)中。对于实例对象的成员变量,无论类型都被存在堆中。当两个线程同时调用了一个对象的同一个方法时,两条线程都会将所涉及的数据复制一份到自己的工作内存中,操作完成后刷新到主内存中。JMM是一种抽象的概念,并不实际存在,在逻辑上分工作内存和主内存,但在物理上二者都可能在主存中也可能在Cache或者寄存器中。Java内存分区也是这个道理。

Java线程的实现原理

在Windows和Linux系统上,Java线程实现是基于一对一的线程模型,即通过语言级的程序(JVM)去间接地调用操作系统内核的线程模型。由于我们编写的多线程程序属于语言层面的,程序一般不会直接去调用内核线程,取而代之的是一种轻量级的进程(Light Weight Process),也就是通常意义上的线程,由于每个轻量级进程都会映射到一个内核线程,因此我们可以通过轻量级进程调用内核线程,进而由操作系统内核将任务映射到各个CPU各个核心进行并发执行,这种轻量级进程与内核线程间1对1的关系就称为一对一的线程模型。

关于其中的内核线程(Kernel-Level Thread,KLT),它是由操作系统内核(Kernel)支持的线程,这种线程是由操作系统内核来完成线程切换,内核通过操作调度器进而对线程执行调度,并将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这也就是操作系统可以同时处理多任务的原因。

并发编程的问题

多线程并发编程会涉及到以下的问题:

  • 原子性:指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。
  • 可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  • 有序性:程序执行的顺序按照代码的先后顺序执行,多线程中为了提高性能,编译器和处理器的常常会对指令做重排(编译器优化重排、指令并行重排、内存系统重排)。

JMM的具体实现

volatile、synchronized 和 final

  • 原子性:Java提供了两个高级字节码指令monitorenter和monitorexit,对应的是关键字synchronized,使用该关键字保证方法和代码块内的操作的原子性。
  • 可见性:Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。除了volatile,Java中的synchronized和final两个关键字也可以实现可见性,只不过实现方式不同。
  • 有序性:用volatile关键字禁止指令重排,用synchronized关键字加锁。

Happens-Before规则

  • 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
  • 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
  • volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。
  • happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
  • 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
  • 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
  • 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
  • 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。

在同一个线程中,书写在前面的操作happen-before后面的操作

好多文章把这理解成书写在前面先发生于书写在后面的代码,但是指令重排序,确实可以让书写在后面的代码先于书写在前面的代码发生。这是把happen-before 理解成“先于什么发生”,其实happen-beofre在这里没有任何时间上的含义。比如下面的代码:

int a = 3;      //1
int b = a + 1; //2

上面 //2 对b赋值的操作会用到变量a,那么java的“单线程happen-before原则”就保证 //2的中的a的值一定是3,而不是0,5等其他乱七八糟的值,因为//1 书写在//2前面, //1对变量a的赋值操作对//2一定可见。因为//2 中有用到//1中的变量a,再加上java内存模型提供了“单线程happen-before原则”,所以java虚拟机不许可操作系统对//1 //2 操作进行指令重排序,即不可能有//2 在//1之前发生。但是对于下面的代码:

int a = 3;
int b = 4;

两个语句直接没有依赖关系,所以指令重排序可能发生,即对b的赋值可能先于对a的赋值。

同一个锁的unlock操作happen-beofre此锁的lock操作

public class A {
   public int var;

   private static A a = new A();

   private A(){}

   public static A getInstance(){
       return a;
   }

   public synchronized void method1(){
       var = 3;
   }

   public synchronized void method2(){
       int b = var;
   }

   public void method3(){
       synchronized(new A()){ //注意这里和method1 method2 用的可不是同一个锁哦
           var = 4;
       }
   }
}
//线程1执行的代码:
A.getInstance().method1(); 
//线程2执行的代码:
A.getInstance().method2(); 
//线程3执行的代码:
A.getInstance().method3();

如果某个时刻执行完“线程1” 马上执行“线程2”,因为“线程1”执行A类的method1方法后肯定要释放锁,“线程2”在执行A类的method2方法前要先拿到锁,符合“锁的happen-before原则”,那么在“线程2”method2方法中的变量var一定是3,所以变量b的值也一定是3。但是如果是“线程1”、“线程3”、“线程2”这个顺序,那么最后“线程2”method2方法中的b值是3,还是4呢?其结果是可能是3,也可能是4。的确“线程3”在执行完method3方法后的确要unlock,然后“线程2”有个lock,但是这两个线程用的不是同一个锁,所以JMM这个两个操作之间不符合八大happen-before中的任何一条,所以JMM不能保证“线程3”对var变量的修改对“线程2”一定可见,虽然“线程3”先于“线程2”发生。

对一个volatile变量的写操作happen-before对此变量的任意操作

volatile int a;
a = 1; //1
b = a;  //2

如果线程1 执行//1,“线程2”执行了//2,并且“线程1”执行后,“线程2”再执行,那么符合“volatile的happen-before原则”所以“线程2”中的a值一定是1。

如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作

volatile int var;
int b;
int c;
b = 4; //1
var = 3; //2
c = var; //3
c = b; //4

假设“线程1”执行//1 //2这段代码,“线程2”执行//3 //4这段代码。如果某次的执行顺序如下:
//1 //2 //3 //4。那么有如下推导( hd(a,b)表示a happen-before b):

因为有hd(//1,//2) 、hd(//3,//4) (单线程的happen-before原则)
且hd(//2,//3) (volatile的happen-before原则)
所以有 hd(//1,//3),可导出hd(//1,//4) (happen-before原则的传递性)
所以变量c的值最后为4
如果某次的执行顺序如下:
//1 //3 //2// //4 那么最后4的结果就不能确定喽。其原因是 //3 //2 直接符合上述八大原则中的任何一个,不能通过传递性推测出来什么。

 

参考文章:

 

posted @ 2022-02-14 18:41  残城碎梦  阅读(267)  评论(0编辑  收藏  举报