1. 程式人生 > 實用技巧 >【併發程式設計】- 執行緒篇

【併發程式設計】- 執行緒篇

執行緒

  • 1. 簡介

  • 1)定義

  • 現代作業系統在執行一個程式時,會為其建立一個程序。例如,啟動一個Java程式,作業系統就會建立一個Java程序。現代作業系統排程的最小單元是執行緒,也叫輕量級程序(LightWeightProcess),在一個程序裡可以建立多個執行緒,這些執行緒都擁有各自的計數器、堆疊和區域性變數等屬性,並且能夠訪問共享的記憶體變數。處理器在這些執行緒上快速切換,讓使讀者感覺到這些執行緒在同時執行。

  • 2) 那麼為何要使用多執行緒

  • 更多的處理器核心

  • 更快的響應

  • 更好的程式設計模型

  • 3)優先順序

  • 由於是作業系統給執行緒分配時間片的處理方式。那麼便可以通過優先順序設定處理的先後,確保處理器不會被獨佔。

  • 4)狀態


  • 狀態變換圖


  • 2. 啟動與終止

  • 1)啟動與中斷

  • 啟動:當前執行緒(即parent執行緒)同步告知Java虛擬機器,只要執行緒規劃器空閒,應立即啟動呼叫start()方法的執行緒。

  • 中斷:即是執行緒的一個標識位屬性,通過呼叫該執行緒的interrupt()方法對其進行中斷操作。也可以呼叫靜態方法Thread.interrupted()對當前執行緒的中斷標識位進行復位,當丟擲InterruptedException之前,Java虛擬機器會先將該執行緒的中斷標識位清除,即此時呼叫isInterrupted()方法將會返回false。

  • 2)安全地終止執行緒(優雅)

  • 通過設定一個boolean變數控制

   public class Shutdown {
        public static void main(String[] args) throws Exception {
            Runner one = new Runner();
            Thread countThread = new Thread(one, "CountThread");
            countThread.start();
            // 睡眠1秒,main執行緒對CountThread進行中斷,使CountThread能夠感知中斷而結束
            TimeUnit.SECONDS.sleep(1);
            countThread.interrupt();
            Runner two = new Runner();
            countThread = new Thread(two, "CountThread");
            countThread.start();
            // 睡眠1秒,main執行緒對Runner two進行取消,使CountThread能夠感知on為falseer結束
            TimeUnit.SECONDS.sleep(1);
            two.cancel();
        }

        private static class Runner implements Runnable {
            private long i;
            private volatile boolean on = true;

            @Override
            public void run() {
                while (on && !Thread.currentThread().isInterrupted()) {
                    i++;
                }
                System.out.println("Count i = " + i);
            }

            public void cancel() {
                on = false;
            }
        }
    }

  • 3. 執行緒間通訊

  • 1)volatile和synchronized

  • java在多個執行緒訪問一個物件或物件的成員變數,每個執行緒擁有其拷貝,將其放入各自的快取中,這樣可以加速程式的執行,故執行緒看到的變數並不一定是最新的。假如需要最新的,此時需要通過volatile通知執行緒間從共享記憶體中獲取,並重新整理回各自的工作記憶體,即是保證了對所有執行緒對變數訪問的可見性。
  • 關鍵字synchronized可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個執行緒在同一個時刻,只能有一個執行緒處於方法或者同步塊中,它保證了執行緒對變數訪問的可見性和排他性。
    • 同步塊的實現使用了monitorentermonitorexit指令。
    • 同步方法則是依靠方法修飾符上的ACC_SYNCHRONIZED來完成的。

本質:對一個物件的監視器(monitor)進行獲取,而這個獲取過程是排他的,也就是同一時刻只能有一個執行緒獲取到由synchronized所保護物件的監視器。



  • 任意執行緒對Object(Object由synchronized保護)的訪問,首先要獲得Object的監視器。如果獲取失敗,執行緒進入同步佇列,執行緒狀態變為BLOCKED。當訪問Object的前驅(獲得了鎖的執行緒)釋放了鎖,則該釋放操作喚醒阻塞在同步佇列中的執行緒,使其重新嘗試對監視器的獲取。

    • 2)等待/通知機制

等待/通知機制,是指一個執行緒A呼叫了物件O的wait()方法進入等待狀態,而另一個執行緒B呼叫了物件O的notify()或者notifyAll()方法,執行緒A收到通知後從物件O的wait()方法返回,進而執行後續操作。

  • 解決兩個問題
    • 確保及時性
    • 降低開銷
  • 方法
    • notify: 通知一個物件上等待的執行緒,使其從wait方法返回,而返回的前提是執行緒獲取到了物件的鎖。
    • notifyAll:通知所有等待在該物件上的執行緒。
    • wait: 呼叫該方法的執行緒進入WAITING狀態,只有等待另外執行緒的通知或被中斷才會返回,需要注意,呼叫wait方法後,會釋放物件的鎖。
    • wait(long): 超時等待一段時間,這裡的引數時間是毫秒,也就是等待長達n毫秒,如果沒有通知就超時返回。
    • wait(long,int): 對於超時時間更細粒度的控制,可以達到納秒。
  • 使用注意事項
    • 使用wait()notify()notifyAll()時需要先對呼叫物件加鎖。
    • 呼叫wait()方法後,執行緒狀態由RUNNING變為WAITING,並將當前執行緒放置到物件的等待佇列。
    • notify()notifyAll()方法呼叫後,等待執行緒依舊不會從wait()返回,需要呼叫notify()notifAll()的執行緒釋放鎖之後,等待執行緒才有機會從wait()返回。
    • notify()方法將等待佇列中的一個等待執行緒從等待佇列中移到同步佇列中,而notifyAll()方法則是將等待佇列中所有的執行緒全部移到同步佇列,被移動的執行緒狀態由WAITING變為BLOCKED
    • wait()方法返回的前提是獲得了呼叫物件的鎖。

上圖中,WaitThread首先獲取了物件的鎖,然後呼叫物件的wait()方法,從而放棄了鎖並進入了物件的等待佇列WaitQueue中,進入等待狀態。由於WaitThread釋放了物件的鎖,NotifyThread隨後獲取了物件的鎖,並呼叫物件的notify()方法,將WaitThread從WaitQueue移到SynchronizedQueue中,此時WaitThread的狀態變為阻塞狀態。NotifyThread釋放了鎖之後,WaitThread再次獲取到鎖並從wait()方法返回繼續執行。


  • 3)Thread.join()

定義:如果一個執行緒A執行了thread.join()語句,當前執行緒A等待thread執行緒終止之後才從thread.join()返回。

  • 4)ThreadLocal

  • ThreadLocal,即執行緒變數,是一個以ThreadLocal物件為鍵、任意物件為值的儲存結構。
  • ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其它執行緒所對應的副本。
    • 每個Thread物件內部都維護了一個ThreadLocalMap這樣一個ThreadLocal的Map,可以存放若干個ThreadLocal。
    • 當我們在呼叫get()方法的時候,先獲取當前執行緒,然後獲取到當前執行緒的ThreadLocalMap物件,如果非空,那麼取出ThreadLocal的value,否則進行初始化,初始化就是將initialValue的值set到ThreadLocal中。
    • 當我們呼叫set()方法的時候,很常規,就是將值設定進ThreadLocal中。
  • 採用ThreadLocal 根本就沒有競爭。

記憶體洩露:
實際上 ThreadLocalMap中使用的key為ThreadLocal的弱引用,弱引用的特點是,如果這個物件只存在弱引用,那麼在下一次垃圾回收的時候必然會被清理掉。所以如果ThreadLocal沒有被外部強引用的情況下,在垃圾回收的時候會被清理掉的,這樣一來ThreadLocalMap中使用這個ThreadLocal的key也會被清理掉。但是,value是強引用,不會被清理,這樣一來就會出現key為null的value。ThreadLocalMap實現中已經考慮了這種情況,在呼叫set()、get()、remove()方法的時候,會清理掉key為null的記錄。如果說會出現記憶體洩漏,那只有在出現了key為null的記錄後,沒有手動呼叫remove()方法,並且之後也不再呼叫get()、set()、remove()方法的情況下。