关于jmm:JMM学习笔记二-规则和volatile

6次阅读

共计 17744 个字符,预计需要花费 45 分钟才能阅读完成。

学生: 老师,我想请问为什么在月球上物体也会有坠落呢,和在地球上一样诶?

老师: 因为他们都遵循雷同的法则, 咱们依据法则就能够预测行为。但你察看到没有在月球上的坠落速度会慢一点。

到底什么是内存模型?

这里咱们来回顾一下《JMM 学习笔记(一) 跨平台的 JMM》讲述的货色,在这篇文章外面有两条线, 第一条是硬件性能晋升带来的问题,在单核时代,晋升 CPU 的方向是优化架构性能和晋升主频速度,然而遗憾的是主频并不能无限度的晋升,主频进步过一个拐点之后,功耗会爆炸晋升。但咱们还须要更强、更快的 CPU,多核是一剂良药,引入了多核当前,如何晋升 CPU 运算性能的问题失去了解决,咱们就能够通过多核来一直的晋升 CPU 的性能了,某种程度上来说,咱们也能够了解为提供给软件的计算资源在一直的减少,那摆在开发者背后的一个问题是如何更好的应用这些计算资源,如何晋升计算资源的使用率。咱们将联合操作系统的倒退历史来答复这个问题。让咱们从纸带计算机开始讲起,如下图所示

一边读一边执行想来是那时的内存比拟小,然而随着技术的倒退,内存在缓缓的变大,高级语言开始呈现,咱们来察看一下一台计算机同时只能执行一个程序会遇到哪些问题, 咱们先来看下上面一个很简略 C 语言小程序:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// argv 用于接管以命令行形式启动传递的参数
int main(int argc , char* argv[])
{
    int to ,sum = 0;
    // 字符串转 int
    to = atoi(argv[1]);
    // 打印传入的内部参数
    printf("sum: %d\n",to);
    // 获取 output.txt 的指针
    FILE* fp = fopen("output.txt", "w");
    // 获取以后工夫
    clock_t start_time = clock();
    for(int i = 0 ; i <= to ; i++){
        sum = sum + i;
        // sum = sum + 1; 语句一
        // fprintf 将 sum 写入到 fp 指向的文件
        fprintf(fp,"%d",sum); // 语句二
    }
    fclose(fp); 
    clock_t end_time  = clock(); // 获取以后零碎工夫
    double total_time = (double)(end_time - start_time) / CLOCKS_PER_SEC;  // 计算总运行工夫(秒
    printf("Total runtime: %.12f seconds\n", total_time);
}

而后咱们以命令行形式启动这个程序:

而后咱们将语句二正文,将语句一解除正文,看下执行成果:

咱们能够看到循环次数回升,只是将 I / O 语句正文,速度失去了飞快的晋升,就简直不须要破费工夫一样,我执行了十几次,我将循环的次数晋升到了 1000w,然而速度依然很快。咱们来计算下语句二不正文的状况下,均匀每次循环执行的工夫是 0.003/10^4^, 那咱们将语句二正文换成语句一,那么破费的工夫,因为分子是 0,那么能够认为不须要破费工夫?可能有敌人感觉还是不够直观咱们就权且取工夫为 10^-8^ , 而后咱们的循环次数是 10^8^, 那么破费的工夫就是 1 / 10^16^, 咱们能够看到相差的数量级。0.003/10^4^ 是有 I / O 指令破费的工夫。两者的比例近似在 1:3 × 10^9^, 能够看到有 I / O 指令会十分慢, 换句话说一条 I / O 指令所破费的工夫够计算指令执行 3×10^9^ 次,假如咱们有如下一个程序:

蓝色区域的代表有 3×10^9^ 条计算指令,绿色局部代表有一条 I / O 指令,处理器在执行完计算指令之后,就收回 I / O 指令,等磁盘上写入或从磁盘上读取,在这段时间内 CPU 处于闲暇状态,利用率也就是百分之之五十,但理论的程序中咱们碰到的问题可能是三十条计算指令,一条 I / O 指令,二十条计算指令,一条 I / O 指令,换句话说,如果计算机只能执行一个程序,那么 CPU 的使用率接近于零,换言之软件并没有享受到计算能力的晋升,那咱们该如何晋升呢,答案就是并发,交替的执行程序,在期待 I / O 指令的时候,CPU 切换到了其余程序上进行执行。就像咱们去烧热水一样,咱们将水灌进热水壶之后,关上开关,咱们就会去做别的事件。那交替的执行会带来什么问题,咱们先在有两个程序,都加载进了计算机开始执行:

程序 1 执行到第 53 条指令,须要将 CPU 切换到程序 2 开始执行,程序 2 也有 I / O 指令,咱们同样也不让 CPU 闲着,将 CPU 切回到程序 1 执行,那么这个时候 CPU 须要晓得切换到程序 2 执行之前,这个程序执行到了哪里和指定到那个地位的数据(也就是程序执行到 53 条指令的时候,寄存器外面存储的数据),也就是咱们须要一块内存来记录运行中程序的相干数据,咱们看到再引入并发进步 CPU 的利用率的时候,刻化运行中的程序就须要再引入一些新的概念,这是绝对于动态的程序不一样的中央,这也就是过程。引入过程之后,CPU 的使用率失去了进步,但遇到了新的问题,比方一个音乐播放软件,先从音频文件中读取数据,而后对数据进行解压缩,而后执行播放,咱们这里用伪代码示意就是:

main{while(true){read(); // I/O  加载数据     
        decomress(); // CPU 解压
        play(); // 播放}
}

咱们的思路是读一些放一些,然而 I / O 很慢,这就会导致咱们听到的歌曲,唱一句等一些工夫再唱一句,咱们也能够想到将这三个性能拆成三个过程,先让读数据的过程先跑一段时间,再执行解压和播放过程,然而多过程之间共享资源可是个问题,除此之外,过程也须要耗费,咱们是否提出一种实体,每一个实体共享地址空间,这些实体也交付给 CPU 并发的执行,这也就是线程,创立线程的代价要远小于过程切换代价,所须要的空间也远小于过程,但在软件畛域就是这样咱们引入一个机制去解决旧的问题,就会带来新的问题:

In multiprocessor systems, processors generally have one or more layers of memory cache, which improves performance both by speeding access to data (because the data is closer to the processor) and reducing traffic on the shared memory bus (because many memory operations can be satisfied by local caches.).

在多处理器零碎内,处理器通常有一级或多级内存,用来减速读写数据改善性能,因为缓存里 CPU 更近,又能够缩小内存总线上的数据传输(因为一些内存操作能够由本地缓存来满足)

Memory caches can improve performance tremendously, but they present a host of new challenges. What, for example, happens when two processors examine the same memory location at the same time? Under what conditions will they see the same value?

缓存能够极大的晋升性能,然而也带来了一系列新挑战,比方当两个解决同时查看同一内存地位会产生什么? 在什么条件下它们会看到雷同的值。《JSR 133 (Java Memory Model) FAQ》

咱们能够将这个同一内存地位了解为多核 CPU 操纵共享变量,为了解决这个问题,就须要制订协定,这也就是缓存一致性协定,这类的协定有 MESI、MSI、MOSI 等,应用较为宽泛的就是 MESI 协定,MESI 解决了缓存一致性问题,但其实也存在一个性能弱点,处理器执行写内存操作时,必须期待其余所有处理器将其通知缓存中相应正本数据删除并接管到这些处理器回复的音讯之后,能力将数据写入通知缓存。为了躲避和缩小这种期待造成的写操作提早,硬件设计者引入了写缓冲器和有效化队列。第二条是 Java 做为一个跨平台的语言, 该如何解决不同平台之间的差别。那这里的平台是什么意思?咱们在学习 Docker 的时候也看到过平台这个词,咱们来回顾一下 Docker 是如何介绍本人的:

Docker is a platform for developers and sysadmins to build, run, and share applications with containers.

Dokcer 是一个开发者和系统管理员构建、运行、共享容器利用的平台。

那 Java 跨的平台呢?在 Oracle 出的教程《The Java™ Tutorials》是如是说的:

A platform is the hardware or software environment in which a program runs.

平台是程序运行的软件或硬件环境。

We’ve already mentioned some of the most popular platforms like Microsoft Windows, Linux, Solaris OS, and Mac OS. Most platforms can be described as a combination of the operating system and underlying hardware.

咱们曾经提及了一些宽泛应用的平台,像 Windows、Linux、Solaris OS、MacOS。大多数平台能够被形容为操作系统和底层硬件的组合。

The Java platform differs from most other platforms in that it’s a software-only platform that runs on top of other hardware-based platforms.

Java 平台和其余大多数平台的不同之处在于,它是一个纯软件平台,在其余基于硬件的平台之上运行。

当我开始学习 Java,Java 向我承诺:

Your applications are portable across multiple platforms. Write your applications once, and you never need to port them–they will run without modification on multiple operating systems and hardware architectures.

你的利用很容易能够跨平台,编写一次,不须要移植,无需批改,就能运行在多个不同的操作系统和硬件架构上。

当 Java 喊出那句“一次编写,到处运行”的口号时,Java 面临的是一个差异化的世界,不同指令集架构的 CPU,不同的操作系统,不同的处理器架构有着不同的内存模型,咱们这里再来温习一下:

从处理器的角度来说,读内存操作的本质是从指定的 RAM 地址加载数据,因而这种内存操作也被称为 Load,写内存操作的本质是将数据存储到指定地址示意的 RAM 存储单元中,因而内存操作通常被称为 Store。所以,内存重排序实际上只有以下四种可能:

X86 系列的处理器只存在最初一种问题,所以 X86 系列的处理器被称为具备强内存模型的处理器,其余平台的处理器则四种重排序都有可能呈现,然而咱们也不能齐全禁止内存重排序,除非咱们可能承受性能升高。这也就是并发问题的起源之一,有序性,有序性指的是在什么状况下一个处理器运行的一个线程所执行的内存拜访操作在另外一个处理器上运行的其余线程看来是乱序的。所谓乱序,是指内存拜访操作的程序看起来像是产生了变动。咱们再回顾一下 JSR 133 中的话:

Memory caches can improve performance tremendously, but they present a host of new challenges. What, for example, happens when two processors examine the same memory location at the same time? Under what conditions will they see the same value?

缓存能够极大的晋升性能,然而也带来了一系列新挑战,比方当两个解决同时查看同一内存地位会产生什么? 在什么条件下它们会看到雷同的值。

这说的也就是可见性问题,古代计算机的执行单位是线程,下面的话换一句话说也就是一个线程对共享变量的更新在什么状况下对另一个线程可见。可见性是有序性的根底,有序性形容的是一个处理器上运行的线程对共享变量做所的更新,在其余解决上运行的其余线程是以什么样的程序察看到这些更新。在有序性的根底上咱们又心愿如果咱们须要,拜访某个共享变量的操作从其执行线程以外的任何线程来看,该操作要么曾经执行完结,要么尚未产生,即其余线程不会“看到”该操作的两头成果,这也就是原子性。做最终要答复的问题还是,一个线程对共享变量的更新在什么状况下会另一个线程可见。

Java 的想法是在各个不同的处理器内存模型之间建设一个属于 Java 的内存模型:

The Java Memory Model was an ambitious undertaking; it was the first time that a programming language specification attempted to incorporate a memory model which could provide consistent semantics for concurrency across a variety of architectures.

Java 内存模型是一项雄心勃勃的工作,这是第一次有高级语言试图引入一个内存模型为各种架构的并发性提供统一的语义。

Unfortunately, defining a memory model which is both consistent and intuitive proved far more difficult than expected. JSR 133 defines a new memory model for the Java language which fixes the flaws of the earlier memory model. In order to do this, the semantics of final and volatile needed to change.

可怜的是,定义一个统一又直观的内存模型比料想的要艰难的多,JSR 133 为 Java 语言定义了一个新的内存模型,修复了晚期内存模型的缺点。为了做到这一点,final 和 volatile 的语义须要扭转。

《JSR 133》

Java 内存模型的总体目标是:

the goal of JSR 133 was to create a set of formal semantics that provides an intuitive framework for how volatile, synchronized, and final work.

JSR 133 的指标是创立一套形式化语义,为 volatile、synchronized 和 final 关键字的工作形式提供直观的框架。

更具体一点的指标是:

  1. Preserving existing safety guarantees, like type-safety, and strengthening others. For example, variable values may not be created “out of thin air”: each value for a variable observed by some thread must be a value that can reasonably be placed there by some thread.

保留现有的平安保障,如类型平安,并增强其余。例如变量的值不能“凭空产生”:某个线程察看到的每个变量值都必须是某个线程能够正当设置的值。

  1. The semantics of correctly synchronized programs should be as simple and intuitive as possible.

正确的同步语义该当是尽可能的简略而又直观的。

  1. The semantics of incompletely or incorrectly synchronized programs should be defined so that potential security hazards are minimized.

对不齐全或不正确的同步程序的违心进行定义,以便将潜在的安全隐患降到最低。

  1. Programmers should be able to reason confidently about how multithreaded programs interact with memory.

程序员应该可能自信地推理出多线程与内存的互动形式

  1. It should be possible to design correct, high performance JVM implementations across a wide range of popular hardware architectures.

应该能够在宽泛风行的架构上设计出正确、高性能的 JVM 实现。

  1. A new guarantee of initialization safety should be provided. If an object is properly constructed (which means that references to it do not escape during construction), then all threads which see a reference to that object will also see the values for its final fields that were set in the constructor, without the need for synchronization.

应该提供一个新的初始化平安保障,如果一个对象是被正确创立(这意味着对它的援用在结构过程中没有被逃逸),那么所有看到该对象的援用的线程也将看到在构造函数中设置字段的最终值,而不须要同步。

  1. There should be minimal impact on existing code.

最小化对已有代码的影响

简略总结一下就是: 修复旧模型的 bug 的同时保障高性能,设计一套直观的框架让程序员能够直观的推导进去多线程与内存的交互方式。

内存模型定义了什么?

The Java Memory Model describes what behaviors are legal in multithreaded code, and how threads may interact through memory.

内存模型形容了多线程代码中的哪些行为是非法的,线程如何内存进行交互。

Java includes several language constructs, including volatile, final, and synchronized, which are intended to help the programmer describe a program’s concurrency requirements to the compiler.

Java 语言中提供了多个个性、机制,包含 volatile、final 和 synchronized,帮忙程序员向编译器形容并发需要。

The Java Memory Model defines the behavior of volatile and synchronized, and, more importantly, ensures that a correctly synchronized Java program runs correctly on all processor architectures.

Java 内存模型定义了 volatile 和 synchronized 的行为,更重要的是,确保在所有处理器架构上的程序可能被正确同步。

到这里咱们能够看出 Java 内存模型为咱们做的所有,当咱们编写 Java 程序的时候,Java Memory Model 屏蔽掉了各种硬件和操作系统的内存拜访差别,Java 内存模型给出了一组规定或标准,定义了程序中各个变量 (包含示例字段,动态字段和形成数组对象的元素) 的拜访形式,标准了 Java 虚拟机与计算机内存是如何协同工作的,JVM 运行程序的实体是线程,而每个线程创立时 JVM 都会为其创立一个工作内存 (有些文献也称为栈空间),用于存储线程公有的数据,而 Java 内存模型中规定所有变量都必须存储在内存,主内存是共享内存区域,所有线程能够拜访,但线程对遍历的操作(读取赋值等) 必须在工作内存中进行,首先要将变量从主内存拷贝的本人的工作内存空间,而后对遍历进行操作,操作实现后再将变量写会主内存,不能间接操作主内存中的变量,工作内存中存储着主内存中的变量正本拷贝。工作内存是每个线程的公有数据区域,因而不同的线程无法访问对方的工作内存,线程间的通信(传值) 必读通过主内存来实现。

咱们须要留神的是 JMM 只是一组标准,对于真正的计算机硬件来说,计算机内存只有寄存器、缓存内存、主内存的概念。

不论是工作内存的数据还是主内存的数据,对于真正的计算机硬件来说,可能存储在计算机的主内存中,也有可能存储到 CPU 缓存或者寄存器中 来自参考文献 5

我开始的了解是存在某个数据存在寄存器,不在内存,不在 CPU 缓存。存在某个数据在 CPU 缓存,不在寄存器,不在内存。这可能与咱们的个别了解有所违反,个别的了解是数据调配在主内存存储,CPU 须要操作某个变量的时候,该变量会从主内存加载到缓存,而后再到寄存器。然而这句话让我想到了 C 语言,C 语言外面有一个关键字 register,能够申请编译器将数据调配到寄存器存储,所以我揣测,编译器是否也会将变量间接调配到 CPU 缓存中进行存储,然而没有找到相干的材料,咱们还是将其了解为数据从主内存加载,而后加载到 CPU 缓存,而后加载到寄存器中。当然一些贴近机器的语言,也提供了将变量间接调配进寄存器的选项。

工作内存同步到主内存之间的实现细节,JMM 定义了以下八种操作:

如果要把一个变量从主内存中复制到工作内存中,就须要按程序地执行 read 和 load 操作,如果把变量从工作内存中同步到主内存中,就须要按程序地执行 store 和 write 操作。但 Java 内存模型只要求上述操作必须按程序执行,而没有保障必须是间断执行。

  • 同步规定剖析

(1)不容许一个线程无起因地(没有产生过任何 assign 操作)把数据从工作内存同步回主内存中。

(2)一个新的变量只能在主内存中诞生,不容许在工作内存中间接应用一个未被初始化(load 或者 assign)的变量。即就是对一个变量施行 use 和 store 操作之前,必须先自行 assign 和 load 操作。

(3)一个变量在同一时刻只容许一条线程对其进行 lock 操作,但 lock 操作能够被同一线程反复执行屡次,屡次执行 lock 后,只有执行雷同次数的 unlock 操作,变量才会被解锁。lock 和 unlock 必须成对呈现。

(4)如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎应用这个变量之前须要从新执行 load 或 assign 操作初始化变量的值。

(5)如果一个变量当时没有被 lock 操作锁定,则不容许对它执行 unlock 操作;也不容许去 unlock 一个被其余线程锁定的变量。

(6)对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作)。

对于可见性和有序性问题,Java 内存模型则应用 happens-before 这个术语来解答。

happens-before

The new memory model semantics create a partial ordering on memory operations (read field, write field, lock, unlock) and other thread operations (start and join), where some actions are said to happen before other operations.

新的内存模型在内存操作 (读字段、写字段、锁定、解锁) 和其余线程操作 (启动和退出) 上创立了一个局部有序性,其中一些操作被认为先于其余操作之前产生(也就是 happens-before)。

When one action happens before another, the first is guaranteed to be ordered before and visible to the second. The rules of this ordering are as follows:

当一个操作在另一个操作之前后行产生时(能够了解为一个操作 happens-before),第一个操作保障在第二个操作之前被排序和可见。

这里咱们重点了解一下这个 partial ordering 部分有序,存在一些操作是要排在一些操作之前,这也就是 happens-before。那哪些操作存在这样的关系呢, 咱们先简略的看一个规定,程序秩序规定(Program Order rule):

  • Each action in a thread happens before every action in that thread that comes later in the program’s order.

在一个线程内,依照控制流程序,书写在后面的操作后行产生于书写在前面的操作。

这是合乎咱们的直觉的,咱们再读一下下面的话:

When one action happens before another, the first is guaranteed to be ordered before and visible to the second.

当一个操作后行产生于另一个操作,第一个操作保障在第二个操作之前被排序和可见。

咱们能够将这句话了解为,一个线程内指令的执行程序和程序的书写程序是统一的嘛?当然不能够,我的了解是绝对有序,如同是按程序执行的:

public static void main(String[] args){Student student = new Student(); // 语句一
     System.out.println(student); // 语句二
}

语句一 happens-before 语句二,咱们晓得一个对象的产生,要经验三步:

① 首先为 Student 对象分配内存

② 调用 Student 的构造函数来初始化成员变量

③ 将 student 变量 指向调配的内存

JVM 在执行的时候,发现 2 和 3 没有依赖关系,执行程序就可能是①③②,也可能是①②③,那么无论是哪种执行程序,依据程序秩序规定,语句二拿到的对象就如同是①②③执行一样,规定并没有强制要求执行程序,这会侵害性能。再举一个例子, 在这个例子中咱们用可见性来形容有序性,换句话说从可见性形容有序性也就是再说,最初看到的后果,一个处理器最初看到另一个处理器对共享变量的操作产生的后果,如同是程序操作产生的, 程序操作了解起来更为简略,即便编译器和处理器进行了内存重排,假如处理器 0、处理器 1 上两个线程依照下表所示的交织程序执行,X、Y、Z 和 ready 为共享变量,r1,r2,r3 为局部变量。

进一步假如,Process 1 读取到变量 ready 的值时,S1、S2、S3、S4 的操作后果均曾经提交结束,并且 L2、L3 不会与 L1 进行重排序,那么此时 S1、S2、S3 和 S4 的操作后果对 L1 及其当前的 L2 和 L3 都是可见的。因而,从 L1、L2 和 L3 的角度来看此时 S1、S2 和 S3 就如同是被 Process 0 上的线程按照程序程序执行一样,即 S1、S2、S3 和 S4 对于 L1、L2 和 L3 是有序的。只管 Process 0 可能会对 S1、S2、S3 和 S4 时可能进行指令重排,然而只有 Process 1 开始执行 L1 时,S1、S2、S3 和 S4 的操作后果曾经提交结束,即这些操作的后果同时对 L1 可见,那么 S1、S2、S3 和 S4 在 L1、L2 看来就是有序的。

接着咱们来了解第二条规定,管程锁定规定 (Monitor Lock Rule): 一个 unlock 操作后行产生于前面对同一个锁的 lock 操作。读到这里可能会有疑难,怎么先 unlock,再 lock,不应该是先 lock 再 unlock 嘛,就没锁门,你拿个钥匙开个什么劲。之所以有这个疑难,是因为把“后行产生”了解为一种被动的规定要求,而后行产生事实上是程序运行时的主观后果,正确的解读形式是这样的,对于“同一把锁”,如果在程序运行过程中“一个 unlock”操作后行产生于对同一把锁的 lock 操作,那么该 unlock 操作所产生的影响(批改共享变量的值、发送了音讯、调用了办法),对于该 lock 操作是可见的。咱们联合代码来了解这个规定:

// 这个例子来自参考文档 [4]
public class WithoutMonitorLockRule {
    private static boolean stop = false;

    public static void main(String[] args) {Thread updater = new Thread(new Runnable() {
            @Override
            public void run() {
                try {TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {throw new RuntimeException(e);
                }
                stop = true;
                System.out.println("updater set stop true");
            }
        }, "updater");

        Thread getter = new Thread(new Runnable() {
            @Override
            public void run() {while (true) {if (stop) {System.out.println("getter stopped");
                        break;
                    }
                }
            }
        }, "getter");
        getter.start();
        updater.start();}
}

程序输入后果是 updater set stop true,阐明 getter 线程对共享变量的批改对 getter 不可见。咱们在应用锁,来来试试看, 如下所示:

// 这个例子来自参考文档 [4]
public class MonitorLockRuleSynchronized {
    private static boolean stop = false;
    private static final Object lockObject = new Object();

    public static void main(String[] args) {Thread updater = new Thread(new Runnable() {
            @Override
            public void run() {
                try {TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {throw new RuntimeException(e);
                }
                synchronized (lockObject) {
                    stop = true;
                    System.out.println("updater set stop true.");
                }
            }
        }, "updater");

        Thread getter = new Thread(new Runnable() {
            @Override
            public void run() {while (true) {synchronized (lockObject) {if (stop) {System.out.println("getter stopped.");
                            break;
                        }
                    }
                }
            }
        }, "getter");
        updater.start();
        getter.start();}
}

程序输入后果为:updater set stop true. getter stopped.。这印证了管程锁定规定,,对于“同一把锁”,如果在程序运行过程中“一个 unlock”操作后行产生于对同一把锁的 lock 操作,那么该 unlock 操作所产生的影响(批改共享变量的值、发送了音讯、调用了办法),对于该 lock 操作是可见的。残余的规定如下:

  • volatile 变量规定:对于同一个 volatile 变量,如果对于这个变量的写操作后行产生于这个变量的读操作,那么对于这个变量的写操作所产的影响对于这个变量的读操作是可见的。
  • 线程启动规定 :对于同一个 Thread 对象,该 Thread 对象的 start() 办法后行产生于此线程的每一个动作,也就是说对线程 start()办法调用所产生的影响对于该线程的每一个动作都是可见的。
  • 线程终止规定 :对于一个线程,线程中产生的所有操作后行产生于对此线程的终止检测,也就是说线程中的所有操作所产生的影响对于调用线程 Thread.join() 办法或者 Thread.isAlive()办法都是可见的。
  • 线程中断规定 :对于同一个线程,对线程 interrupt() 办法的调用后行产生于该线程检测到中断事件的产生,也就是说线程 interrupt()办法调用所产生的影响对于该线程检测到中断事件是可见的。
  • 对象终结规定 :对于同一个对象,它的构造方法执行完结后行产生于它的 finalize() 办法的开始,也就是说一个对象的构造方法完结所产生的影响,对于它的 finalize()办法开始执行是可见的。
  • 传递性:如果操作 A 后行产生于操作 B,操作 B 后行产生于操作 C,则操作 A 后行产生于操作 C,也就说操作 A 所产生的所有影响对于操作 C 是可见的。

这里讲一下 volatile,这是 Java 内置的关键字,三年前咱们就在《明天咱们来聊聊单例模式和枚举》遇见了,在单例模式那里,咱们应用 volatile 来禁止指令重排序,这里咱们又了解了它的一个作用,能够用于润饰共享变量,在以前我不了解,volatile 的作用,当一个变量被 volatile 润饰,B 线程读取了这个共享变量,加载到本人的工作内存 , 刚加载完,而后工夫片耗尽,A 线程更新了共享变量,B 线程还怎么读到 A 线程更新的值,这里就要探索 volatile 的实现了,本篇不做摸索,简略的说就是,当 volatile 变量被批改时,其余 CPU 的缓存行会生效,CPU 会一直对总线进行内存嗅探,如果滥用 volatile 可能会引起总线风暴(我在想该如何验证这个说法),除此之外,大量的 CAS 操作也会引发这个问题。咱们不必锁,应用 volatile 润饰 stop,也能输入 updater set stop true. getter stopped。

public class VolatileRule {
    private volatile static boolean stop = false;

    public static void main(String[] args) {Thread updater = new Thread(new Runnable() {
            @Override
            public void run() {
                try {TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {throw new RuntimeException(e);
                }
                stop = true;
                System.out.println("updater set stop true");
            }
        }, "updater");

        Thread getter = new Thread(new Runnable() {
            @Override
            public void run() {while (true) {if (stop) {System.out.println("getter stopped");
                        break;
                    }
                }
            }
        }, "getter");
        getter.start();
        updater.start();}
}

volatile 变量还有一个说是特点,其实也不是的个性。如下代码并未将 stop 变量用 volatile 润饰,而是用 volatile 润饰了 volatileObject 变量,如下所示:

public class VolatileRule1 {
    
    private static  boolean stop = false;
    
    private static volatile Object volatileObject = new Object();

    public static void main(String[] args) {Thread updater = new Thread(new Runnable() {
            @Override
            public void run() {
                try {TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {throw new RuntimeException(e);
                }
                stop = true;
                volatileObject = new Object();
                System.out.println("updater set stop true.");
            }
        }, "updater");

        Thread getter = new Thread(new Runnable() {
            @Override
            public void run() {while (true) {
                    Object volatileObject = VolatileRule1.volatileObject;
                    if (stop) {System.out.println("getter stopped.");
                        break;
                    }
                }
            }
        }, "getter");
        updater.start();
        getter.start();}
}

也能失去:updater set stop true. getter stopped.。这个后果。这看起来颇为神奇,咱们联合传递性和程序程序规定进行剖析,在 updater 线程内,依据程序程序规定,stop = true 先于 volatileObject = new Object(),而在 getter 线程内,对 volatileObject 的读取先于读取 stop 变量,所以 stop = true 后行产生于 if(stop) , 所以 stop = true 对于 if(stop) 就是可见的。

看到这里可能有同学就会问了,这不就是传递性和程序程序规定嘛,怎么能算上 volatile 的特点呢?这里咱们再来读一下 volatile 规定:

对于同一个 volatile 变量,如果对于这个变量的写操作后行产生于这个变量的读操作,那么对于这个变量的写操作所产的影响对于这个变量的读操作是可见的。

如果 volatileObject 上没有 volatile,这个传递性就传不过去,咱们再捋一下,在 updater 线程内,依据程序程序规定,对 stop 变量的更新后行产生于对 volatileObject 的更新,而在 getter 线程内对 volatileObject 的读取先于对 stop 的读取, 那么在依据 volatile 规定,对 volatile 变量的写操作后行产生于这个变量的读操作:

如果 valueObject 没被 volatile 润饰,就传不过去。利用此个性的一个典型例子在 jdk 的 FutureTask,上面的代码来自 FutureTask:

private volatile int state;
 // non-volatile, protected by state reads/writes 
没有被 volatile 润饰,被 state 变量读写爱护
private Object outcome;

其余语言有内存模型嘛?

Most other programming languages, such as C and C++, were not designed with direct support for multithreading.

像其余语言,比方 C 和 C ++,在设计时不被间接反对多线程。

The protections that these languages offer against the kinds of reorderings that take place in compilers and architectures are heavily dependent on the guarantees provided by the threading libraries used (such as pthreads), the compiler used, and the platform on which the code is run.

这些语言对产生在编译器和架构中的重排的爱护很大水平取决于所应用得线程库(如 pthread)、所应用的编译器以及代码运行的平台所提供保障。《JSR 133 (Java Memory Model) FAQ》

总结一下其余语言没有承诺跨平台,语言原生就没有反对多线程,所以也不须要建设一个本人的内存模型来屏蔽零碎和架构差别。

总结一下

总结 Java memory model 是什么?咱们回顾一下《JMM 学习笔记(一) 跨平台的 JMM》中咱们曾经谈到了什么是模型?

模型是指对于某个理论问题或客观事物、法则进行形象后的一种形式化表达方式。

而计算机在倒退过程中,引入多核整体晋升 CPU 性能,引入过程增大 CPU 的利用率,引入线程更好的共享变量,而引入线程操作共享变量再碰上多外围,就会碰到上面三个问题:

  • 可见性: 一个线程对共享变量的更新在什么状况下对另一个线程可见。可见性是有序性的根底
  • 有序性: 形容的是一个处理器上运行的线程对共享变量做所的更新,在其余解决上运行的其余线程是以什么样的程序察看到这些更新。
  • 原子性: 拜访某个共享变量的操作从其执行线程以外的任何线程来看,该操作要么曾经执行完结,要么尚未产生,即其余线程不会“看到”该操作的两头成果

Java 面对的是不拘一格的硬件,有的 CPU 是强内存模型,有的 CPU 是弱内存模型,而 Java 身上的特点就是跨平台,该怎么做到跨平台,Java 的抉择是建设本人的内存模型,在强和弱之间,抉择了本人的模型,给出一组规定答复可见性这个并发的最基本问题,依据这些规定咱们能够揣测出在各个平台上的行为,以这种形式做到跨平台。就像是开篇写的,在月球上物体会着落,在地球上物体也会着落,然而着落的速度不同,这是物理法则的平均性。而 JMM 的规定在各个平台上也具备这种平均性。

写在最初

JDK 9 开始对 JMM 进行调整,但不要放心,没有做破坏性更新,仍旧兼容从前,我本来想在这一篇一口气把这些都介绍,然而发现文章篇幅曾经很大了,我总是雄心勃勃想在一篇文章将所有都将分明,然而想做的货色太多,有的时候我可能潜意识外面也畏惧这么多内容,有的时候甚至畏惧开始写这方面的内容。于是我将 JDK 9 之上,JMM 做的调整挪动到下一篇。以前我不懂 JMM 提供的 happens-before 规定,但也不影响我应用并发编程,起因在于 JMM 的 happens-before 规定也是贴合直觉的。操作系统的课程在 B 站看了两个《【哈工大】操作系统 李治军(全 32 讲)》、《清华 操作系统原理》,李治军老师讲过程的时候,是从晋升 CPU 的利用率来说的,《清华 操作系统原理》是从零碎要运行两个雷同的程序来说的,感觉引出过程这个概念来说,李治军老师讲的更好。在写这篇文章的时候,也始终在回溯本人对于对操作系统的了解,讲多线程、多过程,不仅仅从语言自身来说,总体站在操作系统的角度来说,很多问题不能仅仅从语言自身来说,思路容易狭隘。

参考资料

[1] 清华 操作系统原理 https://www.bilibili.com/video/BV1uW411f72n?p=8&vd_source=aae…

[2] SR 133 (Java Memory Model) http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html

[3]【哈工大】操作系统 李治军(全 32 讲)https://www.bilibili.com/video/BV19r4y1b7Aw?p=8&vd_source=aae…

[4] Happens-Before 准则深刻解读 https://juejin.cn/post/7124504859247804424

[5] Java 并发编程之 JMM & volatile 详解 https://juejin.cn/post/6916331359258542087#heading-7

正文完
 0