情况如下:
- 我有一个有很多二传手和吸气剂的物体。
- 此对象的实例是在一个特定线程中创建的,其中设置了所有值。最初我使用new语句创建一个“空”对象,然后我才根据一些复杂的遗留逻辑调用一些setter方法。
- 只有这个对象才可用于仅使用getter的所有其他线程。
问题是: 我是否必须使此类的所有变量都不变?
关注:
- 创建对象的新实例并设置其所有值
在时间上分开。
- 但所有其他线程都不知道这一点
新实例,直到设置所有值。所以其他线程不应该
有一个未完全初始化对象的缓存。不是吗?
注意: 我知道生成器模式,但由于其他几个原因我无法应用它:(
编辑:
由于我觉得Mathias和axtavt的两个答案不匹配,我想补充一个例子:
假设我们有一个 foo
类:
class Foo {
public int x=0;
}
并且两个线程正在使用它,如上所述:
// Thread 1 init the value:
Foo f = new Foo();
f.x = 5;
values.add(f); // Publication via thread-safe collection like Vector or Collections.synchronizedList(new ArrayList(...)) or ConcurrentHashMap?.
// Thread 2
if (values.size()>0){
System.out.println(values.get(0).x); // always 5 ?
}
据我所知,Mathias可以根据JLS在某些JVM上打印出0。据我所知,它将始终打印5。
你有什么意见?
-
问候,
德米特里
在这种情况下,您需要使用 安全出版习语 当你的对象可用于其他线程时,即(来自 Java并发实践):
- 从静态初始化程序初始化对象引用;
- 将对它的引用存储到volatile字段或AtomicReference中;
- 将对它的引用存储到正确构造的对象的最终字段中;要么
- 将对它的引用存储到由锁正确保护的字段中。
如果使用安全发布,则无需声明字段 volatile
。
但是,如果你不使用它, 声明字段 volatile
(理论上)无济于事,因为内存障碍所致 volatile
是单方面的:易失性写入可以在其后使用非易失性动作重新排序。
所以, volatile
在以下情况下确保正确性:
class Foo {
public int x;
}
volatile Foo foo;
// Thread 1
Foo f = new Foo();
f.x = 42;
foo = f; // Safe publication via volatile reference
// Thread 2
if (foo != null)
System.out.println(foo.x); // Guaranteed to see 42
但在这种情况下不起作用:
class Foo {
public volatile int x;
}
Foo foo;
// Thread 1
Foo f = new Foo();
// Volatile doesn't prevent reordering of the following actions!!!
f.x = 42;
foo = f;
// Thread 2
if (foo != null)
System.out.println(foo.x); // NOT guaranteed to see 42,
// since f.x = 42 can happen after foo = f
从理论的角度来看,在第一个样本中存在一个传递发生在之前的关系
f.x = 42 happens before foo = f happens before read of foo.x
在第二个例子中 f.x = 42
并阅读 foo.x
没有发生在之前的关系,因此它们可以按任何顺序执行。
你不需要声明你的字段volatile之前设置它的值 start
在读取字段的线程上调用方法。
原因是在这种情况下,设置处于先前发生的关系(如Java语言规范中所定义)与另一个线程中的读取。
JLS的相关规则是:
- 线程中的每个动作都发生在该线程中的每个动作之前,该动作在程序的顺序中稍后出现
- 在启动线程中的任何操作之前发生对线程启动的调用。
但是,如果在设置字段之前启动其他线程,则必须声明字段volatile。 JLS不允许您假设线程在第一次读取它之前不会缓存该值,即使在特定版本的JVM上可能就是这种情况。
为了充分了解正在发生的事情,我一直在阅读有关Java内存模型(JMM)的内容。可以在Java Conurrency in Practice中找到对JMM的有用介绍。
我认为问题的答案是:是的,在给出使对象volatile的成员不是必需的示例中。但是,这种实现相当脆弱,因为这种保证取决于完成事情的确切ORDER以及Container的Thread-Safety。构建器模式将是更好的选择。
为什么保证:
- 线程1在将值放入线程安全容器之前完成所有赋值。
线程安全容器的add方法必须使用一些同步构造,如volatile读/写,锁或synchronized()。这保证了两件事:
- 在同步之前的线程1中的指令实际上将在之前执行。也就是说,不允许JVM使用同步指令对指令进行重新排序以进行优化。这称为发生前保证。
- 在线程1中的同步之前发生的所有写入之后将对所有其他线程可见。
发布后永远不会修改对象。
但是,如果容器不是线程安全的,或者事物的顺序被不知道该模式的人改变或者在发布后意外地更改了对象,则不再有任何保证。因此,遵循Builder模式,可以通过谷歌AutoValue或Freebuilder生成更安全。
这篇关于这个主题的文章也很不错:
http://tutorials.jenkov.com/java-concurrency/volatile.html