非易失性字段 + 来自另一个线程的第一个对象访问 (java)

Non-volatile fields + first object access from another thread (java)

我已经在某个服务器类型的应用程序上工作了一段时间,我发现它的设计挑战了我在 Java.[=25= 中看到内存一致性(可以这么说)的方式]


此应用程序使用 NIO,因此 I/O 线程数量有限(它们只做网络 I/O,没有别的;它们永远不会终止 ,但可能会因等待更多工作而受阻)。

每个连接在内部都表示为特定类型的对象,为了本示例,我们将其称为 ClientConClientCon 有各种会话相关字段,none 其中是易变的。这些字段的 getting/setting 值没有任何类型的同步。

接收到的数据由具有固定最大大小的逻辑单元组成。每个这样的单元都有一些元数据,允许决定处理类型(class)。完成后,一个该类型的新对象 被创建。所有此类处理程序都有字段,其中 none 是易变的。一个 I/O 线程(一个具体的 I/O 线程被分配给每个 ClientCon)然后调用一个 protected 读取方法,在新的处理程序上使用剩余的缓冲区内容(在读取元数据之后)对象。

在此之后,相同的处理程序对象被放入一个特殊的队列中,该队列(队列)然后被提交到一个线程池中执行(其中每个处理程序的运行方法被调用以根据读取的数据)。对于这个例子,我们可以说 TP 线程 永远不会终止

因此,TP 线程将获得它以前从未访问过的对象。该对象的所有字段都是非易失性的(并且most/all是非最终的,因为它们是在构造函数之外修改的)。

处理程序的 运行 方法可以根据 ClientCon 中特定于会话的字段进行操作并设置它们 and/or 处理程序对象自己的字段,其值在读取方法。


根据 CPJ(Java 中的并发编程:设计与原则):

The first time a thread accesses a field of an object, it sees either the initial value of the field or a value since written by some other thread.

可以在 JLS 17.5:

中找到此引用的更全面的示例
class FinalFieldExample { 
    final int x;
    int y; 
    static FinalFieldExample f;

    public FinalFieldExample() {
        x = 3; 
        y = 4; 
    } 

    static void writer() {
        f = new FinalFieldExample();
    } 

    static void reader() {
        if (f != null) {
            int i = f.x;  // guaranteed to see 3  
            int j = f.y;  // could see 0
        } 
    } 
}

The class FinalFieldExample has a final int field x and a non-final int field y. One thread might execute the method writer and another might execute the method reader.

Because the writer method writes f after the object's constructor finishes, the reader method will be guaranteed to see the properly initialized value for f.x: it will read the value 3. However, f.y is not final; the reader method is therefore not guaranteed to see the value 4 for it.


此应用程序已 运行在 x86(和 x86/64)Windows/Unix 操作系统(Linux 风格,Solaris)上运行多年(Sun/Oracle 和 OpenJDK JVM,版本 1.5 到 8),显然没有与接收数据处理相关的内存一致性问题。为什么?


总而言之,有没有一种方法可以让 TP 线程在构建后初始化对象时看到对象,而无法看到 I/O 所做的全部或部分更改thread什么时候调用了protected read方法?如果是这样,如果能给出一个详细的例子就好了。

否则,是否有一些副作用可能导致对象的字段值总是在其他线程中可见(例如I/O线程在添加时获取监视器处理程序对象到队列)? I/O 线程和 TP 线程都不会在处理程序对象本身上同步。队列也不做这样的事情(不管怎样,这没有意义)。这可能与具体的 JVM 实现细节有关吗?



编辑:

It follows from the above definitions that:

An unlock on a monitor happens-before every subsequent lock on that monitor. – Not applicable: monitor is not acquired on the handler object

A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field. – Not applicable: no volatile fields

A call to start() on a thread happens-before any actions in the started thread. – A TP thread might already exist when the queue with handler object(s) is submitted for execution. A new handler object might be added to queue amidst an execution on an existing TP thread.

All actions in a thread happen-before any other thread successfully returns from a join() on that thread. – Not applicable: threads do not wait for each other

The default initialization of any object happens-before any other actions (other than default-writes) of a program. – Not applicable: field writes are after default init AND after constructor finishes

When a program contains two conflicting accesses (§17.4.1) that are not ordered by a happens-before relationship, it is said to contain a data race.

Memory that can be shared between threads is called shared memory or heap memory.

All instance fields, static fields, and array elements are stored in heap memory. In this chapter, we use the term variable to refer to both fields and array elements.

Local variables (§14.4), formal method parameters (§8.4.1), and exception handler parameters (§14.20) are never shared between threads and are unaffected by the memory model.

Two accesses to (reads of or writes to) the same variable are said to be conflicting if at least one of the accesses is a write.

有一个写入没有在字段上强制 HB 关系,后来有一个读取,再次没有在这些字段上强制 HB 关系。还是我在这里错得离谱?也就是说,没有关于该对象的任何内容可能已更改的声明,那么为什么 JVM 会强制刷新这些字段的缓存值?


TL;DR

线程 #1 以不允许 JVM 知道这些值应该传播到其他线程的方式将值写入新对象的字段。

线程 #2 获取线程 #1 构造后修改的对象并读取这些字段值。

为什么 FinalFieldExample/JLS 17.5 中描述的问题在实践中从未发生过?

为什么线程 #2 永远不会只看到默认初始化的对象(或者,替代地,构造后的对象,但 before/in 线程 #1 更改了字段值的中间值)?

我很确定当线程池启动一个线程/运行一个可调用对象时,它具有 hapens-before 语义,因此 所有更改都发生在 happens-before 可用于线程。

当您有多个线程同时修改同一对象实例上的数据时,您在 CPJ 中提到的场景有效(例如,2 个线程已经 运行 并修改相同的值(或恰好是在堆中彼此相邻)。

在您的情况下,似乎没有并发的 modification/read 个字段。

实际上,您永远不会在此处看到违规的一个原因是“大多数 是非最终的”,这意味着至少有一个最终字段。 HotSpot 在涉及 final 字段时实现 JLS 提供的保证的方式是在构造函数的末尾放置一个内存屏障,从而为非 final 字段提供相同的可见性保证。

理论上现在显然没有必要,这意味着这取决于您如何在线程池中对额外工作进行排队。一般来说,我无法想象任何在排队工作时没有同步的设计,在它被执行之前 - 它不仅会使工作变得非常尴尬(出于同样的原因,启动线程调用发生在行为之前),实现这种数据结构的方式也需要一些同步。

例如,

Java 的 ThreadPoolExecutor.execute() 确实在内部使用了 BlockingQueue,它已经为您提供了所需的所有可见性和排序保证。

这可能取决于您使用的线程池类型。如果它是 ExecutorService,那么 class 对其任务做出了一些强有力的保证。 From the documentation:

Memory consistency effects: Actions in a thread prior to the submission of a Runnable or Callable task to an ExecutorService happen-before any actions taken by that task, which in turn happen-before the result is retrieved via Future.get().

因此,当您初始化任何对象以及任何其他对象,然后将该对象提交给 ExecutorService 时,所有这些写入都对最终将处理您的任务的线程可见。

现在,如果您自行部署了自己的线程池,或者您正在使用没有这些保证的线程池,那么所有的赌注都会落空。我会说切换到有保证的东西。