簡體   English   中英

如何同步 Java 中的兩個線程

[英]How can I synchronize two threads in Java

我在 Java 中學習同步。 我知道這是一個非常基本的問題,但我不知道為什么在運行代碼“計數”后我不能得到 24000。

public class Example extends Thread {

    private static int count;

    public static void main(String[] args) throws InterruptedException {
        Example t1 = new Example();
        Example t2 = new Example();
        
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();
        
        System.out.println(count);
    }

    public void run() {
        for (int i = 0; i < 12000; i++) {
            addToCount();
        }
    }

    public synchronized void addToCount() {
        Example.count++;
    }
}

在單獨的對象上synchronized不會在它們之間進行協調

當我運行您的代碼時,我會得到諸如217882400020521之類的結果。

各種結果的原因是您的synchronized是對兩個單獨對象中的每一個的鎖定。 t1t2引用的對象都有自己的鎖( monitor )。 在每個線程的執行中,執行synchronized方法addToCount會獲取該特定線程的監視器,即特定的Example object。 所以有效地synchronized沒有效果。 您對synchronized關鍵字的使用不是在兩個對象之間進行協調,而是每個單獨的 object 中進行協調。

有關更多信息,請參閱下方Mark Rotteveel 的評論,並參閱 Oracle 同步方法教程。 並閱讀下面鏈接的 Brian Goetz 書。

++不是原子的

因此,在您的代碼中,使addToCount synchronized沒有任何目的。

您的兩個Example對象中的每一個都是一個單獨的線程,每個都訪問一個共享資源static int count變量。 每個都在獲取當前值,有時它們同時獲取和遞增相同的值。 例如,它們都可能是值 42,每個加一得到 43 的結果,並且每個都將 43 放入該變量中。

Java 中的++運算符不是atomic 在源代碼中,我們程序員將其視為單個操作。 但實際上是多次操作。 請參閱為什么 i++ 不是原子的? .

從概念上(不是字面意思),您可以想到您的Example.count++; 代碼為:

int x = Example.count ;  // Make a copy of the current value of `count`, and put that copy into `x` variable.
x = ( x + 1 ) ;          // Change the value of `x` to new incremented value.
Example.count = x ;      // Replace the value of `count` with value of `x`.

在執行 fetch-increment-replace 的多個步驟時,在任何時候操作都可能被掛起,因為該線程的執行會暫停以等待其他線程執行一段時間。 線程可能已完成第一步,獲取42的副本,然后在線程掛起時暫停。 在此暫停期間,其他線程可能會獲取相同的42值,將其增加到 43,然后替換回count 當第一個線程恢復時,已經抓取了 42,第一個線程也增加到 43 並存儲回count 第一個線程不知道第二個線程溜進去已經增加並存儲了 43。所以 43 最終被存儲了兩次,使用我們的兩個for循環。

這種巧合,每個線程都踩到另一個線程的腳趾,是不可預測的。 在此代碼的每次運行中,線程的調度可能會根據主機操作系統和JVM中的當前瞬時條件而有所不同。 如果我們的結果是21788 ,那么我們知道該運行經歷了 2,212 次碰撞 ( 24,000 - 21,788 = 2,212 )。 當我們的結果是 24,000 時,我們知道我們碰巧沒有這樣的碰撞,這完全是靠運氣。

你還有另一個問題。 (並發是棘手的。)繼續閱讀。

能見度問題

由於 CPU 體系結構,兩個線程可能會看到同一個static變量的不同值。 您需要研究Java Memory Model中的可見性

AtomicInteger

您可以通過使用Atomic…類來解決可見性和同步問題。 在這種情況下, AtomicInteger 這個 class 包裝了一個 integer 值,提供了一個線程安全的容器。

AtomicInteger字段標記為final以保證我們永遠只有一個AtomicInteger object,防止重新分配。

final private static AtomicInteger count = new AtomicInteger() ;

要執行加法,請調用諸如incrementAndGet之類的方法。 無需將您自己的方法標記為synchronized AtomicInteger為您處理。

public void  addToCount() {
    int newValue = Example.count.incrementAndGet() ; 
    System.out.println( "newValue " + newValue + " in thread " + Thread.currentThread().getId() + "." ) ;
}

使用這種代碼,兩個線程將相同的AtomicInteger object 遞增 12,000 次,結果為 24,000。

有關更多信息,請參閱這個類似的問題,為什么在 10 個 Java 線程中增加一個數字不會導致值 10? .

執行服務

您的代碼的另一個問題是,在現代 Java 中,我們通常不再直接處理Thread class。 相反,使用添加到 Java 5 的執行器框架。

使您的代碼變得棘手的部分原因在於它將線程管理(作為Thread的子類)與試圖完成工作的工蜂(遞增計數器)混合在一起。 這違反了通常會帶來更好設計的單一職責原則 通過使用執行器服務,我們可以分離兩個職責,線程管理與計數器遞增。

項目織機

已經在 Stack Overflow 的許多頁面上展示了使用 executor 服務。 因此,搜索以了解更多信息。 相反,如果Project Loom技術成為 Java 的一部分,我將展示更簡單的未來方法。 基於早期訪問Java 17的實驗版本現已推出

try-with-resources 語法等待提交的任務

在 Loom 中, ExecutorServiceAutoCloseable 這意味着我們可以使用try-with-resources語法。 只有在所有提交的任務都完成/失敗/取消后, try塊才會退出。 並且在退出try塊時,executor 服務會自動為我們關閉。

這是我們的Incremental class ,其中包含名為count的 static AtomicInteger class 包括增加原子 object 的方法。 而這個 class 是一個Runnable ,它有一個run方法來執行你的 12,000 個循環。

package work.basil.example;

import java.time.Instant;
import java.util.concurrent.atomic.AtomicInteger;

public class Incremental implements Runnable
{
    // Member fields
    static final public AtomicInteger count = new AtomicInteger();  // Make `public` for demonstration purposes (not in real work).

    public int addToCount ( )
    {
        return this.count.incrementAndGet();  // Returns the new incremented value stored as payload within our `AtomicInteger` wrapper.
    }

    @Override
    public void run ( )
    {
        for ( int i = 1 ; i <= 12_000 ; i++ )
        {
            int newValue = this.addToCount();
            System.out.println( "Thread " + Thread.currentThread().getId() + " incremented `count` to: " + newValue + " at " + Instant.now() );
        }
    }
}

並從一個main方法編寫代碼,以利用該 class。 我們通過Executors ExecutorService 然后在 try-with-resources 中,我們提交兩個Incremental實例,每個實例都在各自的線程中運行。

根據您最初的問題,我們仍然有兩個對象、兩個線程、每個線程中的一萬二千條增量命令,並且結果存儲在一個名為countstatic變量中。

// Exercise the `Incremental` class by running two instances, each in its own thread.
System.out.println( "INFO - `main` starting the demo. " + Instant.now() );
Incremental incremental = new Incremental();
try (
        ExecutorService executorService = Executors.newVirtualThreadExecutor() ;
)
{
    executorService.submit( new Incremental() );
    executorService.submit( new Incremental() );
}

System.out.println( "INFO - At this point all submitted tasks are done/failed/canceled, and executor service is shutting down. " + Instant.now() );
System.out.println( "DEBUG - Incremental.count.get()  = " + Incremental.count.get() );  // Access the static `AtomicInteger` object.
System.out.println( "INFO - `main` ending. " + Instant.now() );

運行時,您的 output 可能如下所示:

INFO - `main` starting the demo. 2021-02-10T22:38:06.235503Z
Thread 14 incremented `count` to: 2 at 2021-02-10T22:38:06.258267Z
Thread 14 incremented `count` to: 3 at 2021-02-10T22:38:06.274143Z
Thread 14 incremented `count` to: 4 at 2021-02-10T22:38:06.274349Z
Thread 14 incremented `count` to: 5 at 2021-02-10T22:38:06.274551Z
Thread 14 incremented `count` to: 6 at 2021-02-10T22:38:06.274714Z
Thread 16 incremented `count` to: 1 at 2021-02-10T22:38:06.258267Z
Thread 16 incremented `count` to: 8 at 2021-02-10T22:38:06.274916Z
Thread 16 incremented `count` to: 9 at 2021-02-10T22:38:06.274992Z
Thread 16 incremented `count` to: 10 at 2021-02-10T22:38:06.275061Z
…
Thread 14 incremented `count` to: 23998 at 2021-02-10T22:38:06.667193Z
Thread 14 incremented `count` to: 23999 at 2021-02-10T22:38:06.667197Z
Thread 14 incremented `count` to: 24000 at 2021-02-10T22:38:06.667204Z
INFO - At this point all submitted tasks are done/failed/canceled, and executor service is shutting down. 2021-02-10T22:38:06.667489Z
DEBUG - Incremental.count.get()  = 24000
INFO - `main` ending. 2021-02-10T22:38:06.669359Z

閱讀 Brian Goetz 等人的優秀經典書籍Java Concurrency in Practice

正如其他人所暗示的那樣,原因與您將方法聲明為同步時實際同步的內容有關

  • 如果方法是static ,那么您正在同步class 一次只能有一個線程可以進入該方法。
  • 如果方法不是 static,那么您正在同步 class 的單個實例 多個線程可以在 class 的不同實例上同時調用相同的方法(但不能在同一個實例上)。 由於在您的情況下每個線程都有自己的實例,因此它們可以同時調用該方法,每個線程都在其單獨的實例上。

所以解決方案基本上是一致的:要么(a)有一個共享的 static 變量(如你所見),然后制作方法 static; 或者(b),讓每個單獨的實例都有自己的變量,然后在操作結束時對變量求和以獲得總計數。 (作為 (b) 的變體,您還可以有多個線程引用和訪問同一個實例。)

暫無
暫無

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

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