硬件内存模型:

JMM内存模型+volatile+synchronized+lock

Java内存模型:

JMM内存模型+volatile+synchronized+lock

每个线程都有一个工作内存,线程只可以修改自己工作内存中的数据,然后再同步回主内存,主内存由多个内存共享。

下面 8 个操作都是原子的,不可再分的:

1)  lock:作用于主内存的变量,它把一个变量标识为一个线程独占的状态。

2)  unlock:作用于主内存的变量,他把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

3)  read:作用于主内存变量,他把一个变量的值从主内存传输到线程的工作内存,以便随后的 load 操作使用。

4)  load:作用于工作内存的变量,他把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。

5)  use:作用于工作内存的变量,他把工作内存中一个变量的值传递给执行引擎,每 当虚拟机遇到一个需要使用到变量的值得字节码指令时将会执行这个操作。

6)  assign:作用于工作内存的变量,他把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

7)  store:作用于工作内存的变量,他把工作内存中一个变量的值传送到主内存中,以便随后 write 使用。

8)  write:作用于主内存的变量,他把 store 操作从工作内存中得到的变量的值放入主内存的变量中。

 

JVM对Java内存模型的实现

在JVM内部,Java内存模型把内存分成了两部分:线程栈区和堆区,下图展示了Java内存模型在JVM中的逻辑视图: 
JMM内存模型+volatile+synchronized+lock

JVM中运行的每个线程都拥有自己的线程栈,线程栈包含了当前线程执行的方法调用相关信息,我们也把它称作调用栈。随着代码的不断执行,调用栈会不断变化。

线程栈还包含了当前方法的所有本地变量信息。一个线程只能读取自己的线程栈,也就是说,线程中的本地变量对其它线程是不可见的。

堆区包含了Java应用创建的所有对象信息,不管对象是哪个线程创建的,其中的对象包括原始类型的封装类(如Byte、Integer、Long等等)。不管对象是属于一个成员变量还是方法中的本地变量,它都会被存储在堆区。

下图展示了调用栈和本地变量都存储在栈区,对象都存储在堆区: 
JMM内存模型+volatile+synchronized+lock 
一个本地变量如果是原始类型,那么它会被完全存储到栈区。 
一个本地变量也有可能是一个对象的引用,这种情况下,这个本地引用会被存储到栈中,但是对象本身仍然存储在堆区

对于一个对象的成员方法,这些方法中包含本地变量,仍需要存储在栈区,即使它们所属的对象在堆区。 
对于一个对象的成员变量,不管它是原始类型还是包装类型,都会被存储到堆区

Static类型的变量以及类本身相关信息都会随着类本身存储在堆区

堆中的对象可以被多线程共享。如果一个线程获得一个对象的应用,它便可访问这个对象的成员变量。如果两个线程同时调用了同一个对象的同一个方法,那么这两个线程便可同时访问这个对象的成员变量,但是对于本地变量,每个线程都会拷贝一份到自己的线程栈中。

支撑Java内存模型的基础原理

指令重排序

在执行程序时,为了提高性能,编译器和处理器会对指令做重排序。但是,JMM确保在不同的编译器和不同的处理器平台之上,通过插入特定类型的Memory Barrier来禁止特定类型的编译器重排序和处理器重排序,为上层提供一致的内存可见性保证。

  1. 编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序:如果不存l在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序:处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

数据依赖性

如果两个操作访问同一个变量,其中一个为写操作,此时这两个操作之间存在数据依赖性。 编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序,即不会重排序。

as-if-serial

不管怎么重排序,单线程下的执行结果不能被改变,编译器、runtime和处理器都必须遵守as-if-serial语义。

内存屏障(Memory Barrier )

内存屏障是一组处理器指令,作用:

1.先于这个内存屏障的指令必须先执行,后于这个内存屏障的指令必须后执行。

2.使得内存可见性。

JMM内存模型+volatile+synchronized+lock

volatile是基于Memory Barrier实现的。

如果一个变量是volatile修饰的,JMM会在写入这个字段之后插进一个Write-Barrier指令,并在读这个字段之前插入一个Read-Barrier指令。这意味着,如果写入一个volatile变量,就可以保证:

  1. 一个线程写入变量a后,任何线程访问该变量都会拿到最新值。
  2. 在写入变量a之前的写入操作,其更新的数据对于其他线程也是可见的。因为Memory Barrier会刷出cache中的所有先前的写入。

volatile 禁止指令重排序有两层意思:1)当程序执行到 volatile 变量的读操作或者写操作时,在其前面操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没执行。2)在进行指令优化时,不能将在对 volatile 变量访问的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。

happens-before

在JMM中,如果一个操作的执行结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系,这个的两个操作既可以在同一个线程,也可以在不同的两个线程中。

与程序员密切相关的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。

注意:两个操作之间具有happens-before关系,并不意味前一个操作必须要在后一个操作之前执行!仅仅要求前一个操作的执行结果,对于后一个操作是可见的,且前一个操作按顺序排在后一个操作之前。 

 

synchronized关键字【具有锁重入特性,不具有继承性】:

1)在两个线程访问同一个对象中的同步方法时,一定是线程安全的。什么是两个线程访问同一个对象呢?例子如下:

1 public static void main(String[] args) {
2     HasSelfPrivateNum numRef = new HasSelfPrivateNum();
3     ThreadA athread = new ThreadA(numRef);
4     athread.start();
5     ThreadB bthread = new ThreadB(numRef);
6     bthread.start();
7 }
View Code

相关文章:

  • 2022-12-23
  • 2021-07-16
  • 2021-07-02
  • 2021-08-10
  • 2021-06-20
猜你喜欢
  • 2021-07-31
  • 2021-05-15
  • 2022-01-27
  • 2021-11-18
  • 2022-01-01
  • 2021-08-05
  • 2021-04-15
相关资源
相似解决方案