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

afk*_*min 5 java multithreading java-memory-model jls

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

\n\n
\n\n

该应用程序使用 NIO,因此 I/O 线程数量有限(它们只执行网络 I/O,不执行其他操作;它们永远不会终止,但可能会被阻塞以等待更多工作)。

\n\n

ClientCon每个连接在内部都表示为特定类型的对象,在本示例中我们称其为特定类型的对象。ClientCon有各种与会话相关的字段,这些字段都不是易失性的。不存在与获取/设置这些字段的值相关的任何类型的同步。

\n\n

接收到的数据由具有固定最大大小的逻辑单元组成。每个这样的单元都有一些允许决定处理类型(类)的元数据。一旦完成,就会创建该类型的新对象。所有此类处理程序都有字段,但这些字段都不是易失性的。然后,I/O 线程(为每个线程分配一个具体的 I/O 线程ClientConprotected在新处理程序对象上调用剩余缓冲区内容(在读取元数据之后)的 read 方法。

\n\n

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

\n\n

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

\n\n

处理程序的运行方法可以基于会话特定字段进行操作,ClientCon也可以设置它们和/或对处理程序对象自己的字段进行操作,这些字段的值在读取方法中设置。

\n\n
\n\n

根据 CPJ(Java 并发编程:设计和原理):

\n\n
\n

线程第一次访问对象的字段时,它会看到该字段的初始值或自其他线程写入以来的值。

\n
\n\n

该引用的更全面的示例可以在JLS 17.5中找到:

\n\n
class FinalFieldExample { \n    final int x;\n    int y; \n    static FinalFieldExample f;\n\n    public FinalFieldExample() {\n        x = 3; \n        y = 4; \n    } \n\n    static void writer() {\n        f = new FinalFieldExample();\n    } \n\n    static void reader() {\n        if (f != null) {\n            int i = f.x;  // guaranteed to see 3  \n            int j = f.y;  // could see 0\n        } \n    } \n}\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n

FinalFieldExample 类有一个final int 字段x 和一个非final\n int 字段y。一个线程可能执行方法编写器,另一个线程可能执行方法读取器。

\n\n

因为 writer 方法在对象的构造函数完成后写入 f,所以reader 方法将保证看到 fx 的正确初始化值:它将读取值 3。但是,fy 不是最终的;因此,不能保证 reader 方法看到\n 值 4。

\n
\n\n
\n\n

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

\n\n
\n\n

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

\n\n

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

\n\n
\n\n
\n\n

编辑:

\n\n
\n

从上述定义可以得出:

\n\n

监视器上的解锁发生在该监视器上的每个后续锁定之前。\xe2\x80\x93 不适用:处理程序对象上未获取监视器

\n\n

对易失性字段 (\xc2\xa78.3.1.4) 的写入发生在该字段的每次后续读取之前。\xe2\x80\x93 不适用:无易失性字段

\n\n

线程上对 start() 的调用发生在已启动线程中的任何操作之前。\xe2\x80\x93 当带有处理程序对象的队列被提交执行时,TP 线程可能已经存在。新的处理程序对象可能会在现有 TP 线程上的执行过程中添加到队列中。

\n\n

线程中的所有操作都发生在任何其他线程从该线程上的 join() 成功返回之前。\xe2\x80\x93 不适用:线程不互相等待

\n\n

任何对象的默认初始化发生在程序的任何其他操作(默认写入除外)之前。\xe2\x80\x93 不适用:字段写入在默认初始化之后和构造函数完成之后进行

\n\n

当程序包含两个不按先发生关系排序的冲突访问 (\xc2\xa717.4.1) 时,称其包含数据争用。

\n
\n\n

\n\n
\n

可以在线程之间共享的内存称为共享内存或堆内存。

\n\n

所有实例字段、静态字段和数组元素都存储在堆内存中。在本章中,我们使用术语“变量”来指代字段和数组元素。

\n\n

局部变量 (\xc2\xa714.4)、形式方法参数 (\xc2\xa78.4.1) 和\n 异常处理程序参数 (\xc2\xa714.20) 永远不会在线程之间共享\n 并且不受内存影响模型。

\n\n

如果至少其中一次访问是写入,则对同一变量的两次访问(读取或写入)被认为是冲突的。

\n
\n\n

进行了一次写入,但未在字段上强制建立 HB 关系,随后再次进行读取,未在这些字段上强制建立 HB 关系。或者我在这里大错特错了?也就是说,没有声明该对象的任何内容都可能发生更改,那么为什么 JVM 会强制刷新这些字段可能缓存的值呢?

\n\n
\n\n

长话短说

\n\n

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

\n\n

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

\n\n

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

\n\n

为什么线程 #2 永远不会只看到默认初始化的对象(或者,也可以看到构造后的对象,但在线程 #1 更改字段值之前/中间)?

\n

mar*_*ace 1

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

内存一致性影响:将 Runnable 或 Callable 任务提交给 ExecutorService 之前线程中的操作发生在该任务执行的任何操作之前,而该操作又发生在通过 Future.get() 检索结果之前。

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

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