深入理解Java併發編程之線程Thread

前言

現代操作系統在運行一個程序時,會爲其創建一個進程。例如,啓動一個Java程序,操作系統就會創建一個Java進程。現代操作系統調度的最小單元是線程,也叫輕量級進程(Light Weight Process),在一個進程裏可以創建多個線程,這些線程都擁有各自的計數器、堆棧和局部變量等屬性,並且能夠訪問共享的內存變量。處理器在這些線程上高速切換,讓使用者感覺到這些線程在同時執行。

Java線程通過調用線程的start()方法進行啓動,隨着run()方法的執行完畢,線程也隨之終止。

線程優先級

  1. Java線程優先級從低到高爲1~10,默認優先級爲5。如下爲Thread.java的源碼:
    /**
     * The minimum priority that a thread can have.
     */
    public final static int MIN_PRIORITY = 1;

   /**
     * The default priority that is assigned to a thread.
     */
    public final static int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public final static int MAX_PRIORITY = 10;
  1. 線程優先級不能作爲程序正確性的依賴,因爲操作系統可以完全不用理會Java線程對於優先級的設定。

線程的狀態

Java線程在運行的生命週期中可能處於6種不同的狀態,在給定的一個時刻,線程只能處於其中的一個狀態。如下內容截取JDK 1.8 Thread.java的源碼:

  1. NEW: 初始轉態,線程被構建,但是還沒有調用start()方法。
  2. RUNNABLE: 正在執行的線程狀態,JVM中runnable線程狀態對應於操作系統中的就緒和運行兩種狀態。
  3. BLOCKED: 線程等待monitor互斥量的阻塞狀態,在blocked狀態的線程正在被執行Object.wait()後等着進入或者再次同步塊或者同步方法。
  4. WAITING: 等待狀態,下列方法會導致線程處於等待狀態:
    • Object.wait with no timeout
    • Thread.join with on timeout
    • LockSupport.park
  5. TIMED_WAITING: 超時等待,超過等待時間便會自動返回運行狀態,下列方法會導致線程處於超時等待狀態:
    • Thread.sleep
    • Object.wait(long) with timeout
    • Thread.join(long) with timeout
    • LockSupport.parkNanos
    • LockSupport.parkUntil
  6. TERMINATED: 線程完成執行後結束的狀態。

jstack Dump日誌文件中的線程狀態

Monitor

Monitor是 Java中用以實現線程之間的互斥與協作的主要手段,它可以看成是對象的鎖。每一個對象都有,也僅有一個 monitor。

在HotSpot JVM中,monitor是由ObjectMonitor實現的,其主要數據結構如下(位於HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現的):

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //記錄個數
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //處於wait狀態的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //處於等block狀態的線程,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor中主要有以下4個參數:

  1. _Owner: 用於指向ObjectMonito對象的線程
  2. _EntrySet:用來保存處於blocked狀態的線程列表
  3. _WaitSet: 用來保存處於waiting狀態的線程
  4. _count: 計數器

當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList 集合,當線程獲取到對象的monitor 後進入 _Owner 區域並把monitor中的owner變量設置爲當前線程。同時monitor中的計數器count加1,若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復爲null,count自減1,同時該線程進入 _WaitSet集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其他線程進入獲取monitor(鎖)。如下圖所示:

 

 

 

Dump文件中的線程狀態

  1. Deadlock:死鎖線程,一般指多個線程調用間,進入相互資源佔用,導致一直等待無法釋放的情況。

  2. Runnable:一般指該線程正在執行狀態中,該線程佔用了資源 。

  3. Waiting on condition:等待資源,或等待某個條件的發生 。

  4. Blocked:線程阻塞,是指當前線程執行過程中,所需要的資源長時間等待卻一直未能獲取到,被容器的線程管理器標識爲阻塞狀態,可以理解爲等待資源超時的線程。

  5. Waiting for monitor entry :在線程嘗試執行同步代碼前,在monitor的”Entry Set“隊列中的等待線程。

  6. In Object.wait(): 當線程獲得了 Monitor,進入了臨界區之後,如果發現線程繼續運行的條件沒有滿足,它則調用對象(一般就是被 synchronized 的對象)的 wait() 方法,放棄了 Monitor,進入 “Wait Set”隊列。

線程同步時的執行模型

  1. 任意一個對象都擁有自己的Monitor;
  2. 當這個對象由同步塊或者這個對象的同步方法調用時,執行方法的線程必須先獲取到該對象的監視器才能進入同步塊或者同步方法;
  3. 沒有獲取到監視器(執行該方法)的線程將會被阻塞在同步塊和同步方法的入口處,進入BLOCKED狀態。

下圖描述了對象、對象的監視器、同步隊列和執行線程之間的關係:

 

 

 

從圖中可以看到

  1. 任意線程對Object(Object由synchronized保護)的訪問,首先要獲得Object的監視器。
  2. 如果獲取成功,線程獲得monitor,進入同步區域,執行同步塊。
  3. 如果獲取失敗,線程進入同步隊列,線程狀態變爲BLOCKED。
  4. 當訪問Object的前驅(獲得了鎖的線程)釋放了鎖,則該釋放操作喚醒阻塞在同步隊列中的線程,使其重新嘗試對監視器的獲取。

線程等待/通知機制

等待/通知機制,是指一個線程A調用了對象O的wait()方法進入等待狀態,而另一個線程B調用了對象O的notify()或者notifyAll()方法,線程A收到通知後從對象O的wait()方法返回,進而執行後續操作。上述兩個線程通過對象O來完成交互,而對象上的wait()和notify/notifyAll()的關係就如同開關信號一樣,用來完成等待方和通知方之間的交互工作。

等待/通知的相關方法是任意Java對象都具備的,因爲這些方法被定義在所有對象的超類java.lang.Object上,方法和描述如下:

  1. notify(): 通知一個在對象上等待的線程,使其從wait()方法返回,而返回的前提是該線程獲取到了對象的鎖

  2. notifyAll(): 通知所有等待在該對象上的線程。

  3. wait(): 調用該方法的線程進入WAITING狀態,只有等待另外線程的通知或被中斷纔會返回,需要注意,調用wait()方法後,會釋放對象的鎖

  4. wait(long): 超時等待一段時間,這裏單位是毫秒。

  5. wait(long, int): 超時狀態更細粒度的控制,可以達到納秒。

注意

  1. wait(), notify(), notifyAll()方法的調用都需要位於被synchronized關鍵字包裹的代碼塊或者是方法中,即線程需要獲取鎖才能執行這些方法
  2. wait() - notify(), wait() - notifyAll()的使用需要針對同一個鎖,不然會拋出異常。
  3. 調用wait()方法後,線程狀態由RUNNING變爲WAITING,並將當前線程放置到對象的等待隊列
  4. notify()或notifyAll()方法調用後,等待線程依舊不會從wait()返回,需要調用notify()或notifAll()的線程釋放鎖之後,等待線程纔有機會從wait()返回
  5. notify()方法將等待隊列中的一個等待線程從等待隊列中移到同步隊列中,而notifyAll()方法則是將等待隊列中所有的線程全部移到同步隊列,被移動的線程狀態由WAITING變爲BLOCKED
  6. 從wait()方法返回的前提是獲得了調用對象的鎖

從上述可以發現,等待/通知機制依託於同步機制,其目的就是確保等待線程從wait()方法返回時能夠感知到通知線程對變量做出的修改。

下面是上述示例的過程圖:

 

 

 

  1. WaitThread首先獲取了對象的鎖,然後調用對象的wait()方法,從而放棄了鎖並進入了對象的等待隊列WaitQueue中,進入Waiting狀態。

  2. 由於WaitThread釋放了對象的鎖,NotifyThread隨後獲取了對象的鎖,並調用對象的notify()方法,將WaitThread從WaitQueue移到SynchronizedQueue中,此時WaitThread的狀態變爲Blocked狀態。

  3. NotifyThread釋放了鎖之後,WaitThread再次獲取到鎖並從wait()方法返回繼續執行。

等待/通知的經典範式

等待/通知的經典範式分爲兩個部分:等待方和通知方。 等待方遵循如下原則:

  1. 獲取對象的鎖。
  2. 如果條件不滿足,那麼調用對象的wait()方法,被通知後仍要檢查條件。
  3. 條件滿足則執行對應的邏輯。 對應的僞代碼如下:
synchronized(對象) {
	while(條件不滿足) {
		對象.wait();
	}
	對應的處理邏輯
}

通知方遵循如下原則:

  1. 獲得對象的鎖。
  2. 改變條件。
  3. 通知所有等待在對象上的線程。 對應的僞代碼如下:
synchronized(對象) {
	改變條件
	對象.notifyAll();
}

Thread.join()

這部分內容可以參見深入理解Java併發編程之通過JDK C++源碼以及Debug源碼死扣Thread.join()

ThreadLocal

這部分內容可以參見深入理解Java併發編程之把ThreadLocal扣爛

參考與感謝

  1. https://blog.csdn.net/lengxiao1993/article/details/52296220
  2. https://blog.csdn.net/javazejian/article/details/72828483
  3. https://www.cnblogs.com/zhengyun_ustc/archive/2013/01/06/dumpanalysis.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章