19
// 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 Counting Semaphore Implemented Using Lock。

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

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

4

3 回答 3

15

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

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

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

来自 Java Concurrency in Practice 的清单 3.15:

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

于 2012-05-10T06:33:22.527 回答
0

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

于 2012-05-10T08:35:03.890 回答
0

(只是为我自己可怜的脑袋澄清一下-其他答案是正确的)。

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

  1. 在第一种情况下,它被初始化为默认值0
  2. 然后由线程为其分配一个值(可能不是默认值0T1
  3. 它不是volatile
  4. 它不是final(这使它有点像“一发不可收拾”)

因此,如果我们要T2读取T1最后写入的值,就需要进行同步。我们必须在构造函数中这样做,就像在其他情况下一样。(它是否是原子分配的事实不会影响此可见性问题)。将构造限制在单个线程的策略SemaphoreOnLock对我们不起作用,因为制作它的整个想法@Threadsafe是让我们可以安全地共享它

此示例说明的是,当将任何非静态、非最终、非易失性字段设置为其默认值以外的值时,“线程安全”也适用于对象的构造。

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

于 2012-12-09T13:56:15.130 回答