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

 

 

 


相关文章
|
11月前
|
安全 Java 应用服务中间件
Spring Boot + Java 21:内存减少 60%,启动速度提高 30% — 零代码
通过调整三个JVM和Spring Boot配置开关,无需重写代码即可显著优化Java应用性能:内存减少60%,启动速度提升30%。适用于所有在JVM上运行API的生产团队,低成本实现高效能。
1184 3
|
12月前
|
存储 缓存 Java
Java数组全解析:一维、多维与内存模型
本文深入解析Java数组的内存布局与操作技巧,涵盖一维及多维数组的声明、初始化、内存模型,以及数组常见陷阱和性能优化。通过图文结合的方式帮助开发者彻底理解数组本质,并提供Arrays工具类的实用方法与面试高频问题解析,助你掌握数组核心知识,避免常见错误。
|
10月前
|
Java 大数据 Go
从混沌到秩序:Java共享内存模型如何通过显式约束驯服并发?
并发编程旨在混乱中建立秩序。本文对比Java共享内存模型与Golang消息传递模型,剖析显式同步与隐式因果的哲学差异,揭示happens-before等机制如何保障内存可见性与数据一致性,展现两大范式的深层分野。(238字)
300 4
|
存储 缓存 Java
【高薪程序员必看】万字长文拆解Java并发编程!(5):深入理解JMM:Java内存模型的三大特性与volatile底层原理
JMM,Java Memory Model,Java内存模型,定义了主内存,工作内存,确保Java在不同平台上的正确运行主内存Main Memory:所有线程共享的内存区域,所有的变量都存储在主存中工作内存Working Memory:每个线程拥有自己的工作内存,用于保存变量的副本.线程执行过程中先将主内存中的变量读到工作内存中,对变量进行操作之后再将变量写入主内存,jvm概念说明主内存所有线程共享的内存区域,存储原始变量(堆内存中的对象实例和静态变量)工作内存。
412 0
|
10月前
|
存储 缓存 Java
【深入浅出】揭秘Java内存模型(JMM):并发编程的基石
本文深入解析Java内存模型(JMM),揭示synchronized与volatile的底层原理,剖析主内存与工作内存、可见性、有序性等核心概念,助你理解并发编程三大难题及Happens-Before、内存屏障等解决方案,掌握多线程编程基石。
|
11月前
|
缓存 监控 Kubernetes
Java虚拟机内存溢出(Java Heap Space)问题处理方案
综上所述, 解决Java Heap Space溢出需从多角度综合施策; 包括但不限于配置调整、代码审查与优化以及系统设计层面改进; 同样也不能忽视运行期监控与预警设置之重要性; 及早发现潜在风险点并采取相应补救手段至关重要.
1087 17
|
监控 Kubernetes Java
最新技术栈驱动的 Java 绿色计算与性能优化实操指南涵盖内存优化与能效提升实战技巧
本文介绍了基于Java 24+技术栈的绿色计算与性能优化实操指南。主要内容包括:1)JVM调优,如分代ZGC配置和结构化并发优化;2)代码级优化,包括向量API加速数据处理和零拷贝I/O;3)容器化环境优化,如K8s资源匹配和节能模式配置;4)监控分析工具使用。通过实践表明,这些优化能显著提升性能(响应时间降低40-60%)同时降低资源消耗(内存减少30-50%,CPU降低20-40%)和能耗(服务器功耗减少15-35%)。建议采用渐进式优化策略。
678 2
|
Java 物联网 数据处理
Java Solon v3.2.0 史上最强性能优化版本发布 并发能力提升 700% 内存占用节省 50%
Java Solon v3.2.0 是一款性能卓越的后端开发框架,新版本并发性能提升700%,内存占用节省50%。本文将从核心特性(如事件驱动模型与内存优化)、技术方案示例(Web应用搭建与数据库集成)到实际应用案例(电商平台与物联网平台)全面解析其优势与使用方法。通过简单代码示例和真实场景展示,帮助开发者快速掌握并应用于项目中,大幅提升系统性能与资源利用率。
370 6
Java Solon v3.2.0 史上最强性能优化版本发布 并发能力提升 700% 内存占用节省 50%
|
缓存 监控 Cloud Native
Java Solon v3.2.0 高并发与低内存实战指南之解决方案优化
本文深入解析了Java Solon v3.2.0框架的实战应用,聚焦高并发与低内存消耗场景。通过响应式编程、云原生支持、内存优化等特性,结合API网关、数据库操作及分布式缓存实例,展示其在秒杀系统中的性能优势。文章还提供了Docker部署、监控方案及实际效果数据,助力开发者构建高效稳定的应用系统。代码示例详尽,适合希望提升系统性能的Java开发者参考。
671 4
Java Solon v3.2.0 高并发与低内存实战指南之解决方案优化