Java happens-before关系invokeAndWait

4
我的问题与此问题有关,它已经有了答案:
“是的,调用invokeLater/invokeAndWait的线程和提交的Runnable在EDT上执行的操作之间存在一个'happens-before'关系。”
我的问题更加普遍:是否可能实现一种方法(例如invokeAndWait),使其正常工作,但不要求发生“happens-before”关系?对于“正常工作”,我是指以下特性:
- 已提交的Runnable保证只执行一次。 - 已提交的Runnable在一个特定的线程上执行。 - 该方法等待已提交的Runnable执行完成。 - 该方法保证在已提交的Runnable执行完成后返回。
在我看来,似乎没有办法在不产生“happens-before”关系的情况下实现这一点,我错了吗?如果是这样,请提供一个可以证明这一点的示例实现。

作为好奇,为什么您想在特定线程中完成计算,但又阻塞了调用线程? - rascio
与问题本身无关,但您可能希望仅在特定线程上执行不安全的线程代码,然后在另一个线程上等待结果。我认为这也是SwingUtilities.invokeAndWait(java.lang.Runnable)函数的用例。 - stonar96
好的,这是为了防止在该计算上出现并行性。 - rascio
1个回答

2
这里最困难的要求是:
提交的Runnable保证只会被执行一次。
在使用非volatile(平凡的)字段将工作任务从提交者传递给执行者时,不会创建关系,但也不能保证执行者以有限的时间内看到任务。编译器可以优化那个字段的赋值,或者在运行时,执行者线程可能仅从其高速缓存中读取该值,而不是从主存储器中读取。因此,对于使用Java 8或更低版本的代码,我认为答案是“不,这样的invokeAndWait方法不可能存在”(除非使用本地代码)。
然而,Java 9 添加了内存模式 Opaque。Doug Lea 是 JEP 193(添加此功能的人)的作者,在页面 "Using JDK 9 Memory Order Modes" 中详细描述了这一点。最重要的是,Opaque 模式比 volatile 弱,但仍然提供以下保证:
  • 进展。 写入最终可见。
    [...]
    例如,在仅当一个线程以 Opaque(或更强)模式写入变量 x 的唯一修改时,X.setOpaque(this, 1),任何在 while(X.getOpaque(this)!=1){} 自旋的其他线程最终将终止。
    [...]
    请注意,这个保证在 Plain 模式下不成立,在这种模式下自旋循环可能会(并且通常会)无限循环 [...]
设计这样一个没有“happens-before”关系的invokeAndWait方法时,您还必须考虑在启动线程之前的操作发生在该线程中的第一个操作之前(参见JLS §17.4.4)。因此,在构建操作之前必须启动工作线程。
此外,还必须考虑“final字段语义”(JLS §17.15.1)。如果invokeAndWait的调用者使用lambda表达式创建Runnable,那么通过lambda捕获变量具有(据我理解)隐含的final字段语义。

如果是这样,请包括一个示例实现,证明这一点。

使用示例来证明或反驳线程安全性或“happens-before”关系是困难的,甚至是不可能的,因为它取决于硬件和时间。然而,像jcstress这样的工具可以帮助解决这个问题。
下面是一个(简化的)没有“happens-before”关系的invokeAndWait的潜在实现。请注意,我对Java内存模型并不完全熟悉,所以代码中可能会有错误。
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

class OpaqueExecutor {
  // For simplicity assume there will every only be a single work task
  // So pending task being replaced by other task is not an issue
  private final AtomicReference<Runnable> nextTask = new AtomicReference<>();

  public OpaqueExecutor() {
    Thread worker = new Thread(() -> {
      while (true) {
        // Use getOpaque() to no create happens-before relationship
        Runnable task = nextTask.getOpaque();
        if (task == null) {
          // For efficiency indicate to the JVM that this is busy-waiting
          Thread.onSpinWait();
        } else {
          // Clear pending task; memory mode here does not matter because we only want
          // to guarantee that this thread does not see task again
          nextTask.setPlain(null);
          task.run();
        }
      }
    }, "Worker thread");
    worker.setDaemon(true);
    worker.start();
  }

  public void invokeLater(Runnable runnable) {
    // For simplicity assume that there is no existing pending task which could be
    // replaced by this
    // Use setOpaque(...) to not create happens-before relationship
    nextTask.setOpaque(runnable);
  }

  private static class Task implements Runnable {
    private final AtomicBoolean isFinished = new AtomicBoolean(false);
    // Must NOT be final to prevent happens-before relationship from
    // final field semantics
    private Runnable runnable;

    public Task(Runnable runnable) {
      this.runnable = runnable;
    }

    public void run() {
      try {
        runnable.run();
      } finally {
        // Use setOpaque(...) to not create happens-before relationship
        isFinished.setOpaque(true);
      }
    }

    public void join() {
      // Use getOpaque() to no create happens-before relationship
      while (!isFinished.getOpaque()) {
        // For efficiency indicate to the JVM that this is busy-waiting
        Thread.onSpinWait();
      }
    }
  }

  public void invokeAndWait(Runnable runnable) {
    Task task = new Task(runnable);
    invokeLater(task);
    task.join();
  }

  public static void main(String... args) {
    // Create executor as first step to not create happens-before relationship
    // for Thread.start()
    OpaqueExecutor executor = new OpaqueExecutor();

    final int expectedValue = 123;
    final int expectedNewValue = 456;

    class MyTask implements Runnable {
      // Must NOT be final to prevent happens-before relationship from
      // final field semantics
      int value;

      public MyTask(int value) {
        this.value = value;
      }

      public void run() {
        int valueL = value;
        if (valueL == expectedValue) {
          System.out.println("Found expected value");
        } else {
          System.out.println("Unexpected value: " + valueL);
        }

        value = expectedNewValue;
      }
    }

    MyTask task = new MyTask(expectedValue);
    executor.invokeAndWait(task);

    int newValue = task.value;
    if (newValue == expectedNewValue) {
      System.out.println("Found expected new value");
    } else {
      System.out.println("Unexpected new value: " + newValue);
    }
  }
}

1
在Java 9之前,可以实现这样的行为,而不需要其不透明模式。关键点已经在“为什么Java中双重检查锁定(无需volatile)是错误的”等文章中反复描述过。这种反模式确切地能够产生一个非null值,而不需要实际的* happens-before *关系。 - Holger
@Holger,您在这个领域可能比我更有知识。您介意添加一个单独的答案来描述这个问题吗?在Java 9之前,是否真的可以满足OP的“保证只执行一次”的要求(我理解为隐含的“最终执行”)? - Marcono1234

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