JAVA内存模型


JVM 内存结构和 Java 虚拟机的运行时区域有关;

Java 内存模型和 Java 的并发编程有关。


JVM内存模型

6个区域

堆区(Heap):堆是存储类实例和数组的,通常是内存中最大的一块。实例很好理解,比如 new Object() 就会生成一个实例;而数组也是保存在堆上面的,因为在 Java 中,数组也是对象。

虚拟机栈(Java Virtual Machine Stacks):保存局部变量和部分结果,并在方法调用和返回中起作用。

方法区(Method Area):存储每个类的结构,例如运行时的常量池、字段和方法数据,以及方法和构造函数的代码,包括用于类初始化以及接口初始化的特殊方法。

本地方法栈(Native Method Stacks):与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的 Java 方法服务,而本地方法栈则是为 Native 方法服务

程序计数器(The PC Register):是最小的一块内存区域,它的作用通常是保存当前正在执行的 JVM 指令地址

运行时常量池(Run-Time Constant Pool):是方法区的一部分,包含多种常量,范围从编译时已知的数字到必须在运行时解析的方法和字段引用。


.java->.class->机器指令->cpu运行


JMM 里最重要 3 点内容,分别是:重排序、原子性、内存可见性。


重排序:一个 Java 程序,编译器、JVM 或者 CPU ,出于优化等目的,对于实际指令执行的顺序进行调整。

重排序通过减少执行指令,从而提高整体的运行速度


原子性:一系列的操作,要么全部发生,要么全部不发生,不会出现执行一半就终止的情况。

比如转账行为就是一个原子操作,该过程包含扣除余额、银行系统生成转账记录、对方余额增加等一系列操作。虽然整个过程包含多个操作,但由于这一系列操作被合并成一个原子操作,所以它们要么全部执行成功,要么全部不执行,不会出现执行一半的情况。比如我的余额已经扣除,但是对方的余额却不增加,这种情况是不会出现的,所以说转账行为是具备原子性的。而具有原子性的原子操作,天然具备线程安全的特性。

不具备原子性的例子:i++(3 个指令:读取 增加 保存 )

原子操作:

1、除了 long 和 double 之外的基本类型(int、byte、boolean、short、char、float)的读/写操作,都天然的具备原子性;

2、所有引用 reference 的读/写操作;

3、加了 volatile 后,所有变量的读/写操作(包含 long 和 double)。这也就意味着 long 和 double 加了 volatile 关键字之后,对它们的读写操作同样具备原子性;

4、在 java.concurrent.Atomic 包中的一部分类的一部分方法是具备原子性的,比如 AtomicInteger 的 incrementAndGet 方法。

原子操作 + 原子操作 != 原子操作


内存可见性:

volatile


JMM的抽象:主内存和工作内存

img

每个线程只能够直接接触到工作内存,无法直接操作主内存,而工作内存中所保存的正是主内存的共享变量的副本,主内存和工作内存之间的通信是由 JMM 控制的。

如果一个变量 x 被线程 A 修改了,只要还没同步到主内存中,线程 B 就看不到,所以此时线程 B 读取到的 x 值就是一个过期的值,这就导致了可见性问题。


什么是 happens-before 关系

描述和可见性相关问题

如果第一个操作 happens-before 第二个操作,就说第一个操作对于第二个操作一定是可见的

Happens-before 关系的规则:

1、单线程规则

2、锁操作规则

3、volatile 变量规则

4、线程启动规则

5、线程 join 规则

6、中断规则

7、并发工具类的规则


volatile 是什么

Java关键字 一种同步机制。

当某个变量是共享变量,且这个变量是被 volatile 修饰的,那么在修改了这个变量的值之后,再读取该变量的值时,可以保证获取到的是修改后的最新的值,而不是过期的值。

相比于 synchronized 或者 Lock,volatile 是更轻量的,因为使用 volatile 不会发生上下文切换等开销很大的情况,不会让线程阻塞。但正是由于它的开销相对比较小,所以它的效果,也就是能力,相对也小一些。

不适用:i++ volatile 不能保证原子性

适用:布尔标记位、触发器、

如果某个共享变量自始至终只是被各个线程所赋值或读取,而没有其他的操作(比如读取并在此基础上进行修改这样的复合操作)的话,那么我们就可以使用 volatile 来代替 synchronized 或者代替原子类,因为赋值操作自身是具有原子性的,volatile 同时又保证了可见性,这就足以保证线程安全了。

一个比较典型的场景就是布尔标记位的场景,例如 volatile boolean flag。因为通常情况下,boolean 类型的标记位是会被直接赋值的,此时不会存在复合操作(如 a++),只存在单一操作,就是去改变 flag 的值,而一旦 flag 被 volatile 修饰之后,就可以保证可见性了,那么这个 flag 就可以当作一个标记位,此时它的值一旦发生变化,所有线程都可以立刻看到,所以这里就很适合运用 volatile 了。

volatile 的作用:保证可见性、禁止重排序


单例模式的双重检查锁模式为什么必须加 volatile?

单例模式指的是,保证一个类只有一个实例,并且提供一个可以全局访问的入口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Singleton {

    private static volatile Singleton singleton;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

方法中首先进行了一次 if (singleton == null) 的检查,然后是 synchronized 同步块,然后又是一次 if (singleton == null) 的检查,最后是 singleton = new Singleton() 来生成实例。

我们进行了两次 if (singleton == null) 检查,这就是“双重检查锁”这个名字的由来。这种写法是可以保证线程安全的,假设有两个线程同时到达 synchronized 语句块,那么实例化代码只会由其中先抢到锁的线程执行一次,而后抢到锁的线程会在第二个 if 判断中发现 singleton 不为 null,所以跳过创建实例的语句。再后面的其他线程再来调用 getInstance 方法时,只需判断第一次的 if (singleton == null) ,然后会跳过整个 if 块,直接 return 实例化后的对象。

这种写法的优点是不仅线程安全,而且延迟加载、效率也更高。

在双重检查锁模式中为什么需要使用 volatile 关键字

singleton = new Singleton() ,它并非是一个原子操作,

img