Java Concurrency in Practice - 示例14.12

时间:2012-05-10 06:29:21

标签: java multithreading concurrency locking

// Not really how java.util.concurrent.Semaphore is implemented
@ThreadSafe
public class SemaphoreOnLock {
    private final Lock lock = new ReentrantLock();
    // CONDITION PREDICATE: permitsAvailable (permits > 0)
    private final Condition permitsAvailable = lock.newCondition();
    @GuardedBy("lock") private int permits;

    SemaphoreOnLock(int initialPermits) {
        lock.lock();
        try {
            permits = initialPermits;
        } finally {
            lock.unlock();
        }
    }

/* other code omitted.... */

我对上面的示例提出了一个问题,该示例摘自 Java Concurrency in Practice ,代码清单14.12计算使用Lock实现的信号量。

我想知道为什么我们需要在构造函数中获取锁(如图所示调用lock.lock())。 据我所知,构造函数是 atomic (引用转义除外),因为没有其他线程可以获取引用,因此,半构造对象对其他线程不可见。 因此,我们不需要构造函数的synchronized修饰符。 此外,只要对象安全发布,我们也不需要担心内存可见性

那么,为什么我们需要在构造函数中获取ReentrantLock对象?

3 个答案:

答案 0 :(得分:12)

  

半构造对象对其他线程不可见

事实并非如此。如果对象具有任何非最终/易失性字段,则在构建时对其他线程可见。因此,其他线程可能会看到permits的默认值,即0,这可能与当前线程不一致。

Java内存模型为不可变对象(仅包含最终字段的对象)提供了初始化安全性的特殊保证。另一个线程可见的Object引用并不一定意味着该对象的状态对于消费线程是可见的 - JCP $3.5.2

实践中的Java Concurrency清单3.15:

  

虽然似乎在构造函数中设置的字段值是   写入这些字段的第一个值,因此没有   将“较旧”的值视为过时值,首先是对象构造函数   将默认值写入子类构造函数之前的所有字段   跑。因此,可以将字段的默认值视为   一个过时的值。

答案 1 :(得分:0)

老实说,除了引入内存栅栏这一事实外,我没有看到锁的任何有效用途。无论如何,int分配在32/64位上是原子的。

答案 2 :(得分:0)

(只是为我自己糟糕的头脑澄清一下 - 其他答案都是正确的。)

此假设SemaphoreOnLock类的实例旨在共享。所以线程T1完全构造一个实例,并将它放在线程T2可以看到的地方,并调用一些需要读取permits字段的方法。关于permits字段需要注意的一些重要事项:

  1. 在第一种情况下,它被初始化为默认值0
  2. 然后通过线程0
  3. 为其分配一个值(可能不是默认值T1
  4. 不是volatile
  5. 它不是final(这有点像'一次性挥发')
  6. 因此,如果我们希望T2读取T1上次写入的值,我们需要进行同步。我们必须在构造函数中执行此操作,就像我们在其他所有情况下一样。 (它是否为原子分配的事实不会影响此可见性问题)。 将构造的SemaphoreOnLock限制在单个线程中的策略对我们不起作用,因为制作它@Threadsafe的整个想法是我们可以安全分享

    这个例子说明的是“线程安全”也适用于对象的构造,当设置任何非静态,非最终,非易失性字段时值不是默认值。

    当然,当我们有@NotThreadsafe课时,我们没有义务考虑这个问题。如果调用者构造我们并决定在两个线程之间共享我们,那么调用者必须安排适当的同步。在这种情况下,我们可以在构造函数中做任何我们喜欢的事情而不用担心可见性问题 - 这是其他人的问题。