【问题标题】:Java Concurrency in Practice - Sample 14.12Java 并发实践 - 示例 14.12
【发布时间】:2012-05-10 06:29:20
【问题描述】:
// 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() 被调用)。 据我所知,构造函数是 atomic (除了引用转义),因为没有其他线程可以获取引用,因此,半构造对象对其他线程不可见。 因此,我们不需要构造函数的 synchronized 修饰符。 此外,我们也不需要担心内存可见性,只要对象被安全发布即可。

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

【问题讨论】:

标签: java multithreading concurrency locking


【解决方案1】:

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

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

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

来自 Java 并发实践的清单 3.15:

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

【讨论】:

  • 非常感谢您的回答,非常感谢您的回答非常详细。但是,在构造过程中,除了构造线程之外的线程无法获得对象的引用。由于其他线程无法获取对象引用,它们无法读取对象的int 值。因此,其他线程无法读取过时的值,因为它们甚至无法读取该值。此外,如果对象被安全发布(即静态初始化器/最终字段/易失性字段/由锁保护的字段)。不会出现“陈旧价值问题”。
  • @benben:对象引用可见性和对象的状态可见性是不同的东西。对象的引用和对象的状态必须同时对其他线程可见。阅读清单 JCP 3.14 美元。在您的情况下,使用field guarded by a lock
  • field guarded by a lock 表示@GuardedBy("lock") SemaphoreOnLock sol = ...;。锁不在 SemaphoreOnLock 类的构造函数中。这意味着使用 SemaphoreOnLock 对象锁定其他一些类。
  • 是的,permit 应该由 getter/setter 方法的锁来保护,以确保在构造之后线程之间的内存可见性和互斥性,而不是在构造期间。在对象构造过程中,只有一个线程(构造线程)可以到达半构造对象(假设没有引用转义),因此不需要互斥。此外,安全发布将确保其他线程可以获得对象引用(和其他相关字段)的最新值,从而保证内存可见性。无需锁即可确保线程安全。
  • 其实你可以看到Listing 3.15 Class at Risk of Failure if Not Properly Published的Java Concurrency in Practice。 这里的问题不是Holder类本身,而是Holder没有正确发布……”作者说的。SemaphoreOnLock类和@987654332中的Holder类类似@.
【解决方案2】:

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

【讨论】:

  • 并非如此。两个分配不是原子的。非最终字段首先初始化为默认值,然后才会发生实际分配
  • 在这种情况下,他们可以将字段设置为 volatile。锁本身保护一个单一的原子分配,所以唯一有用的效果是内存栅栏。
  • 是的 volatile 可以使用,因为 int 是原子的。但是这个例子有field guarded by a lock(内存栅栏)机制来确保对象的状态可见性在线程之间是一致的。
  • @Prince John Wesley 你真的认为 volatile 不足以实现跨线程可见性吗?
【解决方案3】:

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

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

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

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

这个例子说明的是,当将任何非静态、非最终、非易失性字段设置为非默认值。

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

【讨论】:

    猜你喜欢
    • 2014-04-12
    • 2017-10-08
    • 2018-11-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多