Java并发编程实战(2)- Java内存模型

本文主要描述了在Java并发编程中非常重要的Java内存模型以及Happens-Before规则。

  • 概述

  • 什么是Java内存模型

    • Java内存模型和JVM的区别

    • volatile关键字

  • Happens-Before规则

    • 顺序性规则

    • volatile变量规则

    • 传递性

    • synchronized规则

    • 线程start()规则

    • 线程join()规则

    • final规则

概述

对于Java并发程序问题存在的各种问题,主要有3个根源:

  • 缓存引发的可见性问题

  • 由线程切换引发的原子性问题

  • 由编译优化引发的有序性问题

为了解决可见性和有序性的问题,Java引入了Java内存模型。

既然可见性问题和有序性问题由缓存和编译优化造成的, 那么最直接的方法就是禁用缓存和编译优化,这样做是可以解决问题的,但是程序的性能会下降到不能接受的程度。

合理的方案是按需禁用缓存和编译优化, 所谓“按需禁用”,就是指按照程序员的要求来禁用,来为程序员开放相应的方法。

什么是Java内存模型

Java内存模型是一个很复杂的规范,可以从不同的角度进行解读,站在程序员的角度,可以将其解决为它规范了JVM如何提供按需禁用缓存和编译优化的方法。

Java内存模型对应的规范是JSR-133,链接:http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf。

Java内存模型和JVM的区别

  • Java内存模型定义了一套规范,它能让JVM按需禁用CPU缓存和编译优化,这套规范包括volatile、synchronized、final三个关键字和7个Happen-Before规则。

  • JVM内存模型是指程序计数器、JVM方法栈、本地方法栈、堆、方法区这5部分。

volatile关键字

volatile关键字的用途是禁用CPU缓存。

例如我们定义一个volatile变量volatile int x = 0;,它表达的是:编译器在对这个变量进行读写操作时,不能使用CPU缓存,而是从内存中直接操作。

我们来看下面的代码示例。


public class VolatileDemo {

int x = 0;
volatile boolean v = false;

public void write() {
x = 42;
v = true;
}

public void read() {
if (v == true) {
System.out.println(String.format("x is %s", x));
}
}
}

如果对同一个VolatileDemo对象,有2个线程,一个调用write()方法,一个调用read()方法,那么当read()方法中v等于true时,x的值是多少?

在Java 1.5版本之前,x的值可能是0或者42, 在Java 1.5版本之后,x的值只能是42。

这是由于Happens-Before规则导致的。

Happens-Before规则

什么是Happens-Before规则?

Happens-Before规则表达的是前面一个操作的结果对后续操作是可见的。它约束了编译器的优化行为,保证其一定要遵守Happens-Before规则。

Happens-Before的语义本质是一种可见性,A Happens-Before B意味着A事件对B事件来说是可见的,无论A事件和B事件是否发生在同一个线程中。

Happens-Before规则有很多条,我们主要关注和程序员相关的几条。

顺序性规则

在一个线程中,按照程序顺序,前面的操作Happens-Before后续的任意操作。

这条规则比较直观,符合单线程里面的思维:程序前面对某个变量的修改一定是对后续操作可见的。

volatile变量规则

对一个volatile变量的写操作,Happens-Before于后续对这个volatile变量的读操作。

传递性

如果A Happens-Before B,且B Happens-Before C,那么A Happens-Before C。

我们再看上面的示例代码:

  • x=42 Happens-Before 写变量 v=true,这是规则1。

  • 写变量 v=true Happens-Before 读变量 v=true,这是规则2。

然后根据传递性规则,我们可以得出x=42 Happens-Before 读变量 v=true。所以在示例代码中,在判断v==true时,x的值等于42。

synchronized规则

对一个锁的解锁要Happens-Before于后续对这个锁的加锁。

我们要首先了解什么是“管程”,管程是操作系统中的一个重要概念,一个管程是一个由过程、变量及数据结构等组成的一个集合,它由四个部分组成:1)管程名称,2)共享数据的说明,3)对数据进行操作的一组过程,4)对共享数据赋初值的语句。

在Java中,管程是通过synchronized关键字实现的。

我们对这个规则可以理解为:假设x的初始值是10,线程A获取锁,执行完代码,x的值会变为12,之后释放锁,接下来线程B获取锁,这时线程B看到的x,一定是12,不应该是10。

线程start()规则

主线程A启动子线程B,子线程B能够看到主线程在启动子线程B之前的操作。

我们来看下面的示例。

public class HappensBeforeDemo {

private int x = 10;

public void threadStartTest() {
Thread t = new Thread(() -> {
System.out.println(String.format("x is %s.",x));
}
);

x = 20;

t.start();
}


public static void main(String[] args) {
HappensBeforeDemo demoObj = new HappensBeforeDemo();
demoObj.threadStartTest();

}
}

程序的输出结果如下。

x is 20.

线程join()规则

主线程A通过调用子线程的join()方法等待子线程结束,当子线程结束后,主线程能够看到子线程对共享变量的操作。

这个规则和线程start()规则类似,我们来看下面的示例代码。

public class HappensBeforeDemo {

private int x = 10;


public void threadJoinTest() throws InterruptedException {
Thread t = new Thread(() -> {
try {
java.lang.Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
x = 30;
}) ;

t.start();
t.join();
System.out.println(String.format("x is %s.",x));

}

public static void main(String[] args) throws InterruptedException {
HappensBeforeDemo demoObj = new HappensBeforeDemo();
demoObj.threadJoinTest();

}
}

程序的输出结果如下。

x is 30.

final关键字

我们用final修饰变量时,就是告诉编译器,这个变量生而不变,可以尽情优化。

但是如果我们将变量设置成final,它的构造函数由于编译优化后的错误重排,还是可能会导致错误,例如我们之前谈到的单例模式的代码。

在Java 1.5之后,Java内存模型对final类型变量的重排进行了约束,只要我们提供的构造函数没有“逸出”,那么就不会有问题。

所谓“逸出”,就是指构造函数中使用了生命周期超过了该对象生命周期的变量。

参考资料

  • https://time.geekbang.org/column/article/84017

  • http://www.cs.umd.edu/~pugh/java/memoryModel/

  • http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf

  • https://docs.oracle.com/javase/specs/jls/se15/html/jls-17.html

  • https://blog.csdn.net/javazejian/article/details/72772461


原文始发于微信公众号(技术修行者):Java并发编程实战(2)- Java内存模型

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/21324.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!