使用锁保护非易失性字段的初始化?
出于教育目的,我正在编写一个简单版本的AtomicLong
,其内部变量由ReentrantReadWriteLock
保护。 这是一个简化的例子:
public class PlainSimpleAtomicLong { private long value; private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); public PlainSimpleAtomicLong(long initialValue) { this.value = initialValue; } public long get() { long result; rwLock.readLock().lock(); result = value; rwLock.readLock().unlock(); return result; } // incrementAndGet, decrementAndGet, etc. are guarded by rwLock.writeLock() }
我的问题 :由于“value”是非易失性的,其他线程是否可以通过PlainSimpleAtomicLong.get()
观察到错误的初始值? 例如,线程T1
创建L = new PlainSimpleAtomicLong(42)
并与线程T2
共享引用。 T2
是否保证将L.get()
视为42?
如果没有,将包装this.value = initialValue;
写入锁定/解锁有所作为?
第17章关于并发代码的原因发生在关系之前。 在你的例子中,如果你采用两个随机线程,那么this.value = initialValue;
之间没有发生之前的关系this.value = initialValue;
和result = value;
。
所以,如果你有类似的东西:
-
T1.start();
-
T2.start();
-
...
- T1:
L = new PlainSimpleAtomicLong(42);
- T2:
long value = L.get();
您拥有的唯一发生之前 (hb)关系(除了每个线程中的程序顺序)是:1&2 hb 3,4,5。
但是没有订购4和5。 然而,如果T1在T2之前调用L.get()
调用L.get()
(从挂钟的角度来看)那么你将在T1中的unlock()
和T2中的lock()
之间建立一个hb关系。
正如已经评论过的那样,我不认为您提出的代码可能会破坏JVM /硬件的任何组合,但它可能会破坏JMM的理论实现。
至于你将构造函数包装在锁定/解锁中的建议,我认为这还不够,因为至少在理论上,T1可以在运行构造函数体之前向L释放有效引用(非null)。 所以风险是T2可以在T1在构造函数中获取锁之前获取锁。 同样,这是一种交错,在当前的JVM /硬件上可能是不可能的。
总而言之,如果你想要理论线程安全,我认为你不能没有一个volatile long value
,这就是AtomicLong
的实现方式。 volatile
将保证在发布对象之前初始化该字段。 最后请注意,我在这里提到的问题不是由于您的对象不安全(请参阅@BrettOkken答案),而是基于不能跨线程安全发布对象的情况。
假设您不允许对实例的引用转义构造函数(您的示例看起来很好),那么第二个线程永远不会看到具有任何“value”值的对象,而不是它的构造,因为所有访问都受到保护通过监视器(读写锁)在构造函数中是最终的。
https://www.ibm.com/developerworks/library/j-jtp0618/ http://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/locks/Lock.html
我认为对于初始值,两个线程都会看到相同的值(因为它们只有在构造函数完成后才能拥有对象)。
但
如果更改1个线程中的值,则如果不使用volatile,则其他线程可能看不到相同的值
如果你想实现set,带锁/解锁的包装将无法解决问题 – 这在需要primefaces操作(如增量)时很好。 一世
这并不意味着它会以您想要的方式工作,因为您不控制上下文切换。 例如,如果2个线程调用set,值为4和8,因为您不知道上下文切换何时发生,您不知道谁将首先获得锁定。