2015-02-23 92 views
0

只是为了确保我理解java实践中呈现的概念。java中的初始化安全性

可以说我有以下程序:

public class Stuff{ 
    private int x; 

    public Stuff(int x){ 
     this.x=x; 
    } 

    public int getX(){return x;} 
} 

public class UseStuff(){ 
    private Stuff s; 

    public void makeStuff(int x){ 
     s=new Stuff(x); 
    } 

    public int useStuff(){ 
     return s.getX(); 
    } 
} 

如果我让多个线程使用此代码打球,那么我不仅麻烦,因为S可能会发生被指向如果两个或更多的多个实例线程正在进入makeStuff方法,但即使只有一个线程创建了一个新的Stuff,那么刚刚进入useStuff的其他线程可以通过其构造函数返回值0(预定义的int值)或分配给“x”的值。

这一切都取决于构造函数是否已完成初始化x。

所以在这一点上,为了使线程安全,我必须做一件事,然后我可以从两种不同的方式中选择。

首先,我必须make makeStuff()原子,所以“s”将一次指向一个对象。

然后,我要么使useStuff同步,以确保我只能在其构造函数完成构建后才能返回Stuff对象x var,或者我可以使Stuff的x最终,并由此JMM确保x的值只有在初始化后才可见。

我了解最终字段在并发和JMM上下文中的重要性吗?

+0

假设UseStuff的实例在线程之间共享,那么:no,s将始终指向一次只有一个实例。你的问题是你不确定它是哪个实例,因为多个线程可能会覆盖该引用。通过将其设置为final,您可以确保在UseStuff初始化时设置了引用s,并且在任何线程中都不会更改它。 – Gergely 2015-02-23 21:23:04

+0

对不起,只是注意到你想做x最后。所以在这种情况下,你要保证一旦创建了一个Stuff实例,它的字段x就再也不会被改变了。在这种情况下,字段仍然可以被新的Stuff覆盖,但是至少可以确定Stuff的特定实例永远不会有任何线程更改其x。 – Gergely 2015-02-23 21:29:23

回答

0

你想通过使事物同步来保护你有什么?你是否担心线程A会调用makeStuff,然后线程B会调用getStuff并且值不会在那里?我不知道如何同步任何这将有助于这一点。根据你想要避免什么问题,它可能就像标记为volatile一样简单。

-1

我不知道你在那里做什么。你为什么要创建一个对象,然后将其分配给一个字段?为什么要保存它,如果它可以被其他电话覆盖makeStuff?它看起来像你使用UseStuff作为代理,并作为你的实际Stuff模型对象的工厂。您更好地分离两个:

public class StuffFactory { 
    public static Stuff createStuff(int value) { 
    return new StuffProxy(value); 
    } 
} 

public class StuffProxy extends Stuff { 
    // Replacement for useStuff from your original UseStuff class 
    @Override 
    public int getX() { 
    //Put custom logic here 
    return super.getX(); 
    } 
} 

这里的逻辑是,每个线程负责创建他们自己的东西的对象(使用工厂),所以并发访问不再是一个问题。

3

我了解最终字段在并发和JMM环境中的重要性吗?

不完全。该规范writes

final领域也允许程序员来实现线程安全的不可变对象不同步。线程安全的不可变对象被所有线程看作是不可变的,即使使用数据竞争将线程间不可变对象的引用传递给线程。这可以通过不正确或恶意代码

提供针对不可变类的误用的安全保证如果您x最终,这保证了每一个获得一个Stuff实例的引用线程将观察x已被分配。它并不保证任何线程都会获得这样的参考。

也就是说,在缺少useStuff()中的同步操作时,允许运行时从寄存器中满足读取s,该寄存器可能会返回一个陈旧值。

此代码的最便宜的正确同步变体声明为s易失性,这可确保写入s发生在之后(因此可见)后续读取s。如果你这样做,你甚至不需要做最终的x(因为写入x发生 - 在写入s之前,s的读取发生在读取x之前,并且发生在传递之前)。

2

一些答案声称s一次只能引用一个对象。这是错误的;因为没有内存屏障,不同的线程可以对s的值有自己的概念。为了让所有线程看到分配给s的一致值,您需要声明svolatile,或者使用其他一些内存屏障。

如果你这样做,你就不会需要声明x作为final正确的价值在于为所有线程都是可见的(但你可能还是想;领域不应该是没有原因的可变)。这是因为x初始化之前发生s在分配“源代码顺序,”和挥发性场s的写操作之前发生其他线程s读取值。但是,如果您随后修改了非最终字段x的值,则可能会遇到麻烦,因为修改不能保证对其他线程可见。使不可变的Stuff将消除这种可能性。

当然,没有什么可以阻止线程将0x123的值分配给s,因此不同的线程仍然可以看到x的不同值。但这不是一个真正的线程问题。即使是单个线程也可以随时写入然后读取不同的值x。但是,在多线程环境中阻止这种行为需要原子性,即检查以查看s是否有值,如果没有,则分配一个值应该显示为其他线程的一个不可分割操作。一个AtomicReference将是最好的解决方案,但​​关键字也可以。

+0

说某个特定时间s可以有多个值可能是正确的,但这种思考方式不会帮助您编写正确的代码。你如何证明线程X正在读取“正确的”值?想一想更好的方法是,变量一次只有一个值,但很难知道它是什么时间。Java的规则是这样的:“发生之前”的规则告诉你如何正确地同步程序,以便当某些给定的线程执行某些特定的代码行时,可以提示它是什么时间(相对于其他线程中的事件)。 – 2015-02-23 22:15:50

+0

@jameslarge这是一个相当的方式来看待它。我不明白它有多好。去任何有意义的东西给你。 – erickson 2015-02-23 23:51:46

+0

这更好,因为它是从Java的“发生之前”规则写入的角度。 “之前发生”规则讨论的线程可能会或可能不会以相同的顺序经历相同的字段更新,并且它们告诉我们如何在重要时限制重新排序(即如何“同步”)。如果您选择实时查看实际发生的内容,则必须调用“缓存”,“刷新”和“无效”等单词。这个观点有助于你看到问题,但这与帮助你解决问题的正式规则是相悖的。 – 2015-02-24 18:29:25