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

 

 

 


相关文章
|
26天前
|
存储 Java 编译器
Java内存模型(JMM)深度解析####
本文深入探讨了Java内存模型(JMM)的工作原理,旨在帮助开发者理解多线程环境下并发编程的挑战与解决方案。通过剖析JVM如何管理线程间的数据可见性、原子性和有序性问题,本文将揭示synchronized关键字背后的机制,并介绍volatile关键字和final关键字在保证变量同步与不可变性方面的作用。同时,文章还将讨论现代Java并发工具类如java.util.concurrent包中的核心组件,以及它们如何简化高效并发程序的设计。无论你是初学者还是有经验的开发者,本文都将为你提供宝贵的见解,助你在Java并发编程领域更进一步。 ####
|
2月前
|
缓存 easyexcel Java
Java EasyExcel 导出报内存溢出如何解决
大家好,我是V哥。使用EasyExcel进行大数据量导出时容易导致内存溢出,特别是在导出百万级别的数据时。以下是V哥整理的解决该问题的一些常见方法,包括分批写入、设置合适的JVM内存、减少数据对象的复杂性、关闭自动列宽设置、使用Stream导出以及选择合适的数据导出工具。此外,还介绍了使用Apache POI的SXSSFWorkbook实现百万级别数据量的导出案例,帮助大家更好地应对大数据导出的挑战。欢迎一起讨论!
173 1
|
6天前
|
Java
java内存区域
1)栈内存:保存所有的对象名称 2)堆内存:保存每个对象的具体属性 3)全局数据区:保存static类型的属性 4)全局代码区:保存所有的方法定义
17 1
|
21天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
40 6
|
25天前
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
37 2
|
26天前
|
存储 安全 Java
什么是 Java 的内存模型?
Java内存模型(Java Memory Model, JMM)是Java虚拟机(JVM)规范的一部分,它定义了一套规则,用于指导Java程序中变量的访问和内存交互方式。
61 1
|
1月前
|
存储 运维 Java
💻Java零基础:深入了解Java内存机制
【10月更文挑战第18天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
32 1
|
2月前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
2月前
|
存储 监控 算法
Java中的内存管理与垃圾回收机制解析
本文深入探讨了Java编程语言中的内存管理方式,特别是垃圾回收机制。我们将了解Java的自动内存管理是如何工作的,它如何帮助开发者避免常见的内存泄漏问题。通过分析不同垃圾回收算法(如标记-清除、复制和标记-整理)以及JVM如何选择合适的垃圾回收策略,本文旨在帮助Java开发者更好地理解和优化应用程序的性能。
|
2月前
|
存储 Java
Java内存模型
【10月更文挑战第11天】Java 内存模型(JMM)是 Java 虚拟机规范中定义的多线程内存访问机制,解决内存可见性、原子性和有序性问题。它定义了主内存和工作内存的概念,以及可见性、原子性和有序性的规则,确保多线程环境下的数据一致性和操作正确性。使用 `synchronized` 和 `volatile` 等同步机制可有效避免数据竞争和不一致问题。
31 3