Java内存模型

简介: 本博客为《深入理解java虚拟机》的学习笔记,所以大部分内容来自此书,另外一部分内容来自网络其他博客和源码分析。 主要内容探讨以下问题: Ø  Java内存模型、协议、规则。 Ø  volatile的可见性和禁止指令重排序是什么意思? Ø  Synchronized是如何做到线程安全的? Ø  先行发生原则。

本博客为《深入理解java虚拟机》的学习笔记,所以大部分内容来自此书,另外一部分内容来自网络其他博客和源码分析。

主要内容探讨以下问题:

Ø  Java内存模型、协议、规则。

Ø  volatile的可见性和禁止指令重排序是什么意思?

Ø  Synchronized是如何做到线程安全的?

Ø  先行发生原则。

 

一  Java内存模型

1       模型

Java内存逻辑模型如下:

9be4c5b2263d64a0d71ccf4fa0a6e21752ac0f79


所有变量都存储在主内存中。

每个线程都有自己的工作内存,工作内存中保存了线程使用到的主内存中变量的副本。

线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接操作主内存。

不同线程之间无法访问对方的工作内存。

线程之间的值传递均需通过主内存来完成。

 

2       协议

操作

解释

作用域

说明

lock

锁定

主内存

把一个变量表示为一个线程独占的状态

unlock

解锁

主内存

把一个变量从线程独占的状态释放出来,释放后的变量才能被其他线程锁定

read

读取

主内存

把一个变量从主内存传输到工作内存中

load

载入

工作内存

把read操作的变量值放入工作内存的变量副本中。

use

使用

工作内存

把一个工作内存的变量传递给执行引擎,当虚拟机遇到一个需要使用变量值的字节码指令时会执行此操作

assign

赋值

工作内存

把从执行引擎收到的值赋给工作内存的变量,当虚拟机遇到一个给变量赋值的字节码指令时会执行此操作。

store

存储

工作内存

把一个变量的值传到主内存中

write

写入

主内存

把store操作的值放到主内存的变量中

如果需要将一个变量从主内存复制到工作内存,就需要顺序的执行read、load;如果需要讲一个变量从工作内存写回到主内存,就需要顺序的执行store、write。Java内存模型要求了这两对命令的顺序,但不要求其连续,即在read和load之间、store和write允许插入其他指令。

 

3       规则

不允许read和load、store和write单独出现。即不允许一个变量从主内存读取了但工作内存不接受的情况;不允许从工作内存回写了但主内存不接受的情况。

不允许一个线程丢弃掉它最近的assign操作。即变量在工作内存中修改之后,必须同步回主内存中。

不允许一个线程无原因的把数据从线程的工作内存同步回主内存。即对变量没有执行assgin操作则不能回写到主内存。

一个新的变量只能在主内存中创建,不允许在工作内存中直接使用一个违背初始化过的变量。即对一个变量use前必须load;对一个变量store前必须assign。

一个变量在同一时刻只允许一个线程对其进行lock操作,但一个线程可以多次执行lock操作,多次执行lock操作以后,只有执行相同次数的unlock,变量才被解锁。

如果一个线程没有被lock操作锁定,那么不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁住的变量。

对一个变量执行unlock操作前,必须先把此变量同步回主内存,即先执行store、write操作。

 

从上面的规则我们可以看到:因为一个变量同一时刻只有一个线程能对其进行lock操作,在unlock前必须将变量同步会主内存,所以使用lock可以保证并发情况下数据安全。

 

4       long和double的非原子协定

虚拟机允许没有被volatile修饰的64位数据的多些操作划分成2次32位操作进行,即允许虚拟机实现对64位的long、double的read、load、store、write不保证原子性,即long和double的非原子协定。

目前商用虚拟机基本上都对long、double保证原子操作。

 

 

二  volatile

1       volatile变量的特性

使用volatile修饰的变量具有两种特性:可见性、禁止指令重排序优化。

1)       可见性

可见性指一个线程修改了这个变量值,新值对其他线程来说是可以立即得知的;普通变量做不到这一点。注意这一点并不意味着使用volatile修饰的变量是线程安全。

 

2)       禁止指令重排序优化

普通的变量仅仅保证在执行过程中,所有依赖赋值结果的地方都能获取正确的结果,而不保证变量赋值操作的顺序与代码中的执行顺序一致,这就是java内存模型中的“线程内表现为串行的语义”;而使用volatile可以实现此点。

单例模式下,如果不使用volatile修饰,通过双重检查锁创建对象,并发场景中可能出现问题,具体见后面的分析。

 

2       volatile变量的特殊规则

说明:因为觉得原文中对于volatile规则的描述不好理解,所以我在这里换了一种描述方式,所以如果发现这里的描述和虚拟机规范不同,请不必疑惑。

假设T表示一个线程,V、W表示两个volatile类型的变量,那么拥有以下规则:

Ø  每次使用volatile修饰的变量前,必须先从主内存中获取最新的值

线程T对变量V的use动作和线程T对变量V的read、load的动作可以认为是相关联的,必须连续一起出现。即线程T对V的前一个动作是load时,线程T才能对变量V执行use操作;如果线程T对V的后一个动作是use时,线程T才能对变量V执行load操作。

此规则要求在工作内存中,每次使用V前必须先从主内存中刷新最新的值,用于保证能看到其他线程对变量V修改后的值。

 

Ø  每次使用volatile修饰的变量后,必须立即同步回主内存

线程T对变量V的assign动作和线程T对变量V的store、write的动作可以认为是相关联的,必须连续一起出现。即线程T对V的前一个动作是assign时,线程T才能对变量V执行store操作;如果线程T对V的后一个动作是store时,线程T才能对变量V执行assign操作。

此规则要求在工作内存中,每次使用V后必须立即同步回主内存,用于保证其他线程能看到当前线程对变量V的值所做的修改。

 

Ø  代码执行顺序和程序的顺序相同

假定动作UV是线程T对变量V执行的use动作,动作RV是与之相关联的read动作;假定动作UW是线程T对变量W的use动作,动作RW是与之相关联的read动作;如果UV先于UW,那么RV先于RW。

假定动作AV是线程T对变量V执行的assign动作,动作WV是与之相关联的write动作;假定动作AW是线程T对变量W的assign动作,动作WW是与之相关联的write动作;如果AV先于AW,那么WV先于WW。

此规则要求volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同

 

3       示例

a)       volalite修饰的变量不是线程安全的

以下示例代码输出的结果不会为100000;基本上都会比此值略小。

public class VolaliteTester {
 
    private static volatile int value;
 
    private static void inc() {
        value++;
    }
 
    public staticvoid main(String[] args) {
        int threadCount= 10;
        final int times = 10000;
 
        Thread[] threads= new Thread[threadCount];
        for (int i = 0; i < threadCount;i++) {
           Threadthread = new Thread(newRunnable() {
               @Override
               public void run() {
                   for (int j = 0; j < times;j++) {
                       inc();
                   }
               }
           });
           threads[i] = thread;
           thread.start();
        }
 
        if (Thread.activeCount() >1) {
           Thread.yield();
        }
        System.err.println("value=" + value);
    }
}

b)       双重检查锁失效

单例模式下创建实例对象时,可能出现双重检查锁失效的情况,即以下示例代码可能会创建多个实例instance对象。

public class Singleton {
    private static volatile Singleton instance;
 
   private Singleton() {
       super();
   }
 
   public staticSingleton getInstance() {
       if (instance == null){
           synchronized(Singleton.class) {
                // 如果在等待synchronized结束前已经有线程创建Instance则直接忽略。
               if(instance == null){
                   instance = newSingleton();
               }
           }
       }
       return instance;
   }
}


 

在执行instance = new Singleton()语句时,实际上分为分配内存、调用构造函数、instance指向分配的内存地址三个步骤。如下伪代码所示:

memery =allocate();    //为对象分配内存
ctorSingleton(memery);  //调用构造函数实例化对象
instance = memery;     //将instance指向新分配的内存

但是实际上有些虚拟机进行指令重排序以后会变成如下顺序(虚拟机的内存模型以及协议规则均没有限制不能进行这种操作)。

memery =allocate();    //为对象分配内存
instance = memery;      //将instance指向新分配的内存,注意此时instance为not null,但是此时对象并未实例化,如果此时执行非空判断,将返回true。
ctorSingleton(memery);  //调用构造函数实例化对象


三  原子性、可见性、有序性

1       原子性(Atomicity)

java内存模型直接对变量的read、load、use、assign、store、write操作的原子性(long、double的非原子协定基本是例外,但基本不会遇到)

通过synchronized关键字实现lock、unlock操作,保证同一时间段内只有一个线程访问同步快,所以可以实现代码块的原子性。

 

2       可见性(Visibility)

java内存模型是通过变量使用前从主内存读取、变量修改后将值同步回主内存来实现可见性的。

volalite的可见性是由:修改后的新值立即同步到主内存,使用前立即从主内存中读取新值这个规则决定的。volatite保证了多线程操作时变量的可见性,而普通变量却不行。

Synchronized的可见性是由:在unlock前必须将变量先同步到主内存这个规则决定的。

final的可见性是由:在构造函数中初始化后,不会将this的引用传递出去,以后将无法修改此值这个规则决定的。

 

3       有序性(Ordering)

如果在本线程内观察,所有操作都是有序的;如果在一个线程观察另外一个线程,所有操作都是无序的。前半句指:线程内变形为串行的语义;后半句指:指令重排序闲现象和工作内存与主内存同步延迟现象。

Volatile本身就有禁止指令重排序的语义,所以可以保证有序性。

Synchronized的有序性是由:同一时刻只允许一个线程对其进行lock操作这个规则决定的,这决定了synchronized的语句块只能串行进入,所以可以保证有序性。

 

四  先行发生原则

以下是java内存模型提供的“天然”的先行发生关系,这些先行发生关系不需任何同步协助就已经存在。如果两个操作之间的关系不在此列,并且无法通过这些规则推导出来,那么他们就没有顺序保证,虚拟机可能对他们随意的进行重排序。

1.      程序次序规则(Program Order Rule)

在一个线程中,按照代码顺序,书写在前的操作先行发生于书写在后的操作。确切的说,应该是控制流顺序而不是书写顺序,例如分支、循环机构。

 

2.      管程锁定规则(Monitor Lock Rule)

一个unlock操作先行发生于后面对同一个锁的lock操作。后面值的是时间上的先后顺序。

 

3.      volatile变量规则(Volatile Rule)

对一个volatile变量的写操作先行发生于后面对这个变量的读操作。。后面值的是时间上的先后顺序。

 

4.      线程启动规则(Thread Start Rule)

Thread对象的start方法先行发生于对此线程的每一个动作。

 

5.      线程终止规则(Thread Termination Rule)

线程中的所有操作都先行发生于对此项承德终止检测.可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到县城已经终止执行。

 

6.      线程中断规则(Thread Interruption Rule)

对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。可以通过Thread.interrupted()方法检测到是否发生中断。

 

7.      对象终结规则(Finalizer Rule)

一个对象的初始化完成(构造函数执行结束)先行发生于他的finalize()方法的开始。

 

8.      传递性(Transitivity)

如果操作A先行发生于操作,操作B先行发生于操作C,那么可以得出操作A先行发生于操作C。

 

 

 

五  参考博客

对象创建过程见博客

 

双重检查锁更多分析见博客

http://blog.csdn.net/zhangzeyuaaa/article/details/42673245

 

 

 


相关文章
|
6天前
|
存储 Java 编译器
Java内存区域详解
Java内存区域详解
18 0
Java内存区域详解
|
16天前
|
缓存 算法 Java
Java内存管理与调优:释放应用潜能的关键
【4月更文挑战第2天】Java内存管理关乎性能与稳定性。理解JVM内存结构,如堆和栈,是优化基础。内存泄漏是常见问题,需谨慎管理对象生命周期,并使用工具如VisualVM检测。有效字符串处理、选择合适数据结构和算法能提升效率。垃圾回收自动回收内存,但策略调整影响性能,如选择不同类型的垃圾回收器。其他优化包括调整堆大小、使用对象池和缓存。掌握这些技巧,开发者能优化应用,提升系统性能。
|
1月前
|
监控 Java 数据库连接
解析与预防:Java中的内存泄漏问题
解析与预防:Java中的内存泄漏问题
|
2月前
|
存储 缓存 算法
深入剖析Java中JVM的内存模型!!!
对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像C/C++程序开发程序员这样为内一个 new 操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序员把内存控制权利交给 Java 虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。
46 1
|
12天前
|
缓存 安全 Java
Java并发编程进阶:深入理解Java内存模型
【4月更文挑战第6天】Java内存模型(JMM)是多线程编程的关键,定义了线程间共享变量读写的规则,确保数据一致性和可见性。主要包括原子性、可见性和有序性三大特性。Happens-Before原则规定操作顺序,内存屏障和锁则保障这些原则的实施。理解JMM和相关机制对于编写线程安全、高性能的Java并发程序至关重要。
|
20天前
|
缓存 Java C#
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍(一)
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍
57 0
|
2月前
|
存储 安全 Java
一文带你读懂深入理解Java内存模型
java内存模型(Java Memory Model,JMM)是java虚拟机规范定义的,用来屏蔽掉java程序在各种不同的硬件和操作系统对内存的访问的差异,这样就可以实现java程序在各种不同的平台上都能达到内存访问的一致性。可以避免像c++等直接使用物理硬件和操作系统的内存模型在不同操作系统和硬件平台下表现不同,比如有些c/c++程序可能在windows平台运行正常,而在linux平台却运行有问题。 物理硬件和内存
20 1
|
2天前
|
存储 缓存 监控
Java内存管理:垃圾回收与内存泄漏
【4月更文挑战第16天】本文探讨了Java的内存管理机制,重点在于垃圾回收和内存泄漏。垃圾回收通过标记-清除过程回收无用对象,Java提供了多种GC类型,如Serial、Parallel、CMS和G1。内存泄漏导致内存无法释放,常见原因包括静态集合、监听器、内部类、未关闭资源和缓存。内存泄漏影响性能,可能导致应用崩溃。避免内存泄漏的策略包括代码审查、使用分析工具、合理设计和及时释放资源。理解这些原理对开发高性能Java应用至关重要。
|
16天前
|
Java
java中jar启动设置内存大小java -jar 设置堆栈内存大小
java中jar启动设置内存大小java -jar 设置堆栈内存大小
11 1
|
20天前
|
监控 网络协议 NoSQL
java线上排查OOM内存溢出
java线上排查OOM内存溢出
19 0