【问题标题】:How to make sure that method is executed only once and from one thread only?如何确保该方法仅执行一次并且仅从一个线程执行?
【发布时间】:2017-06-22 05:22:02
【问题描述】:

我有一个下面的方法,我想在以下条件下执行:

  • 这个方法应该只执行一次。一旦它被执行,它就不能再次执行,所以如果有人试图再次执行,它应该通过记录一些有用的错误消息already executed 或任何有用的东西来返回。
  • 而且它只能由一个线程执行。那么如果多个线程在调用下面的方法,那么它应该只被一个线程调用,其他线程应该等待初始化完成?

下面是我的方法:

  public void initialize() {
    List<Metadata> metadata = getMetadata(true);
    List<Process> process = getProcess();
    if (!metadata.isEmpty() && !process.isEmpty()) {
        Manager.setAllMetadata(metadata, process);
    }
    startBackgroundThread();
  }

这可能吗?我正在使用 Java 7。

【问题讨论】:

  • 如果你想确保一段代码只执行一次,我认为将它放在 Enum 的类静态初始化程序中是尽可能接近的。 JVM 保证每个类加载器最多调用一次。但比这更好的是,我认为没有明确的方法可以提供这样的保证。如果您提供有关您的用例的更多信息,也许可以?
  • 我在我的一个类中有这个方法,它初始化我们所有的元数据,一旦初始化完成,那么我只想在我的应用程序中继续前进。
  • 把它放在这个类的静态初始化器中不够吗?
  • 为什么不设置哪个键是调用者线程ID?
  • 如果是实例级别,我会使用双重检查锁定,如果是类级别,我会使用按需初始化持有者惯用语。我更喜欢前者并使用 Guava 的 Suppliers.memoize() 来封装 DCL 逻辑。

标签: java multithreading concurrency


【解决方案1】:

@ShayHaned 的解决方案使用锁定。您可以通过AtomicBoolean 提高效率,例如:

AtomicBoolean wasRun = new AtomicBoolean(false);
CountDownLatch initCompleteLatch = new CountDownLatch(1);

public void initialize() {
  if (!wasRun.getAndSet(true)) {
      List<Metadata> metadata = getMetadata(true);
      List<Process> process = getProcess();
      if (!metadata.isEmpty() && !process.isEmpty()) {
          Manager.setAllMetadata(metadata, process);
      }
      startBackgroundThread();
      initCompleteLatch.countDown();
  } else {
      log.info("Waiting to ensure initialize is done.");
      initCompleteLatch.await();
      log.warn("I was already run");
  }
}

以上假设您不必等待startBackgroundThread 中的工作完成。如果这样做,解决方案将变为:

AtomicBoolean wasRun = new AtomicBoolean(false);
CountDownLatch initCompleteLatch = new CountDownLatch(1);

public void initialize() {
  if (!wasRun.getAndSet(true)) {
      List<Metadata> metadata = getMetadata(true);
      List<Process> process = getProcess();
      if (!metadata.isEmpty() && !process.isEmpty()) {
          Manager.setAllMetadata(metadata, process);
      }
      // Pass the latch to startBackgroundThread so it can
      // call countDown on it when it's done.
      startBackgroundThread(initCompleteLatch);
  } else {
      log.info("Waiting to ensure initialize is done.");
      initCompleteLatch.await();
      log.warn("I was already run");
  }
}

这样做的原因是AtomicBoolean.getAndSet(true) 将在一个原子操作中返回先前设置的值并使新值成为true。因此,第一个访问您的方法的线程将返回 false(因为变量已初始化为 false),并且它将自动将其设置为 true。由于第一个线程返回了错误,它将采用if 语句中的第一个分支,并且您的初始化将发生。任何其他调用都会发现 wasRun.getAndSet 返回 true,因为第一个线程将其设置为 true,因此他们将采用第二个分支,您将获得所需的日志消息。

CountDownLatch 被初始化为 1,因此除第一个线程之外的所有线程都在其上调用 await。它们将阻塞,直到第一个线程调用countDown,这会将计数设置为 0,从而释放所有等待的线程。

【讨论】:

  • 能否请您添加一些解释以便我理解?它还会照顾我的两种情况吗?如果您可以在此基础上进行解释,那么它将有助于我理解。我正在做一些研究,我想我可能必须按照你在这里的建议使用CountDownLatchAtomicBoolean
  • 添加了解释。
  • 我认为这个答案不能完全满足要求。 @user1950349 不满足第二个要求。在这里,不是第一个之后的所有线程都会认为这个方法运行了;但该方法可能仍在运行。第一个之后的所有线程都必须等待,这就是CountdownLatch 的效率所在。这个post 涵盖了这种方法——特别是@pdeva 的回答。
  • @Keith 你是对的。不知何故,我只是忽略了这一点。将更新我的答案。
【解决方案2】:

您可以为您的方法创建一个静态标志,该标志只有在调用该方法时才会更改。使用静态标志背后的想法是它不属于它属于类的实例,这意味着从同一个类创建的所有线程都可以访问相同的标志值,因此一旦标志的布尔值在第一次调用后更改方法所有其他线程将被 if else 条件语句绕过。

 static boolen flag;
public void initialize() {
if (flag)
{// return from here or some message you want to generate 
}else{
    List<Metadata> metadata = getMetadata(true);
    List<Process> process = getProcess();
    if (!metadata.isEmpty() && !process.isEmpty()) {
        Manager.setAllMetadata(metadata, process);
    }
       flag = true;
    startBackgroundThread();       }}

我希望这能解决您的问题

【讨论】:

    【解决方案3】:

    •而且它应该只由一个线程执行。那么如果多个线程在调用下面的方法,那么它应该只被一个线程调用,其他线程应该等待初始化完成?

    public static final Object singleThreadLock = new Object();
    
    public void initialize()
    {
        synchronized( singleThreadLock )
        {
    
            List<Metadata> metadata = getMetadata(true);
            List<Process> process = getProcess();
            if (!metadata.isEmpty() && !process.isEmpty()) 
            {
                Manager.setAllMetadata(metadata, process);
            }
           startBackgroundThread();
        } 
      }
    

    这些代码行保证每个线程只调用一次 initialize(),并且由于 singleThreadLock 被声明为静态的,那么你当前生成的 JVM 将永远不会允许任何其他线程访问锁,直到块内同步完全执行。也请远离尝试同步(this),因为这样的语句可能会导致严重的并发问题。

    【讨论】:

    • 没有什么能阻止多个线程最终调用这个函数。阅读他的第二个要求。在调用startBackgroundThread() 之后,需要将某种wasRun 变量设置为true,然后应在临界区的开头评估wasRun
    猜你喜欢
    • 1970-01-01
    • 2022-08-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-04
    • 1970-01-01
    相关资源
    最近更新 更多