Java内存模型——JMM

本文详细介绍了Java内存模型(JMM),包括其工作原理、关键交互操作及如何确保数据的一致性。通过实例阐述了volatile关键字如何解决多线程环境下的可见性问题,并探讨了原子性与有序性的概念。

什么是Java内存模型?

JMM((JavaMemoryModel)即为Java内存模型,JVM(Java Virtual Machine)是Java虚拟机,两者不能混为一谈。

下图为JMM模型图

JMM
  其流程简单来说,就是主内存中的变量先写入到工作内存中,接着交给线程去进行运算,运算后工作内存中的变量副本值发生改变,然后再重新写入到主内存中。

其交互操作如下所示:

  • lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
  • unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
  • use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
  • assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
  • store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
  • write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

用张图来解释一下:

在这里插入图片描述
  工作内存中的变量副本是线程独占的,不同于主内存中的共享变量,它不能被其他线程读取到值。也就是说,如果有A、B两个线程同时运行,当A线程的进行到第④步时,变量副本值发生修改i=1,但由于没有写回到主内存中,还处于线程独占的状态,B线程便不会知道i的值发生变化,如果B线程中是一个循环,当i=1时跳出循环,那么B线程永远不会跳出循环。
  那如何让B线程知道A线程中的变量值发生了改变呢?这个时候就引入了一个概念,可见性——当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

关键字volatile

  仔细的同学会发现,上述列的交互操作一共有8个,而图中只有6个,剩下的lock和unlock没有出现!而这就是volatile实现的关键点。
  在讲volatile的实现原理之前,要先提一下MESI缓存一致性协议,它提供一个嗅探机制(可以简单理解为监听,具体是硬件层面的实现,个人也不太了解),在缓存(工作内存)写入到主内存时,使其他线程缓存中的该变量副本失效,不得不去主内存中获取正确的值。
  volatile修饰的变量,在进行到第⑤步时,会加上一个lock操作,直到第⑥步完成之后,才会unlock,而这段时间内,别的线程是不能进行⑤⑥两步操作的。同时由于MESI的存在,线程B便能从主内存中拿到正确的i值进行操作。
  一句话概括:volatile — 保证读写的都是主内存的变量。

原子性,可见性,有序性

  可见性已经说过了,这里着重讲一下原子性和有序性。

  • 原子性
      即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
      还是以上面那张图来做解释,假设A,B线程相同都执行1W次加1的操作,如果A线程执行到第④步,处理器没有继续操作第⑤步,转而去B线程完成第④步操作,这个时候问题就出现了。
      由于A线程没有及时的完成第⑤步,B线程中的变量副本依旧有效,所以他能够完成加1的操作,随后A,B线程都向主内存中写值,那么两次加1的操作最终等于只完成了一次。
      这个时候,synchronized同步代码块的作用就体现出来了,它的底层是对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则就只能等待。而这个获取的过程是互斥的,所以同一时刻只有一个线程能够获取到monitor。

直观感受一下

在这里插入图片描述
在这里插入图片描述

  • 有序性
      为了优化性能,编译器和处理器通常会对指令做重排序。
      这里以一个双重锁的单例模式来介绍。
public class SecondSingleton {
    //volatile关键字保证可见性 同时禁用指令重排(jdk1.5后生效)
    private static volatile SecondSingleton singleton;
    
    private SecondSingleton(){     
    }
    
    public static SecondSingleton getSingleton() {
        if (singleton == null) {
            synchronized (SecondSingleton.class) {
                if(singleton == null){
                    singleton = new SecondSingleton();
                }
            }
        }
        return singleton;
    }
}

  singleton = new SecondSingleton()这句,并非是一个原子操作,他在 JVM 中这句话大概做了下面 3 件事情:

     1. 给 singleton 分配内存
     2. 调用 SecondSingleton的构造函数来初始化成员变量
     3. 将singleton 对象指向分配的内存空间
  这时,如果进行重排序,执行顺序改为了1->3->2,同时另一个线程第3步执行完而第2步未执行时在执行语句:

 if(singleton == null){
     singleton = new SecondSingleton();
 }

  那么这时的singleton已经是有内存空间的(非null),但此时它没有成员变量,所以在实例化的时候会出现报错。
  如何避免这个问题? 只需要声明变量的时候加上一个volatile便可以,它能禁止处理器的指令重排序。

最后附一张图方便记忆

在这里插入图片描述

### Java内存模型JMM)的定义 Java内存模型JMMJava Memory Model)作为Java语言规范的一部分,旨在规定多线程环境下变量访问规则,尤其是共享变量的可见性和有序性[^1]。此模型确保不同平台上运行的Java程序能够具有一致的行为模式。 #### 抽象概念区分 为了更好地理解和应用JMM,需明确两个重要抽象概念——主存与工作内存: - **主存**:代表所有线程可访问的公共存储空间;它涵盖了诸如堆和方法区这样的区域。 - **工作内存**:每个线程拥有自己独立的工作副本,其中保存了该线程正在使用的局部变量和其他状态信息,类似于实际计算机体系结构中的寄存器或高速缓存[^2]。 这种设计使得即使是在高度并行化的环境中执行代码时也能保持数据的一致性和准确性。 #### 原子性、有序性及可见性的保障机制 JMM围绕三个主要特性构建其核心功能: - **原子性**:保证某些特定操作要么全部完成,要么完全不发生; - **有序性**:防止编译器重排序优化破坏程序逻辑顺序; - **可见性**:当一个线程修改了一个共享变量之后,其他线程可以立即看到这个变化[^4]。 这些特性的实现依赖于一系列复杂的协议和技术手段来协调多个处理器之间的交互过程,并且还涉及到如何处理锁竞争等问题。 ```java // 使用 volatile 关键字确保字段更新对于所有线程都是可见的 public class SharedResource { private volatile boolean flag = false; public void setFlag(boolean value) { this.flag = value; } public boolean getFlag() { return this.flag; } } ``` 上述例子展示了`volatile`关键字的应用场景,它可以用来增强单个布尔型标志位或其他简单类型的读写操作的安全性,从而简化同步控制逻辑的同时提高性能效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值