没有最终修饰符的按需初始化持有者习语线程安全吗

Posted

技术标签:

【中文标题】没有最终修饰符的按需初始化持有者习语线程安全吗【英文标题】:Is Initialization On Demand Holder idiom thread safe without a final modifier 【发布时间】:2014-01-26 12:24:51 【问题描述】:

我有一种预感,使用 holder 惯用语而不将 holder 字段声明为 final 不是线程安全的(由于 Java 中不变性的工作方式)。有人可以证实这一点(希望有一些消息来源)吗?

public class Something 
    private long answer = 1;

    private Something() 
         answer += 10;
         answer += 10;
    

    public int getAnswer() 
      return answer;
    

    private static class LazyHolder 
        // notice no final
        private static Something INSTANCE = new Something();
    

    public static Something getInstance() 
        return LazyHolder.INSTANCE;
    


编辑:我绝对想要来源声明,而不仅仅是“它有效”之类的断言——请解释/证明它是安全的

EDIT2:稍作修改以使我的观点更清楚 - 我可以确定无论调用线程如何 getAnswer() 方法都会返回 21?

【问题讨论】:

它完全是线程安全的,不管是否是最终的。 这个断言的来源? Java 内存模型 - 我会回答...哎呀,太晚了:@assylias 已经为我完成了。 【参考方案1】:

class initialization procedure 保证如果使用静态初始化程序(即static variable = someValue;)设置静态字段的值,则该值对所有线程都是可见的:

10 - 如果初始化程序的执行正常完成,则获取 LC,将 C 的 Class 对象标记为完全初始化,通知所有等待线程,释放 LC,并正常完成此过程。


关于您的编辑,让我们想象一下有两个线程 T1 和 T2 的情况,从挂钟的角度来看,按照该顺序执行:

T1:Something s = Something.getInstance(); T2:Something s = Something.getInstance(); i = s.getAnswer();

那么你有:

T1 获取 LC,T1 运行 Something INSTANCE = new Something();,初始化 answer,T1 释放 LC T2 尝试获取 LC,但已被 T1 锁定 => 等待。当 T1 释放 LC 时,T2 获取 LC,读取INSTANCE,然后读取answer

因此,您可以看到answer 的写入和读取之间存在正确的happens-before 关系,这要归功于LC 锁定。

【讨论】:

@edit - 你回答后我很清楚,但无论如何感谢这个例子 @assylias,我不确定当s(reference) 已发布但answer 尚未初始化时,这里是否不可能出现这种情况,或者我错了?谢谢。【参考方案2】:

它肯定是线程安全的,但它是可变的。所以任何得到它的人都可以将它分配给其他东西。这是首先要担心的事情(甚至在考虑线程安全之前)。

【讨论】:

只有,Something 类以外的人可以得到它。 是的,我确实 +1。所以实际上可变性问题只会出现在 Something 类的方法中。但我认为它是线程安全的,因为无论如何都会同步类加载(无论是最终还是非最终)。 @MarkoTopolnik - 当然任何人都可以使用公共方法来获取实例,你是什么意思? @JakubBochenski 我的意思是分配它。 @JakubBochenski 您也可以使用反射修改private static final 字段(除非它是编译时常量)。

以上是关于没有最终修饰符的按需初始化持有者习语线程安全吗的主要内容,如果未能解决你的问题,请参考以下文章

SecureRandom 线程安全吗?

同一个类中,有两个方法都用 synchronized 修饰,这两个方法线程安全吗?

线程安全的Singleton要点

final 关键字与安全发布 多线程中篇(十三)

浅谈死锁问题以及如何解决

java中Volatile修饰符的含义