谈谈对 JMM 的理解

前文

volatile 与 synchronized 详解

一文搞懂 Java 线程

什么是 JMM?

JMM 即为 Java 内存模型(Java Memory Model)。是一种抽象的概念并不真实存在,它描述的是一组规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式

JMM 关于同步的规定:

  • 线程解锁之前,必须把共享变量的值刷新回主内存
  • 线程加锁之前,必须读取主内存的最新值到自己的工作内存
  • 加锁解锁是同一把锁

由于 JVM 运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而 Java 内存模型中规定所有变量都存在 主内存 中,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到的线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的 变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,如下图所示:
在这里插入图片描述

可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值

volatile 关键字

volatile 这个关键字可能很多朋友都听说过,或许也都用过。在 Java 5 之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在 Java 5 之后,volatile 关键字才得以重获生机。关于 volatile 的更多介绍请看这里 volatile 与 synchronized 详解

volatile 关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事。volatile 保证了可见性但是没有保证原子性,而且 volatile 是进制指令重排序的

原子性

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行

有序性

即程序执行的顺序按照代码的先后顺序执行

    在 Java 内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性

    在Java里面,可以通过 volatile 关键字来保证一定的“有序性”。另外可以通过 synchronized 和 Lock 来保证有序性,很显然,synchronized 和 Lock 保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

    另外,Java内存模型具备一些先天的 “有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从 happens-before 原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序

happens-before 原则

什么是 happens-before?

    一方面,程序员需要 JMM 提供一个强的内存模型来编写代码;另一方面,编译器和处理器希望 JMM 对它们的束缚越少越好,这样它们就可以最可能多的做优化来提高性能,希望的是一个弱的内存模型

    JMM 考虑了这两种需求,并且找到了平衡点,对编译器和处理器来说,只要不改变程序的执行结果(单线程程序和正确同步了的多线程程序),编译器和处理器怎么优化都行

    而对于程序员,JMM 提供了 happens-before 原则(JSR-133 规范),满足了程序员的需求——简单易懂并且提供了足够强的内存可见性保证。换言之,程序员只要遵循 happens-before 原则,那他写的程序就能保证在 JMM 中具有强的内存可见性

    JMM 使用 happens-before 的概念来定制两个操作之间的执行顺序,这两个操作可以在一个线程以内,也可以是不同的线程之间。因此,JMM 可以通过 happens-before 关系向程序员提供跨线程的内存可见性保证

happens-before 关系的定义如下:

  • 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第一个操作之前
  • 两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果,与按 happens-before 关系来执行的结果一致,那么 JMM 也允许这样的重排序

happens-before 关系本质上和 as-if-serial 语义是一回事

    as-if-serial 语义保证单线程内重排序后的执行结果和程序代码本身应有的结果是一致的,happens-before 关系保证正确同步的多线程程序的执行结果不被重排序改变

    总之,如果操作 A happens-before 于操作 B,那么操作 A 在内存上所做的操作对操作 B 都是可见的,不管它们在不在一个线程

天然的 happens-before 关系

在 Java 中,有以下天然的 happens-before 关系:

  • 程序顺序规则:一个线程中的每一个操作,happens-before 于该线程中的任意后续操作
  • 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁
  • volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读
  • 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C
  • start 原则:如果线程 A 执行操作 ThreadB.start() 启动线程 B,那么 A 线程的 Thread.start() 操作 happens-before 于线程 B 中的任意操作
  • join 原则:如果线程 A 执行操作 ThreadB.join() 并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 Thread.join() 操作成功返回

举例:

int a = 1;        // A 操作
int b = 2;        // B 操作
int sum = a + b;  // C 操作
System.out.println(sum);

根据以上介绍的 happens-before 原则,假如只有一个线程,那么不难得出:

A happens-before B
B happens-before C
A happens-before C

注意,真正在执行指令的时候,其实 JVM 有可能对操作 A & B 进行重排序,因为无论先执行 A 还是 B,它们都对对方是可见的,并且不影响执行结果

如果这里发生了重排序,这在视觉上违背了 happens-before 原则,但是 JMM 允许这样的重排序的

所以,我们只关心 happens-before 原则,不用关心 JVM 到底是什么执行的,只要确定操作 A happens-before 操作 B 就行了

重排序有两类,JMM 对这两类重排序有不同的策略:

  • 会改变程序执行结果的重排序,比如 A ——> C,JMM 要求编译器和处理器都禁止这种重排序
  • 不会改变程序执行结果的重排序,比如 A ——> B,JMM 对编译器和处理器不做要求,允许这种重排序

代码演示

比如我们先定义一个资源类 DemoNumber

class DemoNumber {
	// 定义一个 number 字段,并赋值为 10
    int number = 10;
    // 定义一个方法来修改 number 的值,并且返回该值
    public int increment() {
        this.number = 1024;
        return this.number;
    }
}

我们都知道 main 方法其实也是一个线程(主线程)来的,所以这里我们只需要在 main 方法里面定义一个线程就可以了,这样就可以实现上述图中的两个线程了,代码如下:

public class TestDemo {
    public static void main(String[] args) {
        DemoNumber demoNumber = new DemoNumber();
        new Thread(() -> {
            try {
                System.out.println("****** come in ******");
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " update number is " + demoNumber.increment());
        },"AAA").start();


        while (demoNumber.number == 10) {
        	// 需要有一种通知机制告诉 main 线程,number 已经修改为 1024 了,跳出 while 循环
        }
        System.out.println("main is over");
    }
}

OK,这是一个很简单的一个程序,我们运行一下
在这里插入图片描述
可以发现,我们的程序并没有输出 main is over 这个结果,也就是说其实 AAA 线程已经将 number 的值修改为 1024 了,但是 main 线程不知道,所以 main 线程得到的值还是 10,所以会一直执行 while 循环


所以这里我们就可以加上 volatile 关键字来修饰 number 这个变量

// 加上 volatile 关键字
volatile int number = 10;

main 方法的代码不用改变,我们再运行一下程序
在这里插入图片描述
这时的程序就可以运行并结束了,因为 AAA 线程会将 number 的值从主内存中读取 number 的值并拷贝到工作内存中修改为 1024,修改之后写回到主内存中(刷新主内存的值),然后 main 线程会去主内存中读取 AAA 修改后的 number 的值,所以这里也就不会走 while 循环了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值