马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
Java内存模型(Java Memory Model,简称JMM)是Java虚拟机(JVM)规范中界说的一套规则,用于形貌多线程环境下变量如何被访问和同步。在多线程编程中,内存模型的紧张性不言而喻,它直接关系到程序的正确性和性能。本文将深入探讨Java内存模型的原理、特性、示例以及总结。
一、基本概念
1. 主内存与工作内存
- 主内存:主内存是JVM中所有线程共享的内存区域,用于存储Java程序中的变量和对象实例等数据。所有的共享变量都存储在主内存中,包罗实例字段、静态字段和数组元素等。
- 工作内存:每个线程都有本身的工作内存,也称为本地内存。工作内存是线程私有的,用于存储主内存中共享变量的副本。线程对变量的所有操纵(读取、写入)都在工作内存中进行,而不是直接操纵主内存。
2. 内存交互操纵
JMM界说了以下八种操纵来完成主内存和工作内存之间的交互:
- lock(锁定):作用于主内存的变量,将变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存的变量,释放处于锁定状态的变量,允许其他线程锁定该变量。
- read(读取):作用于主内存的变量,将变量值从主内存传输到线程的工作内存中。
- load(载入):作用于工作内存的变量,将read操纵从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,将工作内存中的一个变量值传递给执行引擎。
- assign(赋值):作用于工作内存的变量,将执行引擎吸收到的值赋给工作内存的变量。
- store(存储):作用于工作内存的变量,将工作内存中的一个变量的值传送到主内存中。
- write(写入):作用于主内存的变量,将store操纵从工作内存中得到的变量的值放入主内存的变量中。
三、特性
1. 原子性(Atomicity)
- 界说:一个或多个操纵,要么全部执行,要么全部不执行,在执行过程中不会被任何因素打断。
- 实现方式:在Java中,对基本数据范例的访问和操纵是原子的,例如对int、long、short、byte、char、boolean和reference范例的读写操纵都是原子的。对于复合操纵(如i++),则必要通过同步机制(如synchronized、Lock等)来保证原子性。
2. 可见性(Visibility)
- 界说:一个线程对共享变量的修改,能够被其他线程看到。
- 实现方式:Java内存模型通过volatile关键字和synchronized关键字来实现可见性。
- volatile关键字:当一个变量被声明为volatile时,对该变量的读写操纵都会直接作用于主内存,从而保证了可见性。但是,volatile关键字不能保证操纵的原子性。
- synchronized关键字:synchronized关键字通过锁定机制来确保只有一个线程可以执行一段代码。当一个线程访问一个对象的synchronized方法或代码块时,它将得到该对象的锁,其他线程则必须等待。在解锁之前对变量的修改对其他线程是可见的。
3. 有序性(Ordering)
- 界说:程序执行的次序按照代码的先后次序执行。
- 实现方式:Java内存模型通过Happens-Before原则来界说操纵之间的偏序关系,从而允许一定水平的重排序,但同时又保证程序终极执行的效果与预期同等。
三、Happens-Before原则
Happens-Before原则是Java内存模型中界说的一组偏序关系,用于判断两个操纵之间的内存可见性和有序性。它包罗以下几种环境:
* 程序次序规则:
一个线程中的每个操纵,Happens-Before于该线程中的任意后续操纵。
* 监视器锁规则:
对一个锁的解锁,Happens-Before于随后对这个锁的加锁。
* volatile变量规则:
对一个volatile变量的写,Happens-Before于任意后续对这个volatile变量的读。
* 传递性:
如果A Happens-Before B,且B Happens-Before C,那么A Happens-Before C。
* 线程启动规则:
Thread对象的start()方法调用Happens-Before于该线程的每一个动作。
* 线程停止规则:
线程的所有操纵都Happens-Before于其他线程检测到这个线程已经停止。
* 线程中断规则:
对线程interrupt()方法的调用Happens-Before于被中断线程的代码检测到中断事件的发生。
* 对象终结规则:
一个对象的初始化完成(构造函数执行竣事)Happens-Before于它的finalize()方法的开始。
四、代码示例
1. 可见性示例
代码示例:
- public class VisibilityExample {
- private static boolean ready;
- private static int number;
- public static void main(String[] args) throws InterruptedException {
- Thread one = new Thread(() -> {
- number = 42;
- ready = true; // ①
- });
- Thread two = new Thread(() -> {
- while (!ready) {
- Thread.onSpinWait(); // ②
- }
- System.out.println(number); // ③
- });
- one.start();
- two.start();
- one.join();
- }
- }
复制代码 问题分析:
- 在这个例子中,线程one设置了number的值并标记ready为true。线程two在一个循环中等待ready变为true,然后打印number的值。
- 如果没有正确的同步机制,线程two可能看不到线程one对number和ready的修改,因为这两个线程的工作在不同的内存区域中。这可能导致线程two打印出number的初始值(0),而不是线程one设置的值(42)。
办理方案:
- 可以使用volatile关键字或synchronized关键字来确保内存可见性。在这个例子中,如果将ready声明为volatile,则线程one对ready的修改将立刻对线程two可见,从而确保线程two能够正确打印出number的值。
修改后的代码:
- public class VisibilityExample {
- private static volatile boolean ready; // 使用volatile关键字
- private static int number;
- public static void main(String[] args) throws InterruptedException {
- Thread one = new Thread(() -> {
- number = 42;
- ready = true; // ①
- });
- Thread two = new Thread(() -> {
- while (!ready) {
- Thread.onSpinWait(); // ②
- }
- System.out.println(number); // ③
- });
- one.start();
- two.start();
- one.join();
- }
- }
复制代码 2. 有序性示例
代码示例:
- public class Singleton {
- private static Singleton instance;
- public static Singleton getInstance() {
- if (instance == null) {
- synchronized (Singleton.class) {
- if (instance == null) {
- instance = new Singleton();
- }
- }
- }
- return instance;
- }
- }
复制代码 问题分析:
- 在这个例子中,Singleton类使用了双重检查锁定(Double-Checked Locking)模式来确保单例模式的安全性。
- 然而,由于指令重排序的存在,这种实现方式在某些环境下可能会出现问题。具体来说,new Singleton()这个操纵现实上会有以下三个步骤:
- 分配一块内存M。
- 在内存M上初始化Singleton对象。
- 将M的地点赋值给instance变量。
- 如果发生指令重排序,次序可能会变为:
- 分配一块内存M。
- 将M的地点赋值给instance变量。
- 在内存M上初始化Singleton对象。
- 在这种环境下,如果线程A在执行到步骤2时发生线程切换,切换到线程B。线程B在执行getInstance()方法时,会发现instance不为null,因此直接返回instance。然而,此时的instance对象可能还没有被初始化,如果此时访问instance的成员变量就可能触发空指针非常。
办理方案:
- 可以通过在对象引用前添加volatile关键字来办理这个问题。volatile关键字可以克制指令重排序,确保对象在初始化完成后再将引用赋值给变量。
修改后的代码:
- public class Singleton {
- private static volatile Singleton instance;
- public static Singleton getInstance() {
- if (instance == null) {
- synchronized (Singleton.class) {
- if (instance == null) {
- instance = new Singleton();
- }
- }
- }
- return instance;
- }
- }
复制代码 五、内存模型与硬件内存架构的关系
Java内存模型并不是对硬件内存架构的简单模仿,而是对其的一种抽象和简化。当代盘算机为了高效运行,在CPU与内存之间设置了高速缓存(Cache)作为数据缓冲,这一机制在多线程环境下可能会引发缓存同等性问题。
为了办理这个问题,处置惩罚器必要遵循一些缓存同等性协议,如MSI、MESI(Modified, Exclusive, Shared, Invalid)等。这些协议确保了多个处置惩罚器核心在访问共享内存时,能够保持数据的同等性。例如,在MESI协议中,缓存行(Cache Line)有四种状态:修改(Modified)、独占(Exclusive)、共享(Shared)和无效(Invalid)。通过这些状态,处置惩罚器可以跟踪缓存行的数据是否与其他核心保持同等。
Java内存模型(JMM)在设计时,考虑了这些硬件层面的缓存同等性问题。JMM界说了一套规范,用于形貌变量在内存中的存储方式、线程如何读取和写入内存中的变量,以及这些操纵如何与其他线程进行同步。JMM的核心目的是确保在多线程环境中,程序的执行效果具有可预测性,即程序的行为与单线程环境下的行为同等(在遵守JMM规则的前提下)。
为了实现这一目的,JMM引入了几个关键概念:
1. 主内存与工作内存:
JMM将内存分为两部分:主内存(Main Memory)和工作内存(Working Memory)。主内存是所有线程共享的,它存储了程序中的所有变量。每个线程都有本身的工作内存,它是线程私有的,存储了线程从主内存中读取的变量的副本。线程对变量的所有读写操纵都必须在工作内存中进行,不能直接操纵主内存中的变量。
2. volatile关键字:
volatile是Java中的一个关键字,用于修饰变量。被volatile修饰的变量具有可见性和有序性两个特性。可见性意味着当一个线程修改了volatile变量的值,其他线程会立刻看到这个修改。有序性则限制了编译器和处置惩罚器对volatile变量相关操纵的重排序,从而保证了程序执行的正确性。
3. 原子性、可见性和有序性:
原子性是指操纵是不可中断的,要么全部执行完,要么完全不执行。可见性是指一个线程对共享变量的修改对其他线程是可见的。有序性是指程序执行的次序是按照代码的次序来的(在遵守JMM规则的前提下)。JMM通过一系列规则来保证这三个特性的实现。
4. 先行发生原则(Happens-Before):
这是JMM界说的一种偏序关系,用于形貌两个操纵之间的执行次序。如果操纵A先行发生于操纵B,那么操纵A对共享变量的影响在操纵B中是可见的。先行发生原则是判断数据是否存在竞争、线程是否安全的主要依据。
总结
Java内存模型(JMM)是JVM规范中界说的多线程环境下变量访问和同步的规则。它抽象了硬件内存架构,分为主内存和工作内存,线程通过工作内存与主内存交互。JMM保证了原子性、可见性和有序性,此中volatile和synchronized是关键实现方式。Happens-Before原则界说了操纵间的偏序关系,确保内存可见性和有序性。示例展示了可见性和有序性问题及办理方案。JMM还考虑了硬件缓存同等性问题,通过规范确保多线程环境下程序执行的可预测性。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |