出于教育目的,我编写了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()
}
我的问题:因为"价值"是非易失性的,其他线程是否可以通过PlainSimpleAtomicLong.get()
观察到错误的初始值?
例如。线程T1
创建L = new PlainSimpleAtomicLong(42)
并与线程T2
共享引用。 T2
是否保证将L.get()
视为42?
如果没有,将this.value = initialValue;
包装到写锁定/解锁中会有所作为吗?
答案 0 :(得分:4)
第17章关于并发代码的原因发生在关系之前。在您的示例中,如果您使用两个随机线程,那么this.value = initialValue;
和result = value;
之间就没有发生过关系。
所以如果你有类似的东西:
T1.start();
T2.start();
...
L = new PlainSimpleAtomicLong(42);
long value = L.get();
唯一发生在(hb)之前的关系(除了每个线程中的程序顺序)是:1& 2 hb 3,4,5。
但是没有订购4和5。但是,如果T2在T2之前调用L.get()
调用L.get()
(从挂钟角度来看)那么你在T1 unlock()
和{{1}之间会有一个 hb 关系在T2。
正如已经评论过的那样,我不认为您提出的代码可能会破坏JVM /硬件的任何组合,但它可能会打破JMM的理论实现。
至于你把构造函数包装成锁定/解锁的建议,我认为这还不够,因为理论上至少T1可以在运行之前释放一个有效的引用(非null)给L构造函数。所以风险是T2可以在T1在构造函数中获取锁之前获取锁。同样,这是一种交错,在当前的JVM /硬件上可能是不可能的。
总而言之,如果你想要理论线程安全,我认为你不能没有lock()
,这就是volatile long value
的实现方式。 AtomicLong
将保证在发布对象之前初始化该字段。最后请注意,我在这里提到的问题不是由于您的对象不安全(请参阅@BrettOkken答案),而是基于不能跨线程安全发布对象的情况。
答案 1 :(得分:1)
假设您不允许对实例的引用转义构造函数(您的示例看起来很好),那么第二个线程永远不会看到具有" 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
答案 2 :(得分:-1)
我认为对于初始值,两个线程都会看到相同的值(因为只有在构造函数完成后它们才能拥有对象)。
但是
如果更改1个线程中的值,则如果不使用volatile,则其他线程可能看不到相同的值
如果你想实现set,带锁定/解锁的包装集将无法解决问题 - 这在需要原子操作(如增量)时很好。 我
这并不意味着它会以您想要的方式工作,因为您不控制上下文切换。例如,如果2个线程调用set,则值为4& 8,因为您不知道上下文切换何时发生,您不知道谁将首先获得锁定。