Java Concurrency in Practice - 示例14.12

ben*_*ben 15 java concurrency multithreading 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.... */
Run Code Online (Sandbox Code Playgroud)

我有一个关于上面的示例的问题,该示例是从Java Concurrency中提取的实践清单14.12计算使用Lock实现的信号量.

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

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

Pri*_*ley 12

其他线程看不到半构造对象

这不是真的.如果对象具有任何非final/volatile字段,则在构造时对其他线程可见.因此,其他线程可能会看到permitsie 的默认值,0这可能与当前线程不一致.

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

从Java Concurrency in Practice的清单3.15开始:

虽然在构造函数中设置的字段值似乎是写入这些字段的第一个值,因此没有"较旧"的值可以看作是过时值,但是在子类构造函数运行之前,Object构造函数首先将默认值写入所有字段.因此,可以将字段的默认值视为陈旧值.

  • 是的,`permit` 应该由 getter/setter 方法的锁保护,以确保构造后线程之间的内存可见性和互斥,而不是构造期间。在对象构造过程中,只有一个线程(构造线程)可以到达半构造对象(假设没有引用逃逸),所以不需要互斥。此外,安全发布将确保其他线程可以获得对象引用(和其他相关字段)的最新值,从而保证内存可见性。在没有锁的情况下确保线程安全。 (2认同)