多线程:线程操作同一对象的不同字段

Multithreading: Threads manipulating different fields of the same object

假设我有一个带有两个变量的 class X。

class X {
    Integer a;
    Y b;
    Integer c;
}

classY

class Y {
    Integer y1;
    String y2;
}

比如说,我们有 4 个线程,T1、T2、T3 和 T4。

T1 在 a 上运行,T2 在 b.y1 上运行(类似于 x.getB().setY1()),T3 在 b.y2 上运行,T4 在 [=16= 上运行].

我不会在任何线程中读取任何“最深”值 (a, y1, y2, c),直到所有这些值都被执行(但是 T2 和 T3 会执行 x.getB())。

我会遇到与多线程相关的任何典型问题吗?

我的问题

  1. 我想我可能不会遇到关于 a 和 c 的任何竞争条件,因为它们不会被“它们的”线程以外的线程读取。这个推理对吗?
  2. T2 和 T3 x.getB() 怎么样?
  3. 在多核环境中处理器缓存怎么样?他们缓存整个对象吗?或者他们只缓存他们修改的字段?还是他们会缓存整个内容但只更新他们更改的字段?
  4. 他们甚至能识别物体和场吗?或者他们只是在内存块上工作?在那种情况下,Java 会告诉他们需要缓存的内存地址吗?

当处理器在处理完成后将缓存与主内存进行协调时,它们是只更新它们更改的内存块,还是用它们缓存的整个内存块覆盖主内存?

例如,假设最初,a 和 c 都具有值 a = 1c = 1。 P1 和 P4 缓存这些值(a=1c=1)。 T1 将其更改为 a = 2,T4 将其更改为 c = 2.

现在缓存C1中的值为a=2, c=1;在 C2 中,a=1, c=2.

所以,写回主存的时候,说P1先写完,再更新主存。所以,现在的值为 a=2, c=1.

现在,当 P4 完成时,它是否只更新 c 的值,因为它只修改了 c?或者它只是用缓存中的值覆盖主内存,使 a=1, c=2?

或者他们只是缓存他们将读取或写入的值,这意味着 T1 永远不会缓存 c 的值,而 T4 永远不会缓存 a.[=34= 的值]

Would I face any of the typical issues associated with multithreading?

你只是在看书,所以,当然不是。你的问题甚至不相关。 Java 内存模型描述了如何将对字段的更改传播到其他线程。这需要首先进行实际更改。

do they only update the chunk of memory they changed, or do they overwrite the main memory with the entire block of memory that they cached?

只有他们改变了什么。

What about caching by processors in a multi-core environment? Do they cache the entire object? Or do they cache only the field that they modify?

你的问题毫无意义。您根本不能将 object 放在字段或变量中,那是不可能的。您唯一可以坚持 fields/variables/parameters 的是 references 到 objects。在 String x = "foo" 中,您没有在 x 中输入“foo”。 “foo”存在于堆中的某个地方。您确保它存在于堆中,并将对它的引用分配给 x。这个引用相当简单,通常是 64 位的,并且是原子的。

只有 可以在与更新相关的线程之间共享的东西是字段。方法不能改变(你不能修改某个实例的方法;java 不像 python 或 javascript,你不能写 someRef.flibbledyboo = thingie; where 'flibbledyboo' 是你刚刚编造的东西。局部变量(包括参数)不可能与其他线程共享;java 在所有情况下都是 pass-by-value,所以如果你这样做,在方法内部, someOtherMethod(variable);, 你传递的是一份副本,使 'what happens if I change my variable, and someOtherMethod hands it to another thread?' 的观点变得无关紧要。

如果创建 lambda 或本地 class,您似乎可以与线程共享本地 var,但是 java 将拒绝编译它,除非 var 是最终的,或者实际上是最终的。如果它(实际上)是最终的,那么这一点就没有意义了——它不能改变,因此问题 'what happens if one thread updates this value, when does the other thread see the update' 是无关紧要的。

因此:它只是关于字段,字段只能包含原始值,或引用。引用是简单的东西。如果您熟悉 C,它们就是指针,但这是一个肮脏的词,所以 java 称它们为引用。土豆,土豆。同样的事情。

任何字段(无论是原始字段还是引用字段)都可以由任何线程缓存,或者不缓存,由经销商选择。他们可以随时 'sync' 将其返回主内存。如果你的代码根据这个改变了它的执行方式,你就写了一个很难找到的错误。尽量不要这样做:)

Do they even recognise objects and fields? Or do they just work on chunks of memory?

又是一个 non-sensical 问题,根据前一点:这是您正在寻找的值:基元和参考。这就是 JMM 的意义所在(而不是 'chunks of memory')。 objects 不能在字段中。仅供参考。引用指向 object,但 object 只是另一个字段。一路向下都是田野。

假设线程 A 执行:foo.getX().setY(),线程 B 执行:foo.getX().getY()。假设 foo 永远不会改变,那么大概 foo.getX() 也永远不会改变。这只是一个参考,'.' is java-ese for: 跟随它,找到 bag o' fields 并对其进行操作。因此,两个线程都找到了相同的 object 和 bag-o-fields 。现在线程 A 已经修改了它在那里找到的一个字段,而 B 正在读取其中一个字段。这是一个问题——那些是领域。线程可以缓存它们,由经销商选择。您需要建立 HB/HA 关系,否则您在这里写了一个错误。

Now, when P4 finishes, does it update the value of only c, because it has modified only c? Or does it simply overwrite the main memory with the value in its cache, making a=1, c=2?

否;但这似乎并不特别相关。没有 HBHA (Happens before/after) 关系的无关线程可以合法地观察到 a=1/c=1、a=2/c=1 或 a=1/c=2。然而,如果他们以某种方式观察到 a=2/c=1,那么之后他们将继续观察 a=2。由于 'overwrite entire block' 样式覆盖,它不会回到 1。

Or do they simply cache the values they will read or write, meaning T1 will never cache the value of c, and T4 will never cache the value of a.

经销商的选择。 JMM最好这样理解:

任何时候任何线程更新任何字段(并且值是 总是 原语或引用),它会掷出邪恶的硬币。如果翻转着陆,它会在其本地缓存中更新此值,并且不会 'distribute' 将此值发送给与此字段交互的任何其他代码,除非该线程已建立 HBHA 规则。在尾巴上,它确实会更新任意选择的其他线程的缓存。

每当线程读取任何字段时,它都会再次掷硬币。在头脑中,它只是继续其缓存。在尾部,它从中心值更新。

硬币是邪恶的:这不是50/50的镜头。事实上,今天,在你的笔记本电脑上,写出这段代码,它每次都会失败——即使你重新运行了 100 万次测试。在您的 CI 服务器上,同样的交易。尾巴。然后在生产中 - 每次都是尾巴。然后下周那个重要的客户来了,你要进行演示?很多人头。

因此:

  • 很难检测到您编写的代码的执行取决于投币。
  • 然而,如果您编写的代码的执行取决于翻转,那么您就失败了。这是一个错误。

解决方案通常是完全忘记这种方式的线程,并在 'channeled' 或 up-front 之后进行 inter-thread 通信。

渠道沟通

有更适合的通信渠道系统。例如,数据库:不要更新字段;发送数据库查询。使用事务和 isolationlevel.SERIALIZABLE,支持 RetryException框架(如 JDBI 或 JOOQ - 不要自己滚动,不要直接使用 JDBC)。您可以对数据通道进行细粒度控制。

其他选项是像 rabbitmq 这样的消息总线。

up-front / 之后

使用 fork/join 和朋友之类的框架,或任何其他框架,例如遵循 map/reduce 模型。他们设置了一些数据结构,然后才启动你的线程(或者更确切地说,他们有一个 em 池,并将在池的一个线程中执行你的代码,将数据结构交给你)。您的代码只是查看此数据结构,然后 returns 东西。它根本不涉及其他领域。该框架创建数据并整合您 return。然后信任框架;他们可能没有内存模型错误。

我很想在多线程环境下修改线程

大人,龙来了

如果必须,请查找 'happens-before/happens-after':对于任意 2 行代码,如果存在 HB/HA 关系,(例如,根据 JMM 规则,保证其中一行具有发生在另一条之前),那么前面一行引起的任何字段更新都将保证后面的车道可见 - 没有邪恶的硬币翻转。

快速概览:

  • 在一个线程中,任何稍后执行的行 'happens after'。这是显而易见的 - java 是必要的。您可以从代码中观察到,好像一个线程中的每一行都一个接一个地运行。
  • synchronized:当你有 2 个线程时,一个线程命中由 synchronized(X) 保护的代码块,然后退出该块,另一个线程稍后进入由同一个引用上的同步保护的代码块,则线程 A 的出口点保证 'happen before' B 的入口点:无论 A 内部发生什么变化,您都会在 B 中看到,保证。
  • volatile - 类似的规则,但 volatile 比较棘手。
  • 线程开始:someThread.start() 与该线程中的代码有 HB/HA 关系。
  • 最终字段的构造函数和设置或多或少地保证可以解决(字段设置 'happens before' 构造函数 returns,即使您随后提交 object ref你通过在没有 HB/HA 保护的情况下将构造函数调用到另一个线程,他们以某种方式得到了它,因为邪恶的硬币正面朝上。
  • class 加载器系统永远不会在同一个 class 加载器中加载相同的 class 两次。这是制作安全单例的非常快速且简单的方法。

如果某些代码 X 更新了一个字段,而其他一些代码 Y 读取了该字段,并且 X 和 Y 没有 HB/HA 关系,那么您就完蛋了。你写了一个bug,测试起来会很困难,而且测试不可靠。

你的问题涉及到一些有趣的话题。我会尝试重新表述您的问题并按顺序回答。

关于你的第一个问题如果不同的线程只修改不同的对象,这会造成一致性问题吗?

您需要区分修改对象(或“写入”)和对其他线程进行此类更改可见。在您呈现的情况下,您的各种线程彼此独立地处理各种对象,并且永远不需要“读取”其他对象。所以是的,这很好。

但是,如果一个线程需要读取可能已被另一个线程修改的变量的值,则需要引入一些同步,以便对该变量的修改发生在[=41之前=] 第一个线程读取它(同步块/访问 volatile 变量/信号量等)。我怎么推荐这篇文章都不为过 Fixing the Java Memory Model.

关于你的第二个问题:

与您的第一个问题的答案相同:只要没有线程修改您的 X 实例的成员 b,就无需担心;线程 T2T3 都将获得相同的对象。

关于你的第三个和第四个问题缓存一致性怎么样?

Java 虚拟机如何处理内存分配从程序员的角度来看有点晦涩难懂。你关心的是所谓的虚假分享。 Java 虚拟机将确保内存中存储的内容与您的程序一致。您无需担心错误的缓存会覆盖另一个线程所做的更改。

但是,如果成员上有足够多的争用,您可能会面临性能损失。幸运的是,您可以通过对提出问题的成员使用 @Contended 注释来减少这种影响,以向 Java 虚拟机指示它们应该分配在不同的缓存行上。