介紹
所以,我們知道Java有線程。您可以在標題為Better together:Java 和 Thread 類的評論中閱讀相關內容。第一部分 — 執行線程。線程是並行執行工作所必需的。這使得線程很可能以某種方式相互交互。讓我們看看這是如何發生的以及我們擁有哪些基本工具。
屈服
Thread.yield()令人費解且很少使用。它在 Internet 上以多種不同的方式描述。包括一些人寫道,有一些線程隊列,其中一個線程將根據線程優先級下降。其他人寫道,線程會將其狀態從“Running”更改為“Runnable”(儘管這些狀態之間沒有區別,即 Java 不區分它們)。現實情況是,它的知名度要低得多,但在某種意義上卻更簡單。
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
: 
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 中查看線程的狀態,那麼它將看起來像這樣: 
join
方法非常簡單,因為它只是一個包含 Java 代碼的方法,wait()
只要調用它的線程處於活動狀態,它就會執行。一旦線程死亡(當它完成它的工作時),等待就會被中斷。這就是該方法的全部魔力join()
。那麼,讓我們繼續最有趣的事情。
監視器
多線程包括監視器的概念。監視器一詞通過 16 世紀的拉丁語進入英語,意思是“用於觀察、檢查或連續記錄過程的儀器或設備”。在本文的上下文中,我們將嘗試涵蓋基礎知識。對於任何想要詳細信息的人,請深入研究鏈接的材料。我們從 Java 語言規範 (JLS):17.1 開始我們的旅程。同步。它說了以下內容: 事實
lock()
或釋放它unlock()
。接下來,我們將在 Oracle 網站上找到教程:Intrinsic Locks and Synchronization. 本教程說 Java 的同步是圍繞稱為內部鎖或監視器鎖的內部實體構建的。這種鎖通常簡稱為“監視器”。我們還再次看到,Java 中的每個對像都有一個與之關聯的內部鎖。您可以閱讀Java - Intrinsic Locks and Synchronization。接下來,了解 Java 中的對像如何與監視器相關聯將很重要。在 Java 中,每個對像都有一個標頭,用於存儲程序員無法從代碼中獲得的內部元數據,但虛擬機需要這些元數據才能正確處理對象。對像頭包含一個“標記詞”,如下所示: 
https://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf
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 在該機制的多個實現之間切換。因此,為簡單起見,在談論監視器時,我們實際上是在談論鎖。 
同步(等待鎖)
正如我們之前看到的,“同步塊”(或“臨界區”)的概念與監視器的概念密切相關。看一個例子:
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中,是這樣的: 
th1.getState()
for 循環中的語句會返回BLOCKED,因為只要循環在運行,lock
對象的監視器就被線程佔用main
,th1
線程被阻塞,直到鎖被釋放後才能繼續執行。除了同步塊之外,還可以同步整個方法。例如,這是該類的一個方法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 中,它看起來像這樣: 
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 。
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——等待另一個線程滿足某些條件。
getState()
方法。線程也有一個isAlive()
方法,如果線程未終止,則返回 true。
LockSupport 和線程停放
從 Java 1.6 開始,出現了一種名為LockSupport的有趣機制。
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”。 
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,但 JVisualVMwait
從synchronized
關鍵字和park
類中區分LockSupport
。為什麼這LockSupport
如此重要?我們再次轉向 Java 文檔並查看WAITING線程狀態。如您所見,只有三種方法可以進入它。其中兩種方式是wait()
和join()
。第三個是LockSupport
。在 Java 中,鎖也可以構建在LockSuppor
t 上並提供更高級別的工具。讓我們嘗試使用一個。例如,看看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 類。第一部分——執行線程
- https://dzone.com/articles/the-java-synchronizers
- https://www.javatpoint.com/java-multithreading-interview-questions
GO TO FULL VERSION