Java内存模型
Java内存模型(JMM,Java Memory Mode)指定了Java虚拟机如何与计算机的主存(RAM)进行工作。如下图,Java内存模型定义了线程和主内存之间的抽象关系,具体如下:
- 共享变量存储在主内存中,每个线程都可以访问。
- 每个线程都有私有的工作内存或者成为本地内存。
- 工作内存只存储该线程对共享变量的副本。
- 线程不能直接操作主内存,只有先操作工作内存之后才能写入主内存。
- 工作内存和Java内存模型一样是抽象的概念,它并不存在,它涵盖了缓存、寄存器、编译器优化以及硬件等。
并发编程的三个重要特性
- 原子性:事务内所有的操作要么都执行要么都不执行。
- 可见性:当一个线程对共享变量进行了修改,那么另外的线程可以立即看到修改后的值。
- 有序性:程序代码在执行过程中的先后顺序。
JVM如何保证三大特性
在多线程的情况下,如果不能保证三大特性可能会出现错误。JVM采用内存模型的机制来屏蔽各个平台和操作系统之间内存访问的差异,以实现让Java程序在各个平台下达到一致的内存访问效果,比如C语言中的整型变量,在某些平台下占用了两个字节的内容,在某平台下则占用了四个字节的内容,Java在任何平台下int类型就是四个字节,这就是一致内存访问效果。
JMM与原子性:Java内存模型只保证了基本读取和赋值的原子性操作,其他的均不保证,如自增。
JMM与可见性:volatile关键字、synchronized关键字和JUC提供的显式锁这三种方式都可以保证可见性的语义。
JMM与有序性:Java内存模型中,允许编译器和处理器对指令进行重排序,上面的三种方式也都可以保证有序性。另外Java内存模型具备一些天生的有序性规则(Happens-before原则)。
happens-before原则:
- 程序次数规则:在一个线程内,代码按照编写时的次序执行。
- 锁定规则:一个unlock操作要先行发生于对同一个锁的lock操作。
- volatile变量规则:对一个变量的写操作要早于对这个变量的读操作。
- 传递规则:如果操作A先于操作B且操作B先于操作C,则操作A肯定先于操作C,说明happends-before原则具备传递性。
- 线程启动规则:Thread对象的start方法先行发生于对线程的任何动作。
- 线程中断规则:对线程执行interrupt方法肯定优先于捕获到中断信息。
- 线程的终结规则:线程中所有的操作都要先行发生于线程的终止检测。
- 对象的终结规则:一个对象初始化的完成先行发生于finalize方法之前。
volatile的原理和实现机制
volatile可以确保可见性和顺序性,到底如何实现的呢?我们可以通过OpenJDK下的unsafe.cpp源码看到被volatile修饰的变量存在于一个“lock;”的前缀,该前缀实质上相当于一个内存屏障,该内存屏障会为指令的执行提供下面几个保障。
- 确保指令重排序时不会将其后面的代码排到内存屏障之前。
- 确保指令重排序时不会将其前面的代码排到内存屏障之后。
- 确保在执行到内存屏障修饰的指令时前面的代码必须全部执行完成。
- 强制将线程工作内存中值的修改刷新至主内存中。
- 如果是写操作,则导致其他线程工作内存(CPU Cache)中的缓存数据失效。
volatile使用场景
- 开关控制(利用可见性特点)。
- 状态标记(利用顺序性特点)。
- Singleton设计模式的double-check(利用顺序性特点)。
volatile和synchronized区别
- volatile关键字只能用于修饰实例变量或者类变量,不能用于修饰方法以及方法参数和局部变量、常量等。
- synchronized关键字不能用于对变量的修饰,只能用于修饰方法或者语句块。
- volatile修饰的变量可以为null,synchronized关键字同步语句块的monitor对象不能为null。
- synchronized关键字能保证原子性,而volatile无法保证。(原子性)
- synchronized关键字借助于JVM指令的monitor enter和moniter exit使得同步代码串行化,在monitor exit时所有共享资源都会被刷新到主内存中。(可见性)
- volatile使用机器指令(偏硬件)“.lock;”的方式迫使其他线程工作内存中的数据失效,不得不到主内存中进行再次加载。(可见性)
- volatile关键字禁止JVM编译器以及处理器对其进行重排序。(有序性)
- synchronized关键字是以程序的串行化执行来保证有序性。(有序性)
- volatile不会使线程陷入阻塞,synchronized关键字会使线程进入阻塞。
7种单例设计模式
前面有一篇文章也提到了设计模式,这里主要讲的是单例模式。
- 饿汉式:实例instance初始化收集进方法中,在多线程情况下不可能被实例化两次。
- 懒汉式:使用实例instance的时候再去创建,但是在多线程下无法保证单例的唯一性。
- 懒汉式+同步方法:在懒汉式基础上给getInstance方法加上synchronized关键字,保证数据同步性。
- Double-Check:定义未被初始化的实例,然后在实例首次初始化时加锁,但是无法保证实例化的有序性。
- Volatile+Double-Check:对未被初始化的实例加上volatile关键字保证有序性。
- Holder方式:实例放到静态内部类Holder之中,实例的创建过程在Java程序编译时期收集至方法,目前是使用最广的设计模式之一。
- 枚举方法:枚举类型不允许继承,同样是线程安全且只能被实例化一次。
Holder单例模式代码:
public class HolderSingleton {
public HolderSingleton() {
}
static class Holder {
private static final HolderSingleton instance = new HolderSingleton();
}
![img](https://img-blog.csdnimg.cn/img_convert/7f4b25cc5beaa02f0e18e36e22800011.png)
![img](https://img-blog.csdnimg.cn/img_convert/edcbf62ae925c1a147b2dc48e75d1760.png)
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
-1726115727842)]
[外链图片转存中...(img-Wf4MyJMU-1726115727843)]
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**