CodeGym /Java Blog /Toto sisi /更好的結合:Java 和 Thread 類。第三部分——互動
John Squirrels
等級 41
San Francisco

更好的結合:Java 和 Thread 類。第三部分——互動

在 Toto sisi 群組發布
簡要概述線程如何交互的細節。之前,我們研究了線程是如何相互同步的。這次我們將深入探討線程交互時可能出現的問題,並討論如何避免這些問題。我們還將提供一些有用的鏈接以供更深入的研究。 更好的結合:Java 和 Thread 類。 第三部分——互動 - 1

介紹

所以,我們知道Java有線程。您可以在標題為Better together:Java 和 Thread 類的評論中閱讀相關內容。第一部分 — 執行線程我們在標題為Better together:Java 和 Thread 類的評論中探討了線程可以相互同步這一事實。第二部分 — 同步。是時候討論線程如何相互交互了。他們如何共享共享資源?這裡可能會出現什麼問題? 更好的結合:Java 和 Thread 類。 第三部分——互動 - 2

僵局

最可怕的問題是死鎖。死鎖是指兩個或多個線程永遠在等待另一個線程。我們將從描述死鎖的 Oracle 網頁中獲取示例:

public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s bowed to me!%n",
                    this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s bowed back to me!%n",
                    this.name, bower.getName());
        }
    }

    public static void main(String[] args) {
        final Friend alphonse = new Friend("Alphonse");
        final Friend gaston = new Friend("Gaston");
        new Thread(() -> alphonse.bow(gaston)).start();
        new Thread(() -> gaston.bow(alphonse)).start();
    }
}
第一次可能不會在這裡發生死鎖,但是如果您的程序確實掛起,那麼就該運行了jvisualvm更好的結合:Java 和 Thread 類。 第三部分——互動 - 3安裝了 JVisualVM 插件(通過工具 -> 插件),我們可以看到死鎖發生的位置:

"Thread-1" - Thread t@12
   java.lang.Thread.State: BLOCKED
	at Deadlock$Friend.bowBack(Deadlock.java:16)
	- waiting to lock <33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
線程 1 正在等待線程 0 的鎖。為什麼會發生這種情況?Thread-1開始運行並執行該Friend#bow方法。它被標記為關鍵字,這意味著我們正在為(當前對象)synchronized獲取監視器。this該方法的輸入是對其他對象的引用Friend。現在,Thread-1想要在另一個上執行該方法Friend,並且必須獲取它的鎖才能這樣做。但是如果另一個線程(在本例中Thread-0)設法進入該bow()方法,那麼鎖已經被獲取並Thread-1等待Thread-0,反之亦然。這是無法解決的僵局,我們稱之為僵局。死鎖就像一個無法鬆開的死神之握,是一種無法打破的相互阻礙。關於死鎖的另一種解釋,可以看這個視頻:死鎖和活鎖詳解

活鎖

如果有死鎖,是否也有活鎖?是的,有 :) 活鎖發生在線程表面上看起來還活著,但它們無法做任何事情時,因為它們繼續工作所需的條件無法滿足。基本上,活鎖類似於死鎖,但線程不會“掛起”等待監視器。相反,他們永遠在做某事。例如:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class App {
    public static final String ANSI_BLUE = "\u001B[34m";
    public static final String ANSI_PURPLE = "\u001B[35m";
    
    public static void log(String text) {
        String name = Thread.currentThread().getName(); // Like "Thread-1" or "Thread-0"
        String color = ANSI_BLUE;
        int val = Integer.valueOf(name.substring(name.lastIndexOf("-") + 1)) + 1;
        if (val != 0) {
            color = ANSI_PURPLE;
        }
        System.out.println(color + name + ": " + text + color);
        try {
            System.out.println(color + name + ": wait for " + val + " sec" + color);
            Thread.currentThread().sleep(val * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Lock first = new ReentrantLock();
        Lock second = new ReentrantLock();

        Runnable locker = () -> {
            boolean firstLocked = false;
            boolean secondLocked = false;
            try {
                while (!firstLocked || !secondLocked) {
                    firstLocked = first.tryLock(100, TimeUnit.MILLISECONDS);
                    log("First Locked: " + firstLocked);
                    secondLocked = second.tryLock(100, TimeUnit.MILLISECONDS);
                    log("Second Locked: " + secondLocked);
                }
                first.unlock();
                second.unlock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        new Thread(locker).start();
        new Thread(locker).start();
    }
}
此代碼的成功取決於 Java 線程調度程序啟動線程的順序。如果Thead-1先開始,那麼我們會得到活鎖:

Thread-1: First Locked: true
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
Thread-0: Second Locked: true
Thread-0: wait for 1 sec
Thread-1: Second Locked: false
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
...
從示例中可以看出,兩個線程依次嘗試獲取兩個鎖,但都失敗了。但是,他們並沒有陷入僵局。從表面上看,一切都很好,他們正在做自己的工作。 更好的結合:Java 和 Thread 類。 第三部分——互動 - 4根據 JVisualVM,我們看到了休眠期和停放期(這是當線程試圖獲取鎖時——它進入停放狀態,正如我們之前討論線程同步時所討論的。您可以在此處查看活鎖示例:Java - 線程活鎖

飢餓

除了死鎖和活鎖,多線程還有一個問題:飢餓。這種現像不同於以前的阻塞形式,因為線程沒有被阻塞——它們只是沒有足夠的資源。結果,雖然一些線程佔用了所有執行時間,但其他線程無法運行: 更好的結合:Java 和 Thread 類。 第三部分——互動 - 5

https://www.logicbig.com/

你可以在這裡看到一個超級例子:Java - Thread Starvation and Fairness。此示例顯示線程在飢餓期間會發生什麼,以及從 到 的一個小更改如何Thread.sleep()Thread.wait()您平均分配負載。 更好的結合:Java 和 Thread 類。 第三部分——互動 - 6

競爭條件

在多線程中,存在“競爭條件”這樣的東西。當線程共享資源時會發生這種現象,但代碼的編寫方式無法確保正確共享。看一個例子:

public class App {
    public static int value = 0;

    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                int oldValue = value;
                int newValue = ++value;
                if (oldValue + 1 != newValue) {
                    throw new IllegalStateException(oldValue + " + 1 = " + newValue);
                }
            }
        };
        new Thread(task).start();
        new Thread(task).start();
        new Thread(task).start();
    }
}
此代碼第一次可能不會產生錯誤。當它出現時,它可能看起來像這樣:

Exception in thread "Thread-1" java.lang.IllegalStateException: 7899 + 1 = 7901
	at App.lambda$main$0(App.java:13)
	at java.lang.Thread.run(Thread.java:745)
如您所見,在newValue賦值時出了點問題。newValue太大了。value由於競爭條件,其中一個線程設法更改了兩個語句之間的變量。事實證明,線程之間存在競爭。現在想想不在貨幣交易中犯類似的錯誤是多麼重要......示例和圖表也可以在這裡看到:Code to simulate race condition in Java thread

易揮發的

說到線程的交互,這個volatile關鍵詞值得一提。讓我們看一個簡單的例子:

public class App {
    public static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Runnable whileFlagFalse = () -> {
            while(!flag) {
            }
            System.out.println("Flag is now TRUE");
        };

        new Thread(whileFlagFalse).start();
        Thread.sleep(1000);
        flag = true;
    }
}
最有趣的是,這很可能不起作用。新線程將看不到字段中的更改flag。要為該字段修復此問題flag,我們需要使用關鍵字volatile。如何以及為什麼?處理器執行所有操作。但是計算結果必須存儲在某個地方。為此,有主內存和處理器的緩存。處理器的高速緩存就像一小塊內存,用於比訪問主內存更快地訪問數據。但是任何事情都有一個缺點:緩存中的數據可能不是最新的(如上例中,標誌字段的值未更新時)。所以volatile關鍵字告訴 JVM 我們不想緩存我們的變量。這允許在所有線程上看到最新的結果。這是一個高度簡化的解釋。至於volatile關鍵字,我強烈建議您閱讀這篇文章。有關更多信息,我還建議您閱讀Java 內存模型Java Volatile 關鍵字。此外,重要的是要記住這volatile是關於可見性,而不是關於更改的原子性。查看“競爭條件”部分中的代碼,我們將在 IntelliJ IDEA 中看到一個工具提示: 此檢查已作為IDEA-61117更好的結合:Java 和 Thread 類。 第三部分——互動 - 7問題的一部分添加到 IntelliJ IDEA 中,該問題在2010 年的 發行說明中列出。

原子性

原子操作是不可分割的操作。例如,給變量賦值的操作必須是原子的。不幸的是,遞增操作不是原子的,因為遞增需要多達三個 CPU 操作:獲取舊值,對其加一,然後保存該值。為什麼原子性很重要?隨著增量操作,如果出現競爭條件,那麼共享資源(即共享值)可能隨時突然改變。此外,涉及 64 位結構的操作(例如longdouble)不是原子操作。可以在此處閱讀更多詳細信息:讀寫 64 位值時確保原子性。與原子性相關的問題可以在這個例子中看到:

public class App {
    public static int value = 0;
    public static AtomicInteger atomic = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                value++;
                atomic.incrementAndGet();
            }
        };
        for (int i = 0; i < 3; i++) {
            new Thread(task).start();
        }
        Thread.sleep(300);
        System.out.println(value);
        System.out.println(atomic.get());
    }
}
特殊AtomicInteger班總是給我們30000,但是value會時不時的變化。本主題有一個簡短的概述:Introduction to Atomic Variables in Java。“比較和交換”算法是原子類的核心。您可以在無鎖算法的比較 - JDK 7 和 8 示例中的 CAS 和 FAA或維基百科上的比較和交換文章中閱讀更多相關信息。 更好的結合:Java 和 Thread 類。 第三部分——互動 - 9

http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html

發生在之前

有一個有趣而神秘的概念叫做“發生在之前”。作為學習線程的一部分,您應該閱讀它。happens-before 關係顯示線程之間的操作將被看到的順序。有許多解釋和評論。這是關於此主題的最新演示文稿之一:Java“之前發生”的關係

概括

在這篇評論中,我們探討了線程如何交互的一些細節。我們討論了可能出現的問題,以及識別和消除這些問題的方法。有關該主題的其他材料清單: 更好的結合:Java 和 Thread 類。第 I 部分 — 執行的線程 更好地結合:Java 和 Thread 類。第二部分 — 同步 更好地結合:Java 和 Thread 類。第 IV 部分 — Callable、Future 和朋友 更好地結合在一起:Java 和 Thread 類。第五部分 — Executor、ThreadPool、Fork/Join 更好地結合在一起:Java 和 Thread 類。第六部分——開火!
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION