Java 并发实践 - 示例 14.12

2022-09-02 13:46:56
// 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 并发实践清单 14.12 使用 Lock 实现的计数信号量中提取的。

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

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


答案 1

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

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

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

摘自 Java 并发实践中的清单 3.15:

虽然构造函数中设置的字段值似乎是写入这些字段的第一个值,因此没有“较旧的”值可以视为过时的值,但 Object 构造函数首先将默认值写入所有字段,然后再运行子类构造函数。因此,可以将字段的默认值视为过时值。


答案 2

老实说,我在这里没有看到锁的任何有效用途,除了它引入了内存栅栏。 无论如何,赋值在32/64位上都是原子的。int


推荐