Java并发编程之Java内存模型 (下)

前言

看完《Java并发编程的艺术》整本书之后,再次回顾并发编程中的Java内存模型,有了一写自己见解,这里接着前两次的文章继续做一个总结。

final域的内存语义

关于Java的final修饰符的一些基础知识可以参考我的这篇文章Java final关键字小结。今天要介绍的就是final在并发编程中,Java内存模型如何保证final的线程同步。

1. final域的重排序规则

  1. 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  2. 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

第一句话是保证了通过类的实例获取到的final域的值,一定会是写入后的有效值。不过由于final域只会被写入一次,所以不需要担心后续再会有对final域的写操作。
第二句话中的两个操作存在依赖关系,符合as-if-serial语义,所以这两个操作不会被重排序。

public class FinalExample {
    int                 i;  //普通变量
    final int           j;  //final变量
    static FinalExample obj;

    public FinalExample() { //构造函数
        i = 1; //写普通域
        j = 2; //写final域
    }

    public static void writer() { //写线程A执行
        obj = new FinalExample();
    }

    public static void reader() { //读线程B执行
        FinalExample object = obj; //读对象引用
        int a = object.i; //读普通域
        int b = object.j; //读final域
    }

这段代码是《Java并发编程的艺术》第三章的原代码,由于final对重排序的限制,就保证了b读取的值一定会是2;但是a读取到的值有可能为1也有可能为0。

2. final引用“溢出”

如果在构造函数内部,被构造对象的引用为其他线程可见,那么对象引用就会存在“逃逸”现象。此时final域就不能保证线程同步了。

public class FinalReferenceEscapeExample {

    final int                          i;
    static FinalReferenceEscapeExample obj;

    public FinalReferenceEscapeExample() {
        i = 1; //1写final域
        obj = this; //2 this引用在此“逸出”
    }

    public static void writer() {
        new FinalReferenceEscapeExample();
    }

    public static void reader() {
        if (obj != null) { //3
            int temp = obj.i; //4
        }
    }

上面代码的操作1和操作2存在重排序,obj先获得对象引用,再对i进行初始化。那么对于操作3条件为真时i未被初始化,然后操作4读取到i未被初始化时的值。

happens-before

对于编译器和处理器来说,它们希望内存模型的约束越少越好,这样就能做更多的性能优化操作。最常见的就是重排序。而程序员则希望程序能得到一个预期的结果,那么就需要一个强内存模型来支持。

1. happens-before的概念

这个是我个人理解:如果操作B的执行依赖与A操作的结果,那么就说A happens-beforeB。
最常见的例子就是:int a = 1;//1 int b = a + 2; //2这两个操作必须保证1先于2执行,不然程序会得到异常结果。
一般来说,我们认为存在依赖关系的操作都应该有happens-before关系,但是在多线程编程里,本来应该具有happens-before关系的操作往往都不存在了。所以我们需要一些技术告知Java内存模型去对一些操作加上happens-before操作。比如前面提到的final、volatile、锁等技术。
事实上,Java内存模型只会禁止会改变程序执行结果的重排序,而对于那些不会改变程序执行结果的重排序,JMM不做禁止。

JMM其实是在遵循一个基本原则:只要不改变程序的执行结果,编译器和处理器怎么优化都行。

2. happens-before定义和规则

  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
  2. 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。

上面两句话有一个让人疑惑的地方就是,第一句话是如果Ahappens-beforeB,那么A就必须先于B执行,而第二句话的意思就是A可能在Java平台上不先于B执行。我目前的理解是这样的。
int a;
a = 0;//A
int b = a + 1;//B
上面两个操作A happens-beforeB,由于B和A重排序不影响程序的最终执行结果,所以Java内存模型允许A和B的重排序。因为a在未写入值之前默认值为0,写入的值也为0,所以B操作得到的结果都为1。
规则:

  1. 程序顺序规则
  2. 监视器锁规则
  3. volatile变量规则
  4. 传递性
  5. start规则
  6. join规则
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值