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

更好的結合:Java 和 Thread 類。第二部分 — 同步

在 Toto sisi 群組發布

介紹

所以,我們知道Java有線程。您可以在標題為Better together:Java 和 Thread 類的評論中閱讀相關內容。第一部分 — 執行線程。線程是並行執行工作所必需的。這使得線程很可能以某種方式相互交互。讓我們看看這是如何發生的以及我們擁有哪些基本工具。 更好的結合:Java 和 Thread 類。 第二部分 — 同步 - 1

屈服

Thread.yield()令人費解且很少使用。它在 Internet 上以多種不同的方式描述。包括一些人寫道,有一些線程隊列,其中一個線程將根據線程優先級下降。其他人寫道,線程會將其狀態從“Running”更改為“Runnable”(儘管這些狀態之間沒有區別,即 Java 不區分它們)。現實情況是,它的知名度要低得多,但在某種意義上卻更簡單。 更好的結合:Java 和 Thread 類。 第二部分 — 同步 - 2該方法的文檔記錄了一個錯誤 ( JDK-6416721: (spec thread) Fix Thread.yield() javadoc ) yield()。如果你閱讀它,很明顯yield()方法實際上只是向 Java 線程調度器提供了一些建議,可以給這個線程更少的執行時間。但是實際發生了什麼,即調度程序是否根據建議採取行動以及它通常做什麼,取決於 JVM 的實現和操作系統。它也可能取決於其他一些因素。所有的混淆很可能是由於隨著 Java 語言的發展而重新考慮多線程這一事實。在此處的概述中閱讀更多內容:Java Thread.yield() 簡介

睡覺

線程可以在其執行期間進入休眠狀態。這是與其他線程交互的最簡單類型。運行我們Java代碼的Java虛擬機的操作系統有自己的線程調度器。它決定啟動哪個線程以及何時啟動。程序員不能直接從 Java 代碼與這個調度程序交互,只能通過 JVM。他或她可以要求調度程序暫停線程一段時間,即讓它休眠。您可以在這些文章中閱讀更多內容:Thread.sleep()多線程的工作原理。您還可以查看線程在 Windows 操作系統中的工作方式:Internals of Windows Thread。現在讓我們親眼看看。將以下代碼保存在名為的文件中HelloWorldApp.java

class HelloWorldApp {
    public static void main(String []args) {
        Runnable task = () -> {
            try {
                int secToWait = 1000 * 60;
                Thread.currentThread().sleep(secToWait);
                System.out.println("Woke up");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        Thread thread = new Thread(task);
        thread.start();
    }
}
如您所見,我們有一些任務等待 60 秒,然後程序結束。我們使用命令“ javac HelloWorldApp.java”進行編譯,然後使用“”運行程序java HelloWorldApp。最好在單獨的窗口中啟動該程序。例如,在 Windows 上,它是這樣的:start java HelloWorldApp。我們使用jps命令獲取PID(進程ID),我們用"打開線程列表jvisualvm --openpid pid更好的結合:Java 和 Thread 類。 第二部分 — 同步 - 3可以看到,我們的線程現在處於“Sleeping”狀態。其實還有一個更優雅的方法可以幫助我們的線程有美夢:

try {
	TimeUnit.SECONDS.sleep(60);
	System.out.println("Woke up");
} catch (InterruptedException e) {
	e.printStackTrace();
}
你有沒有註意到我們InterruptedException到處都在處理?讓我們理解為什麼。

線程中斷()

問題是當一個線程正在等待/休眠時,有人可能想要中斷。在這種情況下,我們處理一個InterruptedException. 這個機制是在Thread.stop()方法被聲明為 Deprecated 之後創建的,即過時的和不可取的。原因是當stop()方法被調用時,線程被簡單地“殺死”了,這是非常不可預測的。我們無法知道線程何時停止,也無法保證數據的一致性。想像一下,當線程被終止時,您正在將數據寫入文件。Java 的創建者沒有殺死線程,而是決定告訴它應該被中斷會更合乎邏輯。如何響應這些信息是線程自己決定的事情。有關詳細信息,請閱讀為什麼不推薦使用 Thread.stop?在甲骨文的網站上。讓我們看一個例子:

public static void main(String []args) {
	Runnable task = () -> {
		try {
			TimeUnit.SECONDS.sleep(60);
		} catch (InterruptedException e) {
			System.out.println("Interrupted");
		}
	};
	Thread thread = new Thread(task);
	thread.start();
	thread.interrupt();
}
在這個例子中,我們不會等待 60 秒。相反,我們會立即顯示“Interrupted”。這是因為我們調用了interrupt()線程上的方法。此方法設置一個稱為“中斷狀態”的內部標誌。也就是說,每個線程都有一個不可直接訪問的內部標誌。但是我們有與這個標誌交互的本地方法。但這不是唯一的方法。一個線程可能正在運行,而不是等待什麼,只是執行操作。但它可能預料到其他人會希望在特定時間結束其工作。例如:

public static void main(String []args) {
	Runnable task = () -> {
		while(!Thread.currentThread().isInterrupted()) {
			// Do some work
		}
		System.out.println("Finished");
	};
	Thread thread = new Thread(task);
	thread.start();
	thread.interrupt();
}
在上面的例子中,while循環會一直執行到線程被外部中斷為止。至於isInterrupted標誌,重要的是要知道,如果我們捕獲到InterruptedException, isInterrupted 標誌將被重置,然後isInterrupted()返回 false。Thread 類還有一個靜態的Thread.interrupted()方法,它只適用於當前線程,但是這個方法會將標誌重置為 false!在標題為“線程中斷”的這一章中閱讀更多內容。

加入(等待另一個線程完成)

最簡單的等待類型是等待另一個線程完成。

public static void main(String []args) throws InterruptedException {
	Runnable task = () -> {
		try {
			TimeUnit.SECONDS.sleep(5);
		} catch (InterruptedException e) {
			System.out.println("Interrupted");
		}
	};
	Thread thread = new Thread(task);
	thread.start();
	thread.join();
	System.out.println("Finished");
}
在本例中,新線程將休眠 5 秒。同時,主線程會一直等待,直到休眠線程被喚醒並完成它的工作。如果您在 JVisualVM 中查看線程的狀態,那麼它將看起來像這樣: 更好的結合:Java 和 Thread 類。 第二部分 — 同步 - 4感謝監視工具,您可以看到線程正在發生什麼。該join方法非常簡單,因為它只是一個包含 Java 代碼的方法,wait()只要調用它的線程處於活動狀態,它就會執行。一旦線程死亡(當它完成它的工作時),等待就會被中斷。這就是該方法的全部魔力join()。那麼,讓我們繼續最有趣的事情。

監視器

多線程包括監視器的概念。監視器一詞通過 16 世紀的拉丁語進入英語,意思是“用於觀察、檢查或連續記錄過程的儀器或設備”。在本文的上下文中,我們將嘗試涵蓋基礎知識。對於任何想要詳細信息的人,請深入研究鏈接的材料。我們從 Java 語言規範 (JLS):17.1 開始我們的旅程。同步。它說了以下內容: 事實 更好的結合:Java 和 Thread 類。 第二部分 — 同步 - 5證明,Java 使用“監視”機制來實現線程之間的同步。監視器與每個對象相關聯,線程可以使用 獲取它lock()或釋放它unlock()。接下來,我們將在 Oracle 網站上找到教程:Intrinsic Locks and Synchronization. 本教程說 Java 的同步是圍繞稱為內部鎖監視器鎖的內部實體構建的。這種鎖通常簡稱為“監視器”。我們還再次看到,Java 中的每個對像都有一個與之關聯的內部鎖。您可以閱讀Java - Intrinsic Locks and Synchronization。接下來,了解 Java 中的對像如何與監視器相關聯將很重要。在 Java 中,每個對像都有一個標頭,用於存儲程序員無法從代碼中獲得的內部元數據,但虛擬機需要這些元數據才能正確處理對象。對像頭包含一個“標記詞”,如下所示: 更好的結合:Java 和 Thread 類。 第二部分 — 同步 - 6

https://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf

這裡有一篇 JavaWorld 的文章非常有用:Java 虛擬機如何執行線程同步。本文應結合 JDK 錯誤跟踪系統中以下問題的“摘要”部分的描述:JDK-8183909。您可以在這裡閱讀同樣的內容:JEP-8183909。因此,在 Java 中,監視器與對象相關聯,並用於在線程試圖獲取(或獲取)鎖時阻塞線程。這是最簡單的例子:

public class HelloWorld{
    public static void main(String []args){
        Object object = new Object();
        synchronized(object) {
            System.out.println("Hello World");
        }
    }
}
在這裡,當前線程(執行這些代碼行的線程)使用關鍵字synchronized嘗試使用與object"\獲取/獲取鎖的變量。如果沒有其他人在爭奪監視器(即沒有其他人在使用同一對象運行同步代碼),那麼 Java 可能會嘗試執行稱為“偏向鎖定”的優化。在對像頭中的mark word中添加相關的tag和關於哪個線程擁有monitor的鎖的記錄。這減少了鎖定監視器所需的開銷。如果監視器以前由另一個線程擁有,那麼這樣的鎖定是不夠的。JVM 切換到下一種鎖定類型:“基本鎖定”。它使用比較和交換 (CAS) 操作。更重要的是,對像頭的標記詞本身不再存儲標記詞,而是存儲它的位置的引用,並且標記發生變化,以便 JVM 了解我們使用的是基本鎖定。如果多個線程競爭(contend)一個monitor(一個已經獲取了鎖,第二個在等待鎖釋放),那麼mark word中的tag就變了,mark word現在存儲了一個對monitor的引用作為一個對象——JVM 的一些內部實體。正如 JDK Enchancement Proposal (JEP) 中所述,這種情況需要內存的 Native Heap 區域中的空間來存儲此實體。對該內部實體內存位置的引用將存儲在對像頭的標記字中。因此,監視器實際上是一種在多個線程之間同步訪問共享資源的機制。JVM 在該機制的多個實現之間切換。因此,為簡單起見,在談論監視器時,我們實際上是在談論鎖。第二個正在等待鎖被釋放),然後標記詞中的標記發生變化,標記詞現在將對監視器的引用存儲為對象——JVM 的某個內部實體。正如 JDK Enchancement Proposal (JEP) 中所述,這種情況需要內存的 Native Heap 區域中的空間來存儲此實體。對該內部實體內存位置的引用將存儲在對像頭的標記字中。因此,監視器實際上是一種在多個線程之間同步訪問共享資源的機制。JVM 在該機制的多個實現之間切換。因此,為簡單起見,在談論監視器時,我們實際上是在談論鎖。第二個正在等待鎖被釋放),然後標記詞中的標記發生變化,標記詞現在將對監視器的引用存儲為對象——JVM 的某個內部實體。正如 JDK Enchancement Proposal (JEP) 中所述,這種情況需要內存的 Native Heap 區域中的空間來存儲此實體。對該內部實體內存位置的引用將存儲在對像頭的標記字中。因此,監視器實際上是一種在多個線程之間同步訪問共享資源的機制。JVM 在該機制的多個實現之間切換。因此,為簡單起見,在談論監視器時,我們實際上是在談論鎖。mark word 現在將對監視器的引用存儲為對象——JVM 的某個內部實體。正如 JDK Enchancement Proposal (JEP) 中所述,這種情況需要內存的 Native Heap 區域中的空間來存儲此實體。對該內部實體內存位置的引用將存儲在對像頭的標記字中。因此,監視器實際上是一種在多個線程之間同步訪問共享資源的機制。JVM 在該機制的多個實現之間切換。因此,為簡單起見,在談論監視器時,我們實際上是在談論鎖。mark word 現在將對監視器的引用存儲為對象——JVM 的某個內部實體。正如 JDK Enchancement Proposal (JEP) 中所述,這種情況需要內存的 Native Heap 區域中的空間來存儲此實體。對該內部實體內存位置的引用將存儲在對像頭的標記字中。因此,監視器實際上是一種在多個線程之間同步訪問共享資源的機制。JVM 在該機制的多個實現之間切換。因此,為簡單起見,在談論監視器時,我們實際上是在談論鎖。對該內部實體內存位置的引用將存儲在對像頭的標記字中。因此,監視器實際上是一種在多個線程之間同步訪問共享資源的機制。JVM 在該機制的多個實現之間切換。因此,為簡單起見,在談論監視器時,我們實際上是在談論鎖。對該內部實體內存位置的引用將存儲在對像頭的標記字中。因此,監視器實際上是一種在多個線程之間同步訪問共享資源的機制。JVM 在該機制的多個實現之間切換。因此,為簡單起見,在談論監視器時,我們實際上是在談論鎖。 更好的結合:Java 和 Thread 類。 第二部分 — 同步 - 7

同步(等待鎖)

正如我們之前看到的,“同步塊”(或“臨界區”)的概念與監視器的概念密切相關。看一個例子:

public static void main(String[] args) throws InterruptedException {
	Object lock = new Object();

	Runnable task = () -> {
		synchronized(lock) {
			System.out.println("thread");
		}
	};

	Thread th1 = new Thread(task);
	th1.start();
	synchronized(lock) {
		for (int i = 0; i < 8; i++) {
			Thread.currentThread().sleep(1000);
			System.out.print(" " + i);
		}
		System.out.println(" ...");
	}
}
在這裡,主線程首先將任務對像傳遞給新線程,然後立即獲取鎖並與之進行長時間操作(8 秒)。一直以來,任務無法繼續,因為它無法進入塊synchronized,因為已經獲取了鎖。如果線程拿不到鎖,就會等待監聽。一旦獲得鎖,它將繼續執行。當線程退出監視器時,它會釋放鎖。在JVisualVM中,是這樣的: 更好的結合:Java 和 Thread 類。 第二部分 — 同步 - 8在JVisualVM中可以看到,狀態為“Monitor”,意思是線程被阻塞,不能拿monitor。您也可以使用代碼來確定線程的狀態,但是以這種方式確定的狀態名稱與 JVisualVM 中使用的名稱不匹配,儘管它們是相似的。在這種情況下,th1.getState()for 循環中的語句會返回BLOCKED,因為只要循環在運行,lock對象的監視器就被線程佔用mainth1線程被阻塞,直到鎖被釋放後才能繼續執行。除了同步塊之外,還可以同步整個方法。例如,這是該類的一個方法HashTable

public synchronized int size() {
	return count;
}
在任何給定時間,此方法將僅由一個線程執行。我們真的需要鎖嗎?是的,我們需要它。在實例方法的情況下,“this”對象(當前對象)充當鎖。這裡有一個關於這個主題的有趣討論:Is there an advantage to using a Synchronized Method instead of a Synchronized Block? . 如果該方法是靜態的,那麼鎖定的將不是“this”對象(因為靜態方法沒有“this”對象),而是一個 Class 對象(例如,)Integer.class

等待(等待監視器)。notify() 和 notifyAll() 方法

Thread 類有另一個與監視器關聯的等待方法。sleep()與and不同join(),不能簡單地調用此方法。它的名字是wait()wait在與我們要等待的監視器關聯的對像上調用該方法。讓我們看一個例子:

public static void main(String []args) throws InterruptedException {
	    Object lock = new Object();
	    // The task object will wait until it is notified via lock
	    Runnable task = () -> {
	        synchronized(lock) {
	            try {
	                lock.wait();
	            } catch(InterruptedException e) {
	                System.out.println("interrupted");
	            }
	        }
	        // After we are notified, we will wait until we can acquire the lock
	        System.out.println("thread");
	    };
	    Thread taskThread = new Thread(task);
	    taskThread.start();
        // We sleep. Then we acquire the lock, notify, and release the lock
	    Thread.currentThread().sleep(3000);
	    System.out.println("main");
	    synchronized(lock) {
	        lock.notify();
	    }
}
在 JVisualVM 中,它看起來像這樣: 更好的結合:Java 和 Thread 類。 第二部分 — 同步 - 10要理解它是如何工作的,請記住wait()notify()方法與 相關聯java.lang.Object。與線程相關的方法在類中可能看起來很奇怪Object。但其原因現在正在展開。您會記得 Java 中的每個對像都有一個標頭。標頭包含各種內部管理信息,包括有關監視器的信息,即鎖的狀態。請記住,每個對像或類的實例都與 JVM 中的一個內部實體相關聯,稱為內部鎖或監視器。在上面的示例中,任務對象的代碼表明我們進入了與該對象關聯的監視器的同步塊lock。如果我們成功獲得了這個監視器的鎖,那麼wait()叫做。執行任務的線程會釋放lock對象的監視器,但會進入等待對象監視器通知的線程隊列lock。這個線程隊列稱為 WAIT SET,它更恰當地反映了它的用途。也就是說,它更像是一個集合而不是一個隊列。該main線程使用任務對象創建一個新線程,啟動它,並等待 3 秒。這使得新線程很有可能在線程之前獲得鎖main,並進入監視器的隊列。之後main線程自己進入lock對象的synchronized塊,使用monitor進行線程通知。發送通知後,main線程釋放lock對象的監視器,之前等待lock對象監視器被釋放的新線程繼續執行。可以只向一個線程 ( notify()) 發送通知,也可以同時向隊列中的所有線程 ( notifyAll()) 發送通知。在此處閱讀更多信息:Java 中的 notify() 和 notifyAll() 之間的區別。請務必注意,通知順序取決於 JVM 的實現方式。在此處閱讀更多內容:如何使用 notify 和 notifyAll 解決飢餓問題?. 可以在不指定對象的情況下執行同步。當同步整個方法而不是單個代碼塊時,您可以執行此操作。例如,對於靜態方法,鎖將是一個 Class 對象(通過 獲得.class):

public static synchronized void printA() {
	System.out.println("A");
}
public static void printB() {
	synchronized(HelloWorld.class) {
		System.out.println("B");
	}
}
在使用鎖方面,這兩種方法是一樣的。如果一個方法不是靜態的,那麼同步將使用當前的instance,即使用this。順便說一下,我們之前說過您可以使用該getState()方法來獲取線程的狀態。wait()例如,對於隊列中等待監視器的線程,如果方法指定超時 ,狀態將為 WAITING 或 TIMED_WAITING 。更好的結合:Java 和 Thread 類。 第二部分 — 同步 - 11

https://stackoverflow.com/questions/36425942/what-is-the-lifecycle-of-thread-in-java

線程生命週期

在線程的生命週期中,線程的狀態會發生變化。事實上,這些變化構成了線程的生命週期。一旦線程被創建,它的狀態就是 NEW。在這種狀態下,新線程還沒有運行,Java 線程調度器還不知道任何關於它的信息。為了讓線程調度程序了解線程,您必須調用該thread.start()方法。然後線程將轉換為 RUNNABLE 狀態。Internet 上有很多區分“Runnable”和“Running”狀態的錯誤圖表。但這是一個錯誤,因為 Java 不區分“準備工作”(runnable)和“工作中”(running)。當線程處於活動狀態但不活動(不是 Runnable)時,它處於兩種狀態之一:
  • BLOCKED — 等待進入臨界區,即塊synchronized
  • WAITING——等待另一個線程滿足某些條件。
如果滿足條件,則線程調度程序啟動線程。如果線程正在等待指定時間,則其狀態為 TIMED_WAITING。如果線程不再運行(結束或拋出異常),則進入 TERMINATED 狀態。要找出線程的狀態,請使用getState()方法。線程也有一個isAlive()方法,如果線程未終止,則返回 true。

LockSupport 和線程停放

從 Java 1.6 開始,出現了一種名為LockSupport的有趣機制。 更好的結合:Java 和 Thread 類。 第二部分 — 同步 - 12此類將“許可”與使用它的每個線程相關聯。如果許可可用,則對該方法的調用park()會立即返回,並在此過程中消耗許可。否則,它會阻塞。unpark如果許可尚不可用,則調用該方法可使許可可用。只有 1 個許可證。的 Java 文檔LockSupport引用了該類Semaphore。讓我們看一個簡單的例子:

import java.util.concurrent.Semaphore;
public class HelloWorldApp{
    
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(0);
        try {
            semaphore.acquire();
        } catch (InterruptedException e) {
            // Request the permit and wait until we get it
            e.printStackTrace();
        }
        System.out.println("Hello, World!");
    }
}
這段代碼將一直等待,因為現在信號量有 0 個許可。當acquire()在代碼中調用時(即請求許可),線程將等待直到它收到許可。由於我們在等待,所以我們必須處理InterruptedException. 有趣的是,信號量獲得了一個單獨的線程狀態。如果我們查看 JVisualVM,我們會看到狀態不是“Wait”,而是“Park”。 更好的結合:Java 和 Thread 類。 第二部分 — 同步 - 13讓我們看另一個例子:

public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            // Park the current thread
            System.err.println("Will be Parked");
            LockSupport.park();
            // As soon as we are unparked, we will start to act
            System.err.println("Unparked");
        };
        Thread th = new Thread(task);
        th.start();
        Thread.currentThread().sleep(2000);
        System.err.println("Thread state: " + th.getState());
        
        LockSupport.unpark(th);
        Thread.currentThread().sleep(2000);
}
線程的狀態將是 WAITING,但 JVisualVMwaitsynchronized關鍵字和park類中區分LockSupport。為什麼這LockSupport如此重要?我們再次轉向 Java 文檔並查看WAITING線程狀態。如您所見,只有三種方法可以進入它。其中兩種方式是wait()join()。第三個是LockSupport。在 Java 中,鎖也可以構建在LockSupport 上並提供更高級別的工具。讓我們嘗試使用一個。例如,看看ReentrantLock

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class HelloWorld{

    public static void main(String []args) throws InterruptedException {
        Lock lock = new ReentrantLock();
        Runnable task = () -> {
            lock.lock();
            System.out.println("Thread");
            lock.unlock();
        };
        lock.lock();

        Thread th = new Thread(task);
        th.start();
        System.out.println("main");
        Thread.currentThread().sleep(2000);
        lock.unlock();
    }
}
就像前面的示例一樣,這裡的一切都很簡單。該lock對像等待某人釋放共享資源。如果我們查看 JVisualVM,我們會看到新線程將被停放,直到線程main釋放對它的鎖定。你可以在這裡閱讀更多關於鎖的信息:Java 8 StampedLocks vs. ReadWriteLocks and Synchronized and Lock API in Java。為了更好地理解鎖是如何實現的,閱讀這篇文章中有關 Phaser 的內容會很有幫助:Java Phaser 指南。談到各種同步器,您必須閱讀有關 The Java Synchronizers 的 DZone文章

結論

在這篇評論中,我們研究了 Java 中線程交互的主要方式。附加材料: 更好的結合:Java 和 Thread 類。第 I 部分 — 執行的線程 更好地結合:Java 和 Thread 類。第 III 部分 — 更好地交互: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