簡體   English   中英

多線程:線程操作同一 object 的不同字段

[英]Multithreading: Threads manipulating different fields of the same object

假設我有一個帶有兩個變量的 class X。

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

class Y

class Y {
    Integer y1;
    String y2;
}

比如說,我們有 4 個線程,T1、T2、T3 和 T4。

T1 在a上運行,T2 在b.y1上運行(類似x.getB().setY1() ),T3 在b.y2上運行,T4 在c上運行。

在所有線程執行之前,我不會在任何線程中讀取任何“最深”值( a, y1, y2, c )(但是,T2 和 T3 將執行x.getB() )。

我會面臨與多線程相關的任何典型問題嗎?

我的問題

  1. 我想我可能不會面臨關於 a 和 c 的任何競爭條件,因為它們不會被“他們的”線程以外的線程讀取。 這個推理對嗎?
  2. T2 和 T3 的x.getB()怎么樣?
  3. 多核環境中的處理器緩存怎么樣? 他們緩存整個 object 嗎? 還是他們只緩存他們修改的字段? 還是他們緩存整個內容但只更新他們更改的字段?
  4. 他們甚至能識別物體和領域嗎? 或者他們只是在 memory 塊上工作? 在那種情況下,Java 是否會告訴他們需要緩存的 memory 地址?

When the processors reconcile their cache with the main memory after the processing is done, 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?

例如,最初,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

因此,當寫回主 memory 時,假設 P1 先完成,然后更新主 memory。 所以,現在的值為a=2, c=1

現在,當 P4 完成時,它是否只更新c的值,因為它只修改了c 或者它只是用緩存中的值覆蓋主 memory ,使a=1, c=2

或者他們只是緩存他們將讀取或寫入的值,這意味着 T1 永遠不會緩存c的值,而 T4 永遠不會緩存a的值。

我會面臨與多線程相關的任何典型問題嗎?

你只是在閱讀,所以,當然不是。 你的問題甚至不相關。 Java Memory Model 描述了如何將字段更改傳播到其他線程。 這需要首先進行實際更改。

他們是只更新他們更改的 memory 塊,還是用他們緩存的整個 memory 塊覆蓋主 memory?

只有他們改變了什么。

多核環境中的處理器緩存怎么樣? 他們緩存整個 object 嗎? 還是他們只緩存他們修改的字段?

你的問題沒有意義。 您根本不能將對象放在字段或變量中,這是不可能的。 您唯一可以保留在字段/變量/參數中的是對對象的引用 String x = "foo"中,您沒有將 "foo" 放入x “foo”存在於堆的某個地方。 您確保它存在於堆中,並將對它的引用分配給x 這個引用相當簡單,通常是 64 位的,並且是原子的。

您可以在與更新相關的線程之間共享的唯一內容是字段 methods cannot be changed (you can't modify a method of an instance of something; java is not like python or javascript, you can't write someRef.flibbledyboo = thingie; where 'flibbledyboo' is something you just made up. Local variables (包括參數)不可能與其他線程共享; java 在所有事情上都是按值傳遞的,所以如果你這樣做,在一個方法中, someOtherMethod(variable); ,你傳遞一個副本,說明“如果我改變我的變量,而 someOtherMethod 將它交給另一個線程會發生什么?”無關緊要。

如果您創建 lambda 或本地 class,您似乎可以與線程共享本地 var,但 java 將拒絕編譯它,除非 var.final 或有效地拒絕編譯它。 如果它(有效地)是最終的,那么這一點是沒有意義的——它不能被改變,因此“如果一個線程更新這個值會發生什么,另一個線程什么時候看到更新”這個問題是無關緊要的。

因此:它只是關於 fields ,並且 fields 只能包含原始值或引用 參考是簡單的事情。 如果您熟悉 C,它們是指針,但這是一個臟字,所以 java 稱它們為引用。 波塔托,波塔托。 一樣。

任何字段(無論是原始字段還是引用)都可以由任何線程緩存,也可以不緩存,由經銷商選擇。 他們可以隨時將其“同步”回主 memory。 如果您的代碼根據此更改其執行方式,則您編寫了一個很難找到的錯誤。 盡量不要這樣做:)

他們甚至能識別物體和領域嗎? 或者他們只是在 memory 塊上工作?

根據前一點,這又是一個無意義的問題:您正在尋找的價值觀是:原語和參考。 這就是 JMM 的意義所在(而不是“內存塊”)。 對象不能在字段中。 僅供參考。 引用到 object,但 object 只是另一個字段。 一路向下都是田野。

想象線程 A 執行: foo.getX().setY()和線程 B 執行: foo.getX().getY() 假設foo永遠不會改變,那么大概foo.getX()也永遠不會改變。 這只是一個參考,和'。 是java-ese:跟隨它,在那里找到bag o'字段,然后對它們進行操作。 因此,兩個線程都找到了相同的 object 和它真正存在的 bag-o-fields。 現在線程 A 修改了它在那里找到的字段之一,而 B 正在讀取其中一個字段。 這是一個問題 - 這些是字段。 線程可以緩存它們,經銷商的選擇。 您需要建立 HB/HA 關系,或者您在這里寫了一個錯誤。

現在,當 P4 完成時,它是否只更新 c 的值,因為它只修改了 c? 或者它只是簡單地用緩存中的值覆蓋主 memory,使 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。 由於“覆蓋整個塊”樣式覆蓋,它不會 go 回到 1。

或者他們只是緩存他們將讀取或寫入的值,這意味着 T1 永遠不會緩存 c 的值,而 T4 永遠不會緩存 a 的值。

經銷商的選擇。 JMM 最好理解如下:

任何時候任何線程更新任何字段(並且值始終是原語或引用),它都會翻轉邪惡的硬幣。 如果翻轉正面朝上,它會在其本地緩存中更新此值,並且不會將其“分發”給與該字段交互的任何其他代碼,除非該線程具有已建立的 HBHA 規則。 在尾部,它確實更新了其他線程緩存的任意選擇。

每當一個線程讀取任何字段時,它都會再次翻轉硬幣。 在頭上,它只是繼續使用它的緩存。 在尾部它從中心值更新。

硬幣是邪惡的:它不是 50/50 的鏡頭。 事實上,今天,在你的筆記本電腦上,寫下這段代碼,它每次都落到尾巴上——即使你重新運行了 100 萬次測試。 在你的 CI 服務器上,同樣的交易。 尾巴。 然后在生產中 - 尾巴,每次。 然后下周那個重要的客戶進來,你要演示? 很多頭。

因此:

  • 很難檢測到您編寫的代碼的執行取決於 coinflip。
  • 然而,如果你編寫的代碼的執行依賴於翻轉,你就失敗了。 那是一個錯誤。

解決方案通常是以這種方式完全忘記線程,並在“引導”或預先和之后進行線程間通信。

渠道溝通

有更適合的通信渠道系統。 例如,數據庫:不要更新字段; 發送數據庫查詢。 使用事務和isolationlevel.SERIALIZABLE,以及支持RetryException 的框架(如JDBI 或JOOQ - 不要自己動手,不要直接使用JDBC)。 您可以對數據通道進行細粒度控制。

其他選項是消息總線,例如 rabbitmq。

前期/后期

使用像fork/join 和friends 之類的框架,或其他任何遵循map/reduce model 的框架。 他們設置了一些數據結構,然后才啟動您的線程(或者更確切地說,他們有一個 em 池,並將在池的一個線程中執行您的代碼,將數據結構交給您)。 您的代碼只是查看此數據結構,然后返回一些內容。 它根本不涉及其他領域。 該框架創建數據並集成您返回的內容。 然后信任框架; 他們可能沒有 memory model 錯誤。

我真的很想在多線程環境中修改線程。

老天,這里有龍。

如果必須,請查找'happens-before/happens-after':對於任何兩行代碼,如果存在 HB/HA 關系,(如根據 JMM 規則,保證一行發生在另一行之前),那么前一行導致的任何字段更新都將保證在后一車道可見 - 沒有邪惡的硬幣翻轉。

一個非常快速的概述:

  • 在一個線程中,任何后來執行的行都“發生在”之后。 這是顯而易見的——java 勢在必行。 您可以從代碼中觀察到,就好像一個線程中的每一行都在一個接一個地運行。
  • 同步:當您有 2 個線程時,一個線程命中由synchronized(X)保護的代碼塊,然后退出該塊,另一個線程稍后進入同一引用上由 synchronize 保護的塊,然后是線程的退出點A 保證“發生在” B 的入口點之前:無論 A 內部發生什么變化,你都會在 B 中看到,保證。
  • volatile - 類似的規則,但 volatile 很棘手。
  • 線程開始: someThread.start()與該線程中的代碼具有 HB/HA 關系。
  • 構造函數和最終字段的設置或多或少保證可以解決(字段設置“發生在”構造函數返回之前,即使您隨后將通過調用該構造函數獲得的 object 引用交給另一個沒有 HB/HA 保護的線程,並且他們不知何故得到了它,因為邪惡的硬幣落在了正面)。
  • class 加載器系統永遠不會在同一個 class 加載器中加載相同的 class 兩次。 這是制作安全單例的非常快速且簡單的方法。

如果某個代碼 X 更新了一個字段,而另一個代碼 Y 讀取了該字段,並且 X 和 Y 沒有 HB/HA 關系,那么您完全被淹沒了。 你寫了一個bug,測試起來會很困難,而且測試也不可靠。

你的問題涉及到一些有趣的話題。 我將嘗試重新表述您的問題並按順序回答。

關於你的第一個問題如果不同的線程只修改不同的對象,這會造成一致性問題嗎?

您需要區分修改 object(或“寫入”)和使此類更改對其他線程可見 在您提出的情況下,您的各個線程彼此獨立地處理各種對象,並且永遠不需要“讀取”其他對象。 所以是的,這很好。

但是,如果一個線程需要讀取可能已被另一個線程修改的變量的值,則需要引入一些同步,以便在第一個線程讀取它之前對該變量進行修改(同步塊/訪問 volatile變量/信號量等)。 我不能推薦足夠的這篇文章修復 Java Memory Model

關於你的第二個問題:

您的第一個問題的答案相同:只要沒有線程修改您的X實例的成員b ,就沒有理由擔心; 線程T2T3都將獲得相同的 object。

在您的第三個和第四個問題上,緩存一致性如何?

從程序員的角度來看,Java 虛擬機如何處理 memory 分配有點模糊。 你所關心的被稱為虛假分享。 Java 虛擬機將確保 memory 中存儲的內容與您的程序一致。 您不必擔心壞緩存會覆蓋另一個線程所做的更改。

但是,如果成員有足夠的爭用,您可能會面臨性能損失。 幸運的是,您可以通過對存在問題的成員使用@Contended注釋來向 Java 虛擬機指示它們應該分配到不同的緩存行上來減少這種影響。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM