04. 共享模型之内存

共享模型之内存

Java内存模型(Java Memory Model,JMM),它定义了主存(共享数据)和工作内存(私有数据)的抽象概念,底层对应着CPU寄存器、缓存、硬件内容和CPU指令优化等。

JMM体现在以下几个方面:

  • 原子性,保证指令不会受到线程上下文切换的影响
  • 可见性,保证指令不会受CPU缓存的影响
  • 有序性,保证指令不会受CPU指令并行优化的影响

JMM是JVM的一种规范,定义了JVM的内存模型,屏蔽了各种硬件和操作系统的访问差异。

1. 可见性

一个线程对主存中的变量修改,另一个线程不可见。

解决方法:

  • 对变量添加volatile关键字,使得线程去主存中读取而不是缓存。它可以修饰成员变量和静态成员变量。
  • volatile只保证可见性,不保证原子性。
  • 使用synchronized也可以解决。解锁前会将本地的修改刷新到主存中,确保了共享变量的值是最新的。
  • synchronized语句块可以保证代码块的原子性,也同时保证代码块内变量的可见性,但缺点是它属于重量级锁,性能相对较低。
  • 另外,在线程中使用System.out.print()也会保证变量的可见性,是因为这个方法会刷新缓存,同时,在方法中有使用synchronized锁。

1.1 同步模式之Balking

用在一个线程发信啊另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回。

注意区分volatile和synchronized的区别
单例初始化可以使用Balking

2. 有序性

JVM会再不影响正确性的前提下,可以调整语句的执行顺序,这种特性称之为 指令重排。多线程下的指令重排会影响正确性。

为什么要有指令重排这项优呢?

在不改变程序结果的前提下,指令的各个阶段可以痛殴该国重排序和组合来实现指令级并行。它虽然不能缩短单个指令执行时间,但可以提高指令吞吐量。

JVM也有指令重排,它是JIT编译器在运行时的一些优化。

JIT,just-in-time compilation。java程序最初是通过解释器进行解释执行的,当JVM发现某个方法或代码块运行特别频繁时,就会把这些代码认定为 热点代码。为了提高热点代码的执行效率,在运行时,JVM会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化。
并不是所有的代码都会被JIT编译,否则会发生 代码爆炸。对一般的Java方法而言,编译后的代码大小相对于字节码大小,膨胀比达到10x是很正常的。

使用Java并发压测工具,jcstress,观察指令重排序现象

创建jcstress测试项目,

mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jcstress -DarchetypeArtifactId=jcstress-java-test-archetype -DarchetypeVersion=0.5 -DgroupId=cn.itcast -DartifactId=ordering -Dversion=1.0
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {

    int num = 0;
    boolean ready = false;

    @Actor
    public void actor1(I_Result r) {
        // Put the code for first thread here
        if (ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }
    @Actor
    public void actor2(I_Result r) {
        // Put the code for second thread here
        num = 2;
        ready = true;
    }
}

结果

Observed state   Occurrences              Expectation  Interpretation
            0         4,143   ACCEPTABLE_INTERESTING  !!!!
            1    38,574,093               ACCEPTABLE  ok
            4    68,104,735               ACCEPTABLE  ok 

出现结果0,是因为num = 2ready = true两个语句发生重排序,后者放在了前面。

给变量ready添加volatile关键字可以防止结果0的出现

对含有volatile关键字变量操作的前面的语句,不会发生重排序。

3. volatile原理

volatile的底层实现原理时内存屏障,Memory Barrier(Memory Fence)。

  • 对volatile变量的写指令的 后面 会加入写屏障
  • 对volatile变量的度指令的 前面 会加入读屏障

3.1 如何保证可见性

写屏障保证在该屏障之前的,对共享变量的改动,都同步到主存当中。

public void actor2(I_Result r) {
    // Put the code for second thread here
    num = 2; // num也会被同步到主存当中
    ready = true; // ready是volatile赋值带写屏障
    // 写屏障
}

读屏障保证在该屏障之后,对共享变量的读取,加载的是主存当中最新的数据。

@Actor
public void actor1(I_Result r) {
    // Put the code for first thread here
    // 读屏障
    if (ready) {
        r.r1 = num + num;
    } else {
        r.r1 = 1;
    }
}

3.2 如何保证有序性

写屏障会保证在指令重排序时,不会将写屏障之前的代码排在写屏障之后;

读屏障会保证在指令重排序时,不会将读屏障之后的代码排在读屏障之前。

3.3 double-checked locking 问题

double-checked locking单例模式:

public class Singleton {
    private static Singleton INSTANCE = null;
    private Singleton() { }
    public static Singleton geteInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

其特点是:

  • 懒惰化实现
  • 首次使用getInstance()才使用synchronized加锁,后续使用无需加锁
  • 第一个if使用了INSTANCE变量,是在同步块之外

在多线程环境下,上面代码是有问题的。

new对象时,可以有三步(简化):

  1. 分配内存;
  2. 初始化;
  3. 内存地址复制给变量INSTANCE。

步骤2和步骤3可能会发生重排序,多个线程操作时,可能会将未初始化的单例返回。

synchronized的有序性并不能防止指令重排的发生,而violate可以。

在INSTANCE变量上加入violate关键字,确保new对象中的步骤1、2和3有序进行,解决该问题。

4. happens-before 规则

happens-before规定了对共享变量的写操作对其他线程读操作可见。它是可见性与有序性的一套规则总结,抛开以下happens-before规则,JMM并不能保证一个线程对共享变量的写,其他线程对该共享变量的读可见。

  • 对象锁解锁前对变量的写,对于接下来同一个对象锁的其他线程对该变量的读可见
  • 线程对volatile变量的写,对接下来其他线程对该变量的读可见
  • 线程start前对变量的写,在该线程开始后对该变量的读可见
  • 线程结束前对变量的写,其他线程在得知它结束后的读可见
  • 线程t1打断t2前对变量的写,对于其他线程在得知t2被打断后的读可见
  • 对变量默认值的写,其他线程对该变量可见
  • 具有传递性,如果x hb-> y,y hb-> z,那么有x hb-> z,配合volatile防止指令重排
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容