Java 并发编程[1] -- Java 内存模型

Java 并发编程[1] – Java 内存模型

推荐无广告版本:Java 并发编程[1] – Java 内存模型

一. 并发的意义

  1. 处理器性能的提高已经从 主频 的提升转变为 核心 的堆加。而并发编程是我们使用更多核心的基础,并发的合理应用能极大提高应用的性能。
  2. 在应用中往往使用计算机中的各种资源,而不同类型的资源速度、个数有些许的差异。在某种资源成为应用性能瓶颈的时候,可以让 CPU 做其他运算。这样,并发编程能够提高 CPU(即使是单核) 的利用率。

二. 并发模型

1. 多进程 与 多线程

在并发编程中,有两种选择:多进程 or 多线程。学过操作系统的都知道它们的区别。只简单的用一句话描述它们的区别:++进程是资源分配的基本单位,而线程是调度的基本单位++。

而在 Java 中,并发编程中最多使用的是:多线程编程。当然,Java 也可以进行多进程编程,使用 Runtime.exec() 创建一个进程(底层使用 fork() 实现,可以用它来执行脚本)。但是,使用的不多。以下的内容都是针对的 Java 多线程。

下面认识两种并发模型(这两种模型同样适用于多进程)。

在开始之前,我们需要认识到并发编程两个关键问题:通信同步。通信,说明的线程间的交流。因为能够通信的线程在并发中才有意义(针对大部分线程来说);同步,讲究的是线程的执行顺序。在一些应用中,线程的执行往往有先后要求的。比如,写线程写完后,读线程才能开始读。经典的 生产者-消费者 问题,就一种同步问题。下面的两种并发模型,针对这两个关键问题有不同的要求。

2. 两种并发模型

基于 共享内存 的并发模型中,所有的线程有一个共享的内存空间。

  • 通信:隐式的。因为各种线程都可以读写共享内存。它们间的交流可通过读写共享内存完成。
  • 同步:需要显式的。

基于 消息 的并发模型中,线程间可以进行消息传递。

  • 通信,需要的显式的。因为线程无法看到其他线程的内存,所以只能通过显式发送、接受消息来完成通信。
  • 同步,隐式的。因为消息的收发是有前后的,所有同步就是隐式的了。

在 Java 中,使用的是基于共享内存的编程模型。Java 中线程使用共享内存来通信,而 Java 虚拟机(JVM)为程序员保证共享内存对线程的可见性。至于,线程的同步就需要程序员自己控制了。但这里需要注意的是,虽然 JVM 提供了内存可见性的支持,但是是有条件的。也就是说,我们必须在其描述的环境下才能获取保证的内存可见性。

Java 内存模型(JMM)就是 JVM 对内存可见性实现的综述。所以,要想深入学习 Java 并发编程,了解 JMM 是很有必要的。

三. 初探 JMM

1. 内存可见性的障碍

内存可见性保证了某一时刻各个线程所见到的共享内存是一致的。如果之前没有并发编程经验,可以会为此感到惊讶。因为,在单线程中无论如何也不会发生同一个变量有不同值的情况。为什么多线程会出现这种情况,认识了内存可见性的障碍就豁然开朗了。

内存可见性有两大障碍:本地内存(缓存)重排序

花开两朵,各表一枝。先看,本地内存,其实它是 JMM 中的一个抽象概念(见下图)。为了方便理解,暂且把它视为 缓存。缓存这一概念在计算机中非常常见。比如,为了缓和 CPU 与 Memory 的速度差异出现了 Cache,它就是一种缓存机制。想一想 Cache 的作用,就不难理解 JMM 中的本地内存,它也是为了提高 Java 性能而提出了。Java 中每一个线程都有自己本地内存。线程要读取变量时,先要在自己的本地内存中查找。如果命中了直接使用本地内存中的,否则,再去内存中查找。就像你现在想到的一样,它虽提高了性能,但会像 Cache 那样读到赃的数据(一个线程正要读取变量a时,恰巧被挂起了而去执行另一个线程。巧的是,该线程要修改变量a。这样前面线程恢复时,由于受缓存的影响读到的变量a的值可能不是最新的)。所以,本地内存给内存可见性带来了极大的障碍。但是,为了性能,我们又不能丢弃它。怎么解决呢?(暂且不说,想想 Cache 是怎么解决类似问题的)

Java 并发编程[1] -- Java 内存模型_第1张图片

再看,重排序。可能这个概念我们比较陌生。其实很简单,说白了就是打破原有的顺序。打破什么的顺序呢?一是 Java 代码的顺序,另一个是指令的顺序。这两者有什么区别呢?前者是在编译期间完成的(编译器重排序),而后者是在执行时完成的(处理器重排序)。为什么重排序呢?还是,为了提高 Java 的性能。比如,一些编译优化手段与代码顺序有关、并行指令优化技术 等等。

再具体看重排序对内存可见性的影响。考虑下面代码,现在有两个线程共享 ReorderExample 的实例:线程A率先调用 writer() 方法,线程B紧接着调用 read() 方法。那线程B 一定能看到线程A对 a 的修改吗?答案是:不一定。其实,我们通过了解了 本地内存 就能解释它的原因。现在,忽略缓存的影响,那一定能看到 a 修改后的值吗?答案还是:不一定。这是因为受 重排序 的影响。重排序后,执行顺序可能变成下面图中那样。代码2)先于代码1),代码3、4)也先于代码1)。如此即使无缓存影响,也不能看到对 a 的修改。怎么解决呢?禁止部分重排序。(具体内容按下不表)

    class ReorderExample {

        int a = 0;
        boolean flag = false;

        public void writer() {
            a = 1;          // 1
            flag = true;    // 2
        }

        public void read() {
            if (flag){          // 3
                int t = a * a;  // 4
            }
        }

    }

Java 并发编程[1] -- Java 内存模型_第2张图片

2. JMM 的两种解释

上面介绍了内存可见性的两大障碍。而 JMM 正是对这两大障碍开刀,为程序员提高透明的内存可见性保障。所以,对 JMM 我们有以下两种解释:

  • JMM 定义了线程和主内存之间的抽象关系。JMM 通过控制主内存与每个线程的本地内存之间的交互,来为 Java 程序员提供内存可见性保证。
  • JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译重排序和处理器重排序,为程序员提供一致的内存可见性保证。

3. JMM 与 happens-before

看到这里,是不是仍对 JMM 有云里雾里的感觉?没关系,Java 大佬考虑到了我们。再 JSR-133 中使用 happens-before 的概念阐述操作之间的内存可见性。在 JMM 中,如果一个操作只从的结果需要另一个操作可见,那么这两个操作之间必须要存在 happens-before 关系(这里提到的两个操作既可以是一个线程之内,也可以是在不同线程之间)。

下图完美阐述了 JMM 与 happens-before 的关系。

Java 并发编程[1] -- Java 内存模型_第3张图片

happens-before 可以说是 JMM 的封装。一个 happens-before 规则对应于一个或多个编译器和处理器重排序规则。对于 Java 程序员来说,happens-before 规则简单易懂,它避免我们为了理解 JMM 提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现方法。

下一篇,让我们开始认识 happens-before

你可能感兴趣的:(Concurrency)