繁体   English   中英

Java 并发实践 - 示例 14.12

[英]Java Concurrency in Practice - Sample 14.12

// 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() 被调用)。 据我所知,构造函数是原子的(引用转义除外),因为没有其他线程可以获取引用,因此,其他线程看不到半构造对象。 因此,我们不需要构造函数的 synchronized 修饰符。 此外,我们也不需要担心内存可见性,只要对象是安全发布的。

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

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

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

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

来自 Java Concurrency in Practice 的代码清单 3.15:

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

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

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

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM