使用锁保护非易失性字段的初始化?

时间:2014-06-01 20:14:14

标签: java concurrency

出于教育目的,我编写了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;包装到写锁定/解锁中会有所作为吗?

3 个答案:

答案 0 :(得分:4)

第17章关于并发代码的原因发生在关系之前。在您的示例中,如果您使用两个随机线程,那么this.value = initialValue;result = value;之间就没有发生过关系。

所以如果你有类似的东西:

  1. T1.start();
  2. T2.start();
  3. ...
  4. T1:L = new PlainSimpleAtomicLong(42);
  5. T2:long value = L.get();
  6. 唯一发生在(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,因为您不知道上下文切换何时发生,您不知道谁将首先获得锁定。