JMM基础

参考书:《Java并发编程艺术》
附图均来自:《Java并发编程艺术》

先了解一下基础的概念:什么是JMM?

JMM(全称:Java Memory Model), JMM定义的是线程和主内存间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本,JMM控制一个线程的共享变量写入何时对另一个线程可见。本地内存是JMM的一个抽象概念,并不真实存在。(有个印象,下面具体介绍)

在学习JMM之前,其实要先保留两个问题。1.那些数据需要JMM控制?2.JMM是怎么进行干预线程的操作的?

源代码到可执行的变成指令序列(重排序)

首先,我们要知道的是,我们所写的代码,并不是原封不动的被转换成可执行的序列。为了提高性能,编译器和处理器经常会进行 重排序

编译重排序的过程

JMM的编译器会对特定类型的编译器进行限制。 所以,首先要了解编译器的重排序处理。

不同处理器的编译器有所不同,但是都遵循以下规则:

1.数据的依赖性

含义:当两个操作访问一个变量,且两个操作中一个是写操作,这两个操作存在数据的依赖性。

这两个操作,可能是:1.先读后写;2.先写后写;3.先写后读

一般存在数据依赖性的数据操作,编译器和处理器不会重新排序两个操作的执行顺序(只针对单处理器);

2.as_if_serial语义

含义:不管怎么重排序,单线程的执行结果不能被改变。因此不会对存在数据以来的操作进行重排序;

上面的这些,我们可以看出,在单线程的情况下,根据数据的依赖性,数据执行结果不能被发生改变。

但是我们实际开发中经常会遇到多线程的情况,如下:

class Test{
   int a = 0 ;
   boolean flag = false;

   public void wirte(){
     	a ++ ;
	flag = true;	
   }
   
   public int read(){
	if(flag){
            return ++a;		
	}
   }	
}

上面的这个类,例如有两个线程同时执行write方法和read方法,因为就单个方法来所编译器的出现重排符合数据依赖性,但是实际多线程访问会出现数据异常查询和写入的问题。

像上面的这种多线程操作同一个变量,且读和写操作不在同一个线程中时,就要通过JMM进行编译器约束了。

了解下JMM的原则

在开始了解JMM编译重排序之前,我们先了解一个模型:顺序一致性模型;

顺序一致性模型

顺序一致性内存模型是一个理论参考模型,处理器内存模型和编程内存模型以顺序一致性为参考。

顺序一致性特征:

1.一个线程的所有操作,必须按照程序的顺序来执行;

2.所有的线程都只能看到单一的操作执行顺序。在顺序一致性模型中,每个操作必须原子执行且立刻对所有线程可见。

happens-before(JMM顺序执行的原则)

happens-before是JMM最核心的概念,也是JMM顺序执行的要遵循的原则。

定义:

1.一个操作happens-before第二个操作,那么第一个操作执行顺序在第二个操作之前,并且第一个执行结果对第二个操作可见。

2.只描述两个操作的关系,java平台不一定按照这种关系执行。

happens-before的几种规则

1、程序顺序规则:一个线程中的每个操作,happens-before于该线程中任意的后续操作。

2、监视器锁规则:对一个锁的解锁操作,happens-before于随后对这个锁的加锁操作。

3、volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。

4、传递性规则:如果 A happens-before B,且 B happens-before C,那么A happens-before C。

JMM控制

现在可以回答上面预留的两个问题的问题了。

1.那些数据需要JMM控制?

  • 方法区的实例,域都是线程共享的;这些是不会涉及到重排序的问题,所以不是JMM管理的范围

  • 局部变量的线程是私有的,才涉及到线程之间的通信;

2.JMM是怎么进行干预线程的操作的?

这就是我们接下来要了解的知识,内存屏障(barriers)和临界区

然鹅JMM并不是禁止所有重排序,对于不会改变程序运行结果的重排序,JMM不做要求;

内存屏障(阻止编译器重排序)

内存屏障的类型

  • volatile变量就是通过屏障阻止编译重排序的;

  • final修饰的变量也是通过屏障进行编译重排序的;

volatile变量

1.volatile重排序的规则表

2.举个栗子(来自《Java并发编程艺术》)

class VolatileBarrierExample {
       int a;
       volatile int v1 = 1;
       volatile int v2 = 2;
       void readAndWrite() {
           int i = v1;
}

临界区(临界区内可以重排序,但是临界区内的代码不能“逸出”到临界区外)

显式的通过给代码增加锁,创建临界区;当线程释放锁的时候,JMM会把本地内存中的数据刷新到主内存中。

指令执行中,是通过#Lock前缀指令,指令在执行期间会锁住。

先看一个代码

class Test{
   int a = 0 ;
   boolean flag = false;

   public synchronized void wirte(){
     	a ++ ;
	flag = true;	
   }
   
   public synchronized int read(){
	if(flag){
            return ++a;		
	}
   }	
}

posted @ 2020-09-18 23:55  PerfectLi  阅读(228)  评论(0编辑  收藏  举报