Java中final修饰符的初始化安全性的理解

今天看《Java并发编程实战》看到安全发布的问题中final修饰符的作用,一时半会没有看明白,查了一些资料才懂了一些深层次的原因,所以做一些记录。

首先我们来看一下书中的例子和描述

//不安全的发布
public Holder holder;

public void initialize() {
    holder = new Holder(42);
}

这段是一个不安全的对象发布,书上的描述是“由于存在可见性问题,其他线程看到的Holder对象将处于不一致的状态,即便在该对象的构造函数中已经正确地构建了不变性条件。这种不正确的发布导致其他线程看到尚未创建完成的对象。”这段我读到的时候的理解就是holder由于是完全可见的,那有可能没有执行构造函数就被使用了,自然会出错嘛…可是越往后读越证明,我想的太天真了。

随后书中举了一个简单的例子证明上面这段代码是不安全的发布

public class Holder {
  private int n;

  public Holder(int n) { this.n = n; }

  public void assertSanity() {
    if (n != n)
      throw new AssertionError("This statement is false.");
  }
}
书上是这么描述问题的,另一个线程调用assertSanity时有可能抛出异常,看到这里我觉得哎呀我想对了,你的holder是个空引用之类的不可预知状态,当然会报错了!可是当看到书中的解释,我就懵逼了…

“由于没有使用同步来确保Holder对象对其他线程可见,因此将Holder称为“未被正确发布”。在未被正确发布的对象中存在两个问题。
首先 ,除了发布对象的线程外,其他线程可以看到的 Holder域是一个失效值 ,因此将看到一个空引用或者之前的旧值。
然而 ,更糟糕的情况是,线程看到Holder引用的值是最新的,但Holder状态的值却是失效的。情况变得更加不可预测的是,某个线程在第一次读取域时得到失效值,而再次读取这个域时会得到一个更新值,这也是assertSainty抛出AssertionError的原因。”

上面说了两个问题,第一种就是我们设想的最简单的情况(还是有悟性的哈哈),可当我看到第二种,我就懵逼了,特别是这句”线程看到Holder引用的值是最新的,但Holder状态的值却是失效的“,我的引用都有效了,状态为何是失效的呢?(觉得简单的大牛不要嘲笑我,一开始看到这里真的懵)

看不懂归看不懂,抱着往下看看我说不定就明白了的心态我硬着头皮往下看,可谁知,越往下看越懵逼,书中说了一种解决问题的方式:“如果将n声明为final类型,那么Holder将不可变,那么即使Holder没有被正确的发布,在assertSanity中也不会抛出异常”,这……本来就不知道为何会又第二种异常,这个解决方法就更不明白了,这就加了个小小的final,就不会有问题了,这简直是一环套一环的懵逼。

好了我的疑惑已经说的很明白了:

第一、我不知道为什么在holder引用最新的情况下会报出异常

第二、我不知道为什么加了final修饰n之后就不会出现问题

下面我就开始解惑了

第一、为什么在holder引用最新的情况下会报出异常

我们来看一下我们认为的过程,
    holder = new Holder(42);
我们认为holder是最新的,就意味着holder指向了我们所创建的新的对象,也就是n为42的对象对吧,既然引用已经指向了,那我调用holder的函数难道还会有问题嘛?这里这个问题在我前一篇讲单例模式在并发中的实现的博客中已经提到了,我们认为的执行过程是怎么样的呢?
(1)给Holder对象分配内存
(2)调用Holder的构造函数,也就是给n赋值的过程,初始化了成员字段
(3)将holder引用指向我们分配的内存空间

这就是这句代码做的基本步骤,我们认为既然第3步已经执行了(holder引用已经指向最新了),那1、2步肯定已经完成了,可是,在JVM自身的性能优化中,是允许这个顺序乱序执行的,也就是说,它不能保证执行的顺序是1、2、3,也有可能是1、3、2等等很多种情况,这就是问题所在,假设执行了1、3,这是引用已经是最新的了,但2的构造函数没有执行,那你的对象的状态值就是失效的,就是说你的n是失效值,当这种偶然的情况出现,另一个线程调用assertSanity自然会报出异常,这就解释了为什么holder引用最新的情况下会报出异常。

第二、为什么加了final修饰n之后就不会出现问题

知道了上面代码的问题所在,我们就可以来了解一下final的作用,javamex上也有一篇“Thread-safety with the Java final keyword”,其中有一段对于final作用的解释是这么说的

“The final field is a means of what is sometimes called safe publication . Here, "publication" of an object means creating it in one thread and then having that newly-created object be referred to by another thread at some point in the future. When the JVM executes the constructor of your object, it must store values into the various fields of the object, and store a pointer to the object data. As in any other case of data writes, these accesses can potentially occur out of order, and their application to main memory can be delayed and other processors can be delayed unless you take special steps to combat this. In particular, the pointer to the object data could be stored to main memory and accessed before the fields themselves have been committed (this can happen partly because of compiler ordering: if you think about how you'd write things in a low-level language such as C or assembler, it's quite natural to store a pointer to a block of memory, and then advance the pointer as you're writing data to that block). And this in turn could lead to another thread seeing the object in an invalid or partially constructed state.
    final prevents this from happening: if a field is final , it is part of the JVM specification that it must effectively ensure that, once the object pointer is available to other threads, so are the correct values of that object's final fields.”

大体的意思很简单,对于含有final域的对象,JVM必须保证对对象的初始引用在构造函数之后执行,不能乱序执行(out of order),也就是可以保证一旦你得到了引用,final域的值都是完成了初始化的,也就是书中所说的“初始化安全性”的保证。这样一来,我们就可以理解为什么上面的问题就可以被解决了,JVM保证了不会乱序执行,自然也不会出现问题。

  • 11
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值