JavaRush /Java Blog /Random-TW /你不能用線程毀掉 Java:第二部分 - 同步
Viacheslav
等級 3

你不能用線程毀掉 Java:第二部分 - 同步

在 Random-TW 群組發布

介紹

所以,我們知道 Java 中有線程,您可以在評論“你不能用線程破壞 Java:第一部分 - 線程”中閱讀有關線程的內容。需要線程來同時完成工作。因此,線程很可能會以某種方式相互交互。讓我們了解這是如何發生的以及我們擁有哪些基本控制措施。 你不能用線程毀掉 Java:第二部分 - 同步 - 1

屈服

Thread.yield()方法很神秘,很少被使用。互聯網上對其描述有多種變體。以至於有些人寫了某種線程隊列,其中線程將根據其優先級向下移動。有人寫道,執行緒會將其狀態從運行變為可運行(儘管這些狀態沒有劃分,Java也不區分它們)。但實際上,一切都更加未知,而且從某種意義上來說,也更加簡單​​。 你不能用線程毀掉 Java:第二部分 - 同步 - 2關於方法文檔的主題,yield有一個錯誤「JDK-6416721:(spec thread) Fix Thread.yield() javadoc」。如果您閱讀它,就會清楚地看出,實際上該方法yield只是向 Java 執行緒調度程序傳達一些建議,即可以為該執行緒分配更少的執行時間。但實際會發生什麼、調度程序是否會聽到建議以及它通常會做什麼取決於 JVM 和作業系統的實現。或者也許是因為其他一些因素。所有的混亂很可能是由於Java語言開發過程中對多執行緒的重新思考所造成的。您可以在評論“ Java Thread.yield()簡介”中閱讀更多內容。

睡眠 - 入睡主題

執行緒在執行期間可能會進入睡眠狀態。這是與其他線程互動的最簡單類型。安裝Java虛擬機器、執行Java程式碼的作業系統有自己的執行緒調度器,稱為Thread Scheduler。他決定何時運行哪個執行緒。程式設計師無法直接從 Java 程式碼與該調度程式交互,但他可以透過 JVM 要求調度程序暫停線程一段時間,以「使其進入睡眠狀態」。您可以在文章「Thread.sleep()」和「多執行緒工作原理」中閱讀更多內容。此外,您可以了解Windows作業系統中執行緒是如何運作的:「Windows執行緒的內部結構」。現在我們將親眼所見。讓我們將以下程式碼儲存到文件中HelloWorldApp.java
class HelloWorldApp {
    public static void main(String []args) {
        Runnable task = () -> {
            try {
                int secToWait = 1000 * 60;
                Thread.currentThread().sleep(secToWait);
                System.out.println("Waked 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,並使用以下命令打開線程列表jvisualvm --openpid pidПроцесса你不能用線程毀掉 Java:第二部分 - 同步 - 3如您所見,我們的線程已進入睡眠狀態。事實上,休眠當前執行緒可以做得更漂亮:
try {
	TimeUnit.SECONDS.sleep(60);
	System.out.println("Waked up");
} catch (InterruptedException e) {
	e.printStackTrace();
}
您可能已經注意到我們無所不在進行處理InterruptedException?讓我們了解一下原因。

中斷線程或 Thread.interrupt

問題是,當線程在夢中等待時,有人可能想中斷這個等待。在本例中,我們處理這樣的異常。這是在該方法Thread.stop被聲明為已棄用之後完成的,即 已經過時且不適合使用。原因是當該方法被呼叫時,stop線程就被簡單地「殺死」了,這是非常不可預測的。我們無法知道串流何時會停止,無法保證資料的一致性。想像一下,您正在將資料寫入文件,然後流被破壞。因此,我們決定不終止該流,而是通知它應該被中斷會更合乎邏輯。如何對此做出反應取決於流程本身。更多詳細資訊可以在 Oracle 的“ Why is Thread.stop deprecated? ”中找到。讓我們來看一個例子:
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!您可以在「線程中斷」一章中閱讀更多內容。

Join — 等待另一個執行緒完成

最簡單的等待類型是等待另一個執行緒完成。
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:第二部分 - 同步 - 4借助監視工具,您可以看到執行緒發生了什麼。這個方法join非常簡單,因為它只是一個帶有 java 程式碼的方法,wait在呼叫它的執行緒處於活動狀態時執行。一旦執行緒死亡(終止時),等待就會終止。這就是該方法的全部魔力join。因此,讓我們進入最有趣的部分。

概念監視器

在多線程中有一個叫做Monitor的東西。一般來說,“監督者”一詞從拉丁語翻譯為“監督者”或“監督者”。在本文的框架內,我們將盡力記住本質,對於那些想要的人,我請您深入了解連結中的材料以獲取詳細資訊。讓我們從Java語言規格開始我們的旅程,即從JLS開始:「17.1.同步」。它是這麼說的: 你不能用線程毀掉 Java:第二部分 - 同步 - 5原來,為了線程之間的同步,Java使用了一種叫做「Monitor」的機制。每個物件都有一個與之關聯的監視器,線程可以鎖定它或解鎖它。接下來,我們會在Oracle網站上找到一個訓練教學:「Intrinsic Locks and Synchronization」。本教學解釋了 Java 中的同步是圍繞著稱為內在鎖或監視器鎖的內部實體建構的。通常這樣的鎖被簡單地稱為「監視器」。我們也再次看到,Java 中的每個物件都有一個與其關聯的內在鎖定。你可以閱讀《Java——本質鎖與同步》。接下來,了解 Java 中的物件如何與監視器關聯非常重要。Java 中的每個物件都有一個標頭 - 一種內部元數據,程式設計師無法從程式碼中獲得它,但虛擬機器需要它才能正確處理物件。物件標頭包含一個 MarkWord,如下所示: 你不能用線程毀掉 Java:第二部分 - 同步 - 6

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

Habr 的一篇文章在這裡非常有用:“但是多線程是如何工作的?第一部分:同步。” 在本文中,值得新增 JDK bugtaker 的任務區塊摘要中的描述:「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 中物件的標題將包含對應的標記以及監視器附加到哪個執行緒的記錄。這減少了捕獲監視器時的開銷。如果監視器之前已經綁定到另一個線程,那麼這種鎖定是不夠的。JVM 切換到下一個鎖定類型 - 基本上鎖定。它使用比較和交換(CAS)操作。同時,Mark Word 中的標頭不再儲存 Mark Word 本身,而是更改了其儲存 + 標記的鏈接,以便 JVM 知道我們正在使用基本鎖定。如果多個執行緒存在對監視器的爭用(一個已捕獲監視器,第二個正在等待監視器被釋放),則Mark Word中的標記發生變化,並且Mark Word開始儲存對監視器的引用為對象- JVM 的一些內部實體。正如 JEP 中所述,在這種情況下,Native Heap 記憶體區域需要空間來儲存該實體。此內部實體的儲存位置的連結將位於Mark Word物件中。因此,正如我們所看到的,監視器實際上是一種確保多個執行緒對共享資源的存取同步的機制。JVM 在該機制的多種實作之間進行切換。因此,為了簡單起見,當談論監視器時,我們實際上是在談論鎖。 你不能用線程毀掉 Java:第二部分 - 同步 - 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:第二部分 - 同步 - 8正如您所看到的,JVisualVM 中的狀態稱為“監視器”,因為線程被阻塞並且無法佔用監視器。您也可以在程式碼中找出執行緒的狀態,但該狀態的名稱與 JVisualVM 術語並不重合,儘管它們很相似。在這種情況下,th1.getState()循環for將返回BLOCKED,因為 當循環運行時,監視器lock被執行緒佔用main,執行緒th1被阻塞,無法繼續工作,直到鎖返回。除了同步區塊之外,還可以同步整個方法。例如,類別中的方法HashTable
public synchronized int size() {
	return count;
}
在一個單位時間內,該方法只會被一個執行緒執行。但我們需要一把鎖,對嗎?是的,我需要它。對於物件方法,鎖將為this。關於這個主題有一個有趣的討論:「使用同步方法而不是同步區塊有優勢嗎?」。如果該方法是靜態的,那麼鎖將不是this(因為對於靜態方法來說它不能是this),而是類別物件(例如,Integer.class)。

在監視器上等待、等待。通知和notifyAll方法

執行緒還有另一個等待方法,它連接到監視器。sleep與and不同join,它不能只是被呼叫。他的名字是waitwait該方法在我們要等待其監視器的物件上執行。讓我們來看一個例子:
public static void main(String []args) throws InterruptedException {
	    Object lock = new Object();
	    // task будет ждать, пока его не оповестят через lock
	    Runnable task = () -> {
	        synchronized(lock) {
	            try {
	                lock.wait();
	            } catch(InterruptedException e) {
	                System.out.println("interrupted");
	            }
	        }
	        // После оповещения нас мы будем ждать, пока сможем взять лок
	        System.out.println("thread");
	    };
	    Thread taskThread = new Thread(task);
	    taskThread.start();
        // Ждём и после этого забираем себе лок, оповещаем и отдаём лок
	    Thread.currentThread().sleep(3000);
	    System.out.println("main");
	    synchronized(lock) {
	        lock.notify();
	    }
}
在 JVisualVM 中,它看起來像這樣: 你不能用線程毀掉 Java:第二部分 - 同步 - 10要理解它是如何工作的,您應該記住方法wait引用. 與執行緒相關的方法在. 但這就是答案。我們記得,Java 中的每個物件都有一個標頭。標頭包含各種服務信息,包括有關監視器的信息 - 有關鎖定狀態的數據。正如我們所記得的,每個物件(即每個實例)都與稱為內在鎖(也稱為監視器)的內部 JVM 實體相關聯。在上面的範例中,任務描述了我們在與 關聯的監視器上輸入同步區塊。如果可以獲得該監視器的鎖定,則. 執行此任務的執行緒將釋放監視器,但將加入等待監視器通知的執行緒佇列。這個執行緒佇列稱為WAIT-SET,它更正確地反映了本質。它更像是一組而不是隊列。該線程使用任務task創建一個新線程,啟動它並等待3秒。這使得新線程很有可能在該線程之前獲取鎖並在監視器上排隊。之後執行緒本身進入同步區塊並在監視器上執行執行緒的通知。發送通知後,執行緒釋放監視器,新執行緒(之前等待的)等待監視器釋放後繼續執行。可以僅向其中一個執行緒發送通知 ( ) 或一次向佇列中的所有執行緒發送通知 ( )。您可以閱讀「Java中notify()和notifyAll()之間的差異」來了解更多內容。值得注意的是,通知順序取決於 JVM 實作。更多內容可以閱讀「如何用notify和notifyall解決飢餓問題?」。無需指定物件即可執行同步。當同步的不是單獨的程式碼段而是整個方法時,可以完成此操作。例如,對於靜態方法,鎖將是類別物件(透過取得): notifyjava.lang.ObjectObjectlockwaitlocklockmainmainmainlockmainlocklocknotifynotifyAll.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:第二部分 - 同步 - 11

線程的生命週期

正如我們所看到的,心流在生命過程中改變著它的狀態。本質上,這些變化就是執行緒的生命週期。當線程剛剛創建時,它具有 NEW 狀態。在這個位置,它還沒有啟動,Java 執行緒調度程式還不知道有關新執行緒的任何資訊。為了讓線程調度程序了解線程,您必須調用thread.start(). 然後線程將進入RUNNABLE狀態。網路上有很多不正確的方案,將Runnable和Running狀態分開。但這是一個錯誤,因為... Java 不區分「準備運行」和「正在運行」狀態。當執行緒處於活動狀態但不活動(不可運行)時,它處於以下兩種狀態之一:
  • BLOCKED - 等待進入受保護的部分,即 到synchonized街區。
  • WAITING - 根據條件等待另一個執行緒。如果條件為真,則執行緒調度程序將啟動執行緒。
如果一個執行緒正在按時間等待,則它處於 TIMED_WAITING 狀態。如果執行緒不再運行(成功完成或出現異常),它將進入 TERMINATED 狀態。要找出執行緒的狀態(它的狀態),可以使用該方法getState。執行緒還有一個方法isAlive,如果執行緒未終止,則傳回 true。

LockSupport 和線程停放

從 Java 1.6 開始,出現了一個有趣的機制,稱為LockSupport你不能用線程毀掉 Java:第二部分 - 同步 - 12此類別將「許可」或權限與使用它的每個執行緒相關聯。park如果許可證可用,則方法呼叫立即返回,並在呼叫期間佔用相同的許可證。否則會被阻止。unpark如果許可證尚不可用,則呼叫該方法可使許可證可用。只有 1 個 Permit。在 Java API 中,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) {
            // Просим разрешение и ждём, пока не получим его
            e.printStackTrace();
        }
        System.out.println("Hello, World!");
    }
}
這段程式碼將永遠等待,因為信號量現在有 0 個許可。當在程式碼中呼叫acquire(即請求權限)時,執行緒會等待,直到收到權限。既然我們在等待,我們就有義務去處理它InterruptedException。有趣的是,信號量實現了單獨的線程狀態。如果我們查看 JVisualVM,我們會看到我們的狀態不是 Wait,而是 Park。 你不能用線程毀掉 Java:第二部分 - 同步 - 13讓我們來看另一個例子:
public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            //Запаркуем текущий поток
            System.err.println("Will be Parked");
            LockSupport.park();
            // Как только нас распаркуют - начнём действовать
            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,但 JVisualVM 區分waitfromsynchronizedparkfrom LockSupport。為什麼這一點如此重要LockSupport?讓我們再次轉向 Java API 並查看線程狀態 WAITING。如您所見,只有三種方法可以進入。2 種方法 - 這個waitjoin. 第三個是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 中的多執行緒程式設計。第二部分。同步對可變物件的存取」和「Java Lock API。理論和使用範例」。為了更好地理解鎖的實現,閱讀概述「 Phaser Class 」中有關 Phazer 的內容很有用。而說到各種同步器,你必須閱讀 Habré 上的文章「Java.util.concurrent.* Synchronizers Reference」。

全部的

在這篇評論中,我們研究了 Java 中線程互動的主要方式。附加資料: #維亞切斯拉夫
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION