乐趣区

关于android:Java并发编程Java内存模型JMM

Java 并发编程 -Java 内存模型(JMM)

前言

在上一章 Java 并发编程 -Android 的 UI 框架为什么是单线程的?中笔者介绍了并发编程线程平安「三大恶」:「可见性」、「原子性」以及「有序性」

狭义上来说,并发编程问题笔者演绎为:是因为后续操作看不到后面操作的后果而引发的

首先「大恶 - 可见性」顾名思义,即线程 B 是否看见线程 A 对共享变量的操作后果

其次「二恶 - 原子性」CPU 指令 / 操作被中断 / 宰割,狭义上看笔者认为也是可见性问题,比方线程 A 批改共享变量 x += 1 时被中断,去执行线程 B x = 40,线程 A 复原执行时可能看不到 x 曾经被批改为 40

最初「三恶 - 有序性」指的是程序依照代码的先后顺序执行,即先执行 x = 40,后续读取 x == 40 肯定为真,如果有各种优化导致指令重排序,后续读取 x == 40 时可能不为真

总而言之,并发编程的问题大部分都是「可见性」问题

Java 内存模型(JMM)

在上一章中笔者介绍了导致「可见性」的起因是缓存,导致「有序性」的起因是各种优化(编译时、处理器以及运行时),首先笔者想到的是禁用缓存和禁止各种优化操作,在 Java 中如何禁用缓存和禁止各种优化呢?

Happens-Before

Java 内存模型是通过各种操作来定义的1,包含对变量的读 / 写操作,监视器锁的加锁和开释操作,以及线程的启动和合并操作。JMM 为程序中所有的操作定义了一个偏序关系2,称之为 Happens-Before。要想保障执行操作 B 的线程看到操作 A 的后果(无论 A 和 B 是否在同一个线程中执行),那么在 A 和 B 之间必须满足 Happens-Before 关系。如果两个操作之间不足 Happens-Before 关系,那么 JVM 能够对它们任意的重排序。

Happens-Before 的规定包含:

规定一:程序程序

这条规定比拟合乎咱们的直觉,在单线程中代码的程序即程序的执行程序:int x = 40 将在 int y = 41 之前执行

public void test() {
    int x = 40;
    int y = 41;
}
规定二:监视器锁3

监视器锁即内置锁(synchronized),在监视器锁上的解锁操作必须在同一个监视器锁的加锁操作之前执行,这条规定比拟好了解,事实世界中的锁在关锁前必须处于关上状态

规定三:volatile 变量45

对 volatile 变量的写入操作必须在对该变量的读取操作之前执行。

这条规定不大好了解,以下代码与文字摘自 深刻了解 Java 内存模型(四)——volatile

了解 volatile 个性的一个好办法是:把对 volatile 变量的单个读 / 写,看成是应用同一个监视器锁对这些单个读 / 写操作做了同步6

class VolatileFeaturesExample {
    volatile long vl = 0L;  // 应用 volatile 申明 64 位的 long 型变量

    public void set(long l) {vl = l;   // 单个 volatile 变量的写}

    public void getAndIncrement () {vl++;    // 复合(多个)volatile 变量的读 / 写}

    public long get() {return vl;   // 单个 volatile 变量的读}
}

假如有多个线程别离调用下面程序的三个办法,这个程序在语意上和上面程序等价:

class VolatileFeaturesExample {
    long vl = 0L;               // 64 位的 long 型一般变量

    public synchronized void set(long l) {     // 对单个的一般 变量的写用同一个监视器同步
        vl = l;
    }

    public void getAndIncrement () { // 一般办法调用
        long temp = get();           // 调用已同步的读办法
        temp += 1L;                  // 一般写操作
        set(temp);                   // 调用已同步的写办法
    }
    
    public synchronized long get() {return vl; // 对单个的一般变量的读用同一个监视器同步}
}

如下面示例程序所示,对一个 volatile 变量的单个读 / 写操作,与对一个一般变量的读 / 写操作应用同一个监视器锁来同步,它们之间的执行成果雷同。

从内存语义的角度来说,volatile 与监视器锁有雷同的成果:volatile 写和监视器的开释有雷同的内存语义;volatile 读与监视器的获取有雷同的内存语义。

简而言之,volatile 变量本身具备下列个性:

  • 可见性。对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最初的写入。
  • 原子性:对任意单个 volatile 变量的读 / 写具备原子性,但相似于 volatile++ 这种复合操作不具备原子性。
规定四:线程启动

在线程上对 Thread.start 的调用必须在该线程中执行任何操作之前执行。

这条规定比拟好了解,即线程 A 启动操作先执行,而后再执行线程 A 内的操作,换言之,线程 A 启动前的操作后果对线程 A 中的操作可见,参考上面的示例代码:

Thread thread = new Thread(() -> {
    // 线程中的操作, 能够看到线程启动前对共享变量的操作后果
    System.out.println(x);
});

// 线程启动前操作共享变量
x = 10;

// 线程启动
thread.start();
规定五:线程完结

线程中的任何操作都必须在其余线程检测到该线程曾经完结之前执行,或者从 Thread.join 中胜利返回,或者在调用 Thread.isAlive 时返回 false。

这条规定也比拟好了解,即线程完结前,线程内的任何操作都曾经执行,换言之,线程完结后,线程内的操作后果对后续操作可见,参考上面的示例代码:

Thread thread = new Thread(() -> {
    // 线程中的操作, 能够看到线程启动前对共享变量的操作后果
    System.out.println(x);

    // 线程中操作共享变量
    x = 20;
});

// 线程启动前操作共享变量
x = 10;

// 线程启动
thread.start();

// 线程 join, 期待线程完结
thread.join();

// 线程中对共享变量的操作后果此处可见
System.out.println(x);
规定六:线程中断

当一个线程在另一个线程上调用 interrupt 时,必须在被中断线程检测到 interrupt 调用之前执行(通过抛出 InterruptedException,或者调用 isInterrupted 和 interrupted)。

这条规定笔者不太了解,只是了解了外表意思即:线程 A 对线程 B interrupt 的调用,必须在线程 B 检测到中断事件之前执行。

规定七:终结器

对象的构造函数必须在启动该对象的终结器 7 之前实现。

这条规定好了解,能够了解为 Android Activity 的生命周期,这里说的构造函数与终结器就是对象的生命周期,调用终结器之前,对象必须曾经初始化。

规定八:传递性

如果操作 A 在操作 B 之前执行,并且操作 B 在操作 C 之前执行,那么操作 A 必须在 操作 C 之前执行。

看到这条规定,笔者居然首先想到了《秦时明月》中《白马非马》答辩:白马 == 传家宝,黑马 == 传家宝,传家宝 == 传家宝,白马 == 黑马;扯远了。

这条规定能够了解为大小关系,比方:a 大于 b,b 大于 c,那么 a 肯定大于 c。

传递性规定能够与其余规定联合起来应用,达到意想不到的成果。

小结1

上述规定尽管只满足偏序关系,但同步操作,如锁的获取与开释等操作,以及 volatile 变量的读取与写入操作,都满足全序关系。因而,在形容 Happens-Before 关系时,能够应用“后续的锁获取操作”和“后续的 volatile 变量的读取操作”等表白术语。

final 域

在 Java 中,不论是在写类、写变量还是写办法时,大部分程序员可能都会漠视 final 关键字 的作用,从而导致一些不可预感的问题,在 Kotlin 中类和办法默认是 final 的,申明变量时倡议应用 val 润饰而不是 var。

在 Java 中基于 final 关键字润饰的变量在多线程中也有不可变的个性,倡议尽量优先思考应用 final 来润饰变量。

总结

Java 内存模型就是对 JVM 的一种标准,其中 Happens-Before 定义了与程序员相干的一些规定,能够了解为 Happens-Before 规定是底层对程序员裸露的一些接口,程序员正确的应用这些接口能够编写出正确的代码。

Happens-Before 规定能够说是强制规定了代码的一些执行程序,同时表白出 后续操作能够看见后面一个操作的后果

对 Java 内存模型的学习,有助于剖析解决遇到的线程平安问题,了解线程平安问题呈现的起因,以及重塑本人的编程思维。

笔者认为最重要的是重塑本人的编程思维,俗话说“读书破万卷, 下笔如有神”,了解原理,把握规定,重塑思维后,能力在编码时写出 “Bug”(狗头保命)。

Java 并发编程属实难,笔者程度无限,如有谬误之处,还请不吝赐教。

阐明与参考文献


  1. 《Java 并发编程实战 - 第十六章》↩
  2. 偏序关系 π 是汇合上的一种关系,具备拥护称、自反和传递属性 ↩
  3. 显示锁与内置锁在加锁和解锁等操作上有雷同的内存语义 ↩
  4. 原子变量与 volatile 变量在读 / 写操作上有雷同的语义 ↩
  5. Java 内存模型 FAQ(十)volatile 是干什么用的 ↩
  6. 深刻了解 Java 内存模型(四)——volatile ↩
  7. 即 finalize() 办法 ↩
退出移动版