在启动时控制竞态条件

3

我有一些代码需要进行一次性初始化。但是这些代码没有明确的生命周期,因此在我的初始化完成之前,我的逻辑可以被多个线程潜在地调用。因此,我希望基本上确保我的逻辑代码“等待”直到初始化完成。

这是我的第一次尝试。

public class MyClass {
    private static final AtomicBoolean initialised = new AtomicBoolean(false);

    public void initialise() {
        synchronized(initialised) {
            initStuff();
            initialised.getAndSet(true);
            initialised.notifyAll();
        }
    }

    public void doStuff() {
        synchronized(initialised) {
            if (!initialised.get()) {
                try {
                    initialised.wait();
                } catch (InterruptedException ex) {
                    throw new RuntimeException("Uh oh!", ex);
                }
            }
        }

        doOtherStuff();
    }
}

我想确认一下这个代码是否能够做到我想要的效果——阻塞doStuff直到initialised为true,并且我没有忽略任何竞争条件,导致doStuff可能会被Object.wait()卡住而永远无法执行。

编辑:

我无法控制线程。而且我想要能够控制所有初始化完成的时间,这就是为什么doStuff()不能调用initialise()的原因。

我使用了AtomicBoolean,因为它既可以作为值持有者,又可以作为一个我可以同步的对象。我也可以简单地使用“public static final Object lock = new Object();”和一个简单的布尔标志。AtomicBoolean恰好给了我这两个功能。布尔值是不可修改的。

CountDownLatch正是我所需要的东西。我还考虑过使用0个许可的Sempahore。但CountDownLatch对于这个任务来说是完美的。


3
为什么不使用静态初始化器? - NG.
初始化函数在哪里被调用? - luke
你在构造函数中是否启动线程来执行初始化工作? - Ricardo Marimon
可能是Callers block until getFoo() has a value ready?的重复问题。 - erickson
5个回答

6

这是一种奇怪的混合库和内置并发控制方式。像这样的代码更加清晰:

public class MyClass {

  private static final CountDownLatch latch = new CountDownLatch(1);

  public void initialise() {
    initStuff();
    latch.countDown();
  }

  public void doStuff() {
    try {
      latch.await();
    } catch (InterruptedException ex) {
      throw new RuntimeException("Uh oh!", ex);
    }
    doOtherStuff();
  }

}

当 CountDownLatch 计数器归零时,是否需要同步/加锁? - mdma
+1 - CountDownLatch.await 在计数器归零时不会阻塞 - 我刚刚查看了 javadocs。 - mdma
没错,我刚才检查了源代码。在计数器归零之后,await() 方法会调用 Thread.interrupted() 并执行一次 volatile 读取。 - Esko Luontola

2

synchronized块会自动阻塞其他线程。只需使用一个简单的锁对象+状态变量:

public class MyClass {
    private static boolean initialised;
    private static final Object lockObject = new Object();

    public void initialise() {
        synchronized (lockObject) {
            if (!initialised) {
                initStuff();
                initialised = true;
            }
        }
    }

    public void doStuff() {
        initialise();
        doOtherStuff();
    }
}

2
如果您始终从同步块内访问布尔值,则不需要将其设置为易失性。 - mdma
糟糕,谢谢。我开始使用双重检查锁定+易失性,但后来决定对于这种简单的情况并不值得引起混淆。即使已经接受了另一个答案,问题还是被解决了。 - Brett Kail

1

最好使用静态初始化程序(如SB所提到的):

public class MyClass {

    public static void doInitialize() {
      ...
    }

    public void doStuff() {
        doOtherStuff();
    }

    static {
       doInitialize();
    }
}

这段代码将在任何其他代码被调用之前执行一次。如果每次使用该类都必须进行初始化,则不会有性能损失,因为该类直到使用时才会被加载。有关更多详细信息,请参见此问题的答案。


0

你一直在同步块内使用 AtomicBoolean。这样做没有太大意义,因为只有一个线程可以访问它。原子变量旨在用于无锁解决方案 - 你可以将其作为不可中断的单元获取和设置值。

我猜你是在寻找一个无锁解决方案,一旦初始化完成:

public class MyClass {
    private static final AtomicBoolean initialised = new AtomicBoolean(false);

    public void initialise() {
        if (!intialized.get())
        {
            synchornized (this)
            {
               if (!initialized.getAndSet(true))
                  doInitialize();
            }
        }
    }

    public void doStuff() {
        initialize();
        doOtherStuff();
    }

你也可以使用一个简单的volatile boolean来实现这个功能,它比AtomicBoolean更高效。

0

如果这是在启动时正确的,为什么不等到初始化完成后再开始其他线程呢?

此外,您可以使用线程同步的IsComplete布尔值,直到初始化程序将其设置为true之前,它将保持为false。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接