Java多線程總結

Java多線程總結java

        有幾天未寫博客了,雖然在網絡上一搜Java多線程主題有許多的結果,並且有許多深刻講解Java多線程的文章,仍想本身簡單總結一下多線程。編程

       線程的生命週期是指從線程建立、運行到消亡的整個過程。線程的狀態能夠代表線程處於生命週期具體那一階段。線程的狀態包括:建立、就緒、運行、阻塞、死亡五個狀態。安全

        線程狀態間的轉化,引用一張網上的圖來講明:網絡

 

Java多線程基本概念多線程

         Java多線程的編程主要經過幾個基礎類來完成的:Thread、Runnable、Object對象的wait()、notify()、notifyAll()方法、synchronized關鍵字以及Java 5.0後引入的一些多線程工具包。併發

         無論是擴展的Thread類仍是實現Runnable接口的實現類來實現多線程編程,最終都是經過Thread對象的API來控制線程的。框架

        在5.0後引入了兩個特殊的接口:Callable和Future接口,這兩個接口結合使用可實現帶返回值的多線程編程。Future接口表示異步計算的結果,它接供檢查計算是否完成,未完成等待計算完成,並取得計算結果的機制,執行Callable任務後,就能夠取得一個Future的對象,在Future對象上調用get方法就能夠獲得任務計算的結果。異步

 

Callable與Runnable的區別:工具

        *  Callable規定的方法是call(),而Runnable規定的方法是run().性能

        *  Callable的任務執行後可返回值,而Runnable的任務是不能返回值的。

        *  call()方法可拋出異常,而run()方法是不能拋出異常的。

        *  運行Callable任務可拿到一個Future對象,經過Future對象可瞭解任務執行狀況,可取消任務的執行,還可獲取任務執行的結果。

 

使用Callable的基本編程方式以下:

class foo {

    ExecutorService executor = ...

    void doAction(final String[] parameters) throws InterruptedException {

        Future<String> future = executor.submit(new Callable<String>() {

            public String call() {

                // do something here.

                 return result;  //返回結果

        }});

        …..

    }

}

 

        前面提到線程最終是用Thread對象來控制的,在Thread對象中咱們能夠設置線程的優先級。優先級的高低反映線程的重要或緊急程度。線程調度是在優先級基礎上的「先到先服務」。

        在Thread類中,還有幾個特殊的方法:yield()方法和join方法。

        yield方法是把CPU的控制權或運行機會讓給同優先級的其餘線程,但原線程仍處於可運行的狀態,它只是讓同優先級的線程有可執行的機會。這點與sleep方法不一樣,sleep方法強制當前運行的線程暫停運行,在其甦醒或睡眠時間到期前,不能返回可運行狀態。這樣使用sleep方法可使用當前線程減慢,同時容許較低優先級的線程得到運行機會。

         Join方法的做用是讓主線程等待子線程終止。即調用join方法後的代碼須要在子線程運行結束後才能被執行。

以下:

ChildThread  t1 = …;

ParentThread  t = …;

class ParentThread extends Thread {

    ChildThread  t1;   

  ….

    public void run() {

        …..

            t1.start();   // t1.start()必須在t1.joint()被調用前被調用,使用t1線程運行起來。

            …..

         t1.join();  //若是線程被生成了,但還未被起動,調用它的join()方法是沒有做用的。

         ……          //線程t1結束後,才能運行此處(t1.join() 代碼後)的代碼。

    }

}

 

        特別注意的是:在調用線程的join方法時,該線程必須是已被運行起的線程,即已經調用了該線程的start()方法。

 

        許多文章已提到Thread類的interrupt方法不會中斷一個正在運行的線程,但會讓線程退出阻塞狀態。        當線程在調用Object類的wait(),或Thread類的join()、sleep()方法受阻時拋出異常,退出阻塞狀態同時提供了一個應用程序處理線程阻塞中斷的機會。它的本質是輪詢中斷變量標誌,這種方式並非一種搶佔式中斷。

        JDK同時廢棄了Thread類的幾個方法:stop()、susupend()、resume()方法。在應用程序中能夠經過設置變量標誌來控制或中止、結束線程。

 

任務調度框架

         JDK提供了一些任務調度框架來執行任務,這樣就不須要直接操做Thread類了。

         Timer/TimerTask任務調度是JDK中最先引進的任務調度框架。其中 Timer 負責設定 TimerTask 的起始與間隔執行時間。使用者只須要建立一個 TimerTask 的繼承類,實現其中 run 方法來定義工做任務,而後將其傳給 Timer 執行。

         Timer 的設計核心是一個 TaskList 和一個 TaskThread。Timer 將接收到的任務丟到本身的 TaskList 中,TaskList 按照 Task 的最初執行時間進行排序。TimerThread 在建立 Timer 時會啓動成爲一個守護線程。這個線程會輪詢全部任務,找到一個最近要執行的任務,而後休眠,當到達最近要執行任務的開始時間點,TimerThread 被喚醒並執行該任務。以後 TimerThread 更新最近一個要執行的任務,繼續休眠。

         Timer有兩種執行任務的模式,最經常使用的是schedule,shedule也能夠以兩種方式執行任務:1.在某個時間(Data);2.在某個固定的時間以後(int delay).

其中值得注意的方法:

1.調用TimerTask的cancel()方法,將退出該任務執行。

2.調用Timer的cancel()方法,將退出全部的任務執行。

 

        說明:Timer的scheduleAtFixedRate模式:在該模式下,Timer會盡可能讓任務在一個固定的頻率下運行。也就是說:運行場景好比是1秒鐘後MyTask 執行一次,由於系統繁忙以後的2.5秒後MyTask 才得以執行第二次,此時Timer會記下這個延遲,並嘗試在下一個任務的時候彌補這個延遲。那麼,在接下來的1.5秒後,MyTask 將執行的三次。"以固定的頻率而不是固定的延遲時間去執行一個任務」。

 

         JDK5.0後引入了新的Executor任務調度框架,其設計思想是,每個被調度的任務都會由線程池中一個線程去執行,所以任務是併發執行的,相互之間不會受到干擾。

         Executor任務調度框架主要由三個接口和其相應的具體類組成:

        •  Executor接口:執行Runnable任務的

        •  ExecutorService接口:繼承了Executor的方法,並提供了執行Callable任務和停止任務執行的服務

       •  ScheduledExecutorService接口:在ExecutorService的基礎上,提供了按時間安排執行任務的功能

       •  Executors工具類:提供獲得Executor接口的具體對象的一些靜態方法

       通常咱們能夠經過Executors工具類獲得Executor接口的具體對象,以下:

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); //建立調度執行器服務類

ExecutorService pool = Executors.newFixedThreadPool(poolSize); //建立執行器服務類

pool.submit(new Callable() {….});

 

        ScheduledExecutorService 中兩種最經常使用的調度方法 ScheduleAtFixedRate 和ScheduleWithFixedDelay。

        ScheduleAtFixedRate模式下每次執行時間爲上一次任務開始起向後推一個時間間隔,是基於固定時間間隔進行任務調度,ScheduleWithFixedDelay模式下每次執行時間爲上一次任務結束起向後推一個時間間隔,是基於不固定時間間隔進行任務調度。

 

        Timer 和 ScheduledExecutor 都提供基於開始時間與重複間隔的任務調度,雖然咱們能夠藉助Calendar類來實現一些更加複雜的調度功能,但實現上使用開源的Quartz將更簡單。

 

線程池

        線程池爲線程生命週期開銷問題和資源不足問題提供瞭解決方案(因爲減小了線程建立和銷燬的開銷)。一個線程池一般包括四個基本組成部分:線程池管理器、工做線程、任務接口、任務隊列。

       線程池的使用能夠帶服務程度性能搞高的好處,但也存在一些風險。常見的多線程編程的風險好比:死鎖、資源不足、併發錯誤等在線程池中也可能存在。線程池的另外一個比較嚴重的風險是線程泄漏,它可能由幾個緣由形成:

        緣由1: 對於一個工做線程數目固定的線程池,若是工做線程在運行任務時拋出 了異常,而這些異常或錯誤沒有被捕獲並處理,那麼這個工做線程就會異常終止而且沒有返回到池中,使得線程池永久失去了一個工做線程。當全部的工做線程都異常終止時,線程池也就最終爲空,再也不有可用的工做線程來處理新的任務了。

        緣由2:工做線程在執行一個任務時被阻塞,好比等待輸入的數據,可是因爲某些緣由用戶一直沒提供輸入數據,致使這個工做線程一直被阻塞。這樣這個工做線程實際上也不執行任何任務了。若是線程池中全部的工做線程都進入了這樣的阻塞狀態,那麼線程池就沒法處理新來的任務了。

        對於使用了線程池的程序來講,可採用打印Thread Dump來排查線程泄漏(建議每一個線程都要有本身的名稱),但對沒有使用線程池的程序來講,還須要跟蹤線程數的增加(緣由在於,一個工做線程被阻塞後,其後不斷有新增線程被阻塞,表象上就是線程數在不斷增加),當線程增加到一程度時,因爲線程的切換以及線程自己使用的資源還可能致使應用性能降低,甚至出現OutofMemenry(內存泄漏)等問題。

 

調整池的大小

        線程池的最佳大小取決於可用處理器的數目以及工做隊列中的任務的性質。若在一個具備 N 個處理器的系統上只有一個工做隊列,其中所有是計算性質的任務,在線程池具備 N 或 N+1 個線程時通常會得到最大的 CPU 利用率。

        對於那些可能須要等待 I/O 完成的任務(例如,從套接字讀取 HTTP 請求的任務),須要讓池的大小超過可用處理器的數目,由於並非全部線程都一直在工做。經過使用概要分析,您能夠估計某個典型請求的等待時間(WT)與服務時間(ST)之間的比例。若是咱們將這一比例稱之爲 WT/ST,那麼對於一個具備 N 個處理器的系統,須要設置大約 N*(1+WT/ST) 個線程來保持處理器獲得充分利用。

 

        JDK中提供了ThreadPoolExecutor的線程池,它是一種以工做隊列爲基礎的線程池的實現,這樣,咱們無須再編寫本身的線程池了。

        ThreadPoolExecutor線程池配置參數:

A.核心和最大的線程池大小:經過把corePoolSize和maximumPoolSize設置爲相同的值,能夠創建一個大小固定的線程池了。

B.根據須要構造:在默認狀況下,只有在新事務要求的時候,ThreadPoolExecutor纔開始創建和啓動核心的線程,可是可使用prestartCoreThread或prestartAllCoreThreads動態地重載它。

C.保持活動的時間:若是線程池中當前線程的數量超過了corePoolSize,那麼這些超過的線程的空閒時間大於keepAliveTime的時候,它們就會被終止。

D.排隊:排隊遵循下面的規則:

    *  若是正在運行的線程數量少於corePoolSize,Executor總會添加新線程而不會排隊。

    *  若是corePoolSize或更多數量的線程在運行,Executor總會對請求進行排隊而不會添加新線程。

    *  若是某個請求不能參與排隊,就會創建新線程,除非線程數量超過了maximumPoolSize(在超過的狀況下,該事務會被拒絕)。

E.Hook方法:這個類提供了beforeExecute()和afterExecute() hook方法,它們分別在每一個事務執行以前和以後被調用。爲了使用它們,你必須創建這個類的子類(由於這些方法是受保護的)。

        建議使用Executors 工廠方法 Executors.newCachedThreadPool()(無界線程池,能夠進行自動線程回收)、Executors.newFixedThreadPool(int)(固定大小線程池)和 Executors.newSingleThreadExecutor()(單個後臺線程)來建立線程池,它們已經能知足大多數的使用場景。

 

同步

        Java比較簡單的同步機制是使用關鍵字synchronized和Object對象方法wait/notify。同步的機制是在每個對象上擁有一個監視器(monitor),同時只容許一個線程持有監視器,而且擁有監視器的線程才容許進行對對象的訪問,那些沒有得到監視器的線程必須等待直到持有監視器的線程釋放監視器。

        關鍵字synchronized的使用有兩種方式:synchronized塊和synchronized方法。

        使用synchronized塊時應該注意到,線程所持有的對象鎖應當是共享且惟一的。這裏須要注意兩種錯誤:

         1、將synchronized關鍵字放在Thread類的run方法上,如:

public class foo extends Thread {
      …..

     public synchronized void run() {

             ….

     }
}

         放在run方法前的synchronized關鍵字其實是不能起到同步的做用。咱們知道對於一個成員方法加synchronized關鍵字,這其實是以這個成員方法所在的對象自己做爲對象鎖,這裏的使用至關於每個Thread實例對象用監視器關聯其自身。當建立多個Thread實例對象時,就會有多個不一樣的實例對象鎖,這些對象鎖並非共享且惟一的。

         可是,若是咱們將上述的run方法改成:

          public void run() {

                synchronized(foo.class) {

                       …..

                }

          }

          這樣,就建立了一個共享且惟一的對象鎖。由於咱們知道在JVM中,全部被加載的類都有惟一的類對象,無論咱們建立某個類的多少個實例,可是它們的類實例仍然是一個。上述修改其實是將監視器關聯到了foo類的類實例上了。

        注意:若是採用method級別的同步,那麼對象鎖爲method所在的對象,若是方法是靜態方法,對象鎖是method所在的類實例 (惟一);

 

         2、對一個可改變的String上使用同步塊。

         咱們知道對象通常經過new在堆中建立,當咱們用String s=new String("Hello World"); 時,實際上用將常量池中的對象「Hello World」 複製到堆中,再把堆地址交給引用變量s(Java確保一個字符串常量只有一個拷貝,「Hello World」是一個字符串常量,它們在編譯期就被肯定了,故放在經常使用量池中,而new String() 建立的字符串不是常量,不能在編譯期就肯定,因此new String()建立的字符串不放入常量池中,它們擁有本身的地址空間;這裏使用String s = new String(「Hello World」);建立了兩個對象)。

        若是對s進行修改:s = 「New World」;」New World」還是常量池中的對象,如今把引用變量s指向了字符串"New World",即用"New World"的引用地址把"Hello World"的引用地址覆蓋了。因此在修改String變量時,實際是改變了變量引用的內存地址,因此在使用用String變量做同步塊時,若是String變量發生變化,就意味着同步塊中的對象鎖已經發生了變化。

 

        關於String更詳細的內容,能夠網上搜索關鍵字  Java String 對象剖析

 

        Synchronized關鍵字提供了對每一個對象相關的隱式監視器鎖定的訪問,但同時也強制全部鎖定的得到和釋放均要在一個塊結構中,多個鎖定必須以相反的順序進行釋放。Synchronized關鍵字沒法處理「hand-over-hand」或「chain locking」:先獲取節點 A 的鎖定,而後再獲取節點 B 的鎖定,而後釋放 A 並獲取 C,而後釋放 B 並獲取 D,依此類推。

        故JDK5.0又提供了一種新的機制來處理更復雜的同步問題:Lock/Condition。

         Lock 接口實現容許鎖定在不一樣的做用範圍內獲取和釋放,並容許以任何順序獲取和釋放多個鎖定。常見的方式以下:

Lock lk = new ReentrantLock();   // ReentrantLock重入鎖是Lock的具體類

lk.lock();   //取得鎖定

try {

    // do something 對共享資源進行操做

} finally {

    lk.unlock(); //消掉鎖定,鎖自己是不會自動解鎖的

}

        Lock 實現提供了使用 synchronized 方法和語句所沒有的其餘功能,包括:

        1)一個非塊結構的獲取鎖定嘗試 (tryLock());

        2)一個獲取可中斷鎖定的嘗試 (lockInterruptibly()) ;

        3)一個獲取超時失效鎖定的嘗試 (tryLock(long, TimeUnit))。

        4)unlock():取消鎖定,須要注意的是Lock不會自動取消,編程時必須手動解鎖。

 

Lock 實例只是一個普通的對象,它自己能夠在 synchronized 語句中做爲目標使用,但建議不要混合使用Lock和synchronized

 

       Condition(條件變量) 替代了 Object 監視器方法的使用,能夠更精細控制線程等待與喚醒。(Lock 替代了 synchronized 方法和語句的使用)

         Condition(條件變量)的實例化是經過一個Lock對象上調用newCondition()方法得到的,這樣,條件就和一個鎖對象進行了綁定。Java中的條件變量只能和鎖配合使用,來控制併發程序訪問競爭資源的安全。

         經典的Producer和Consumer問題,在Java 5.0之前是由Object類的wait(), notify()和notifyAll()等方法來實現,在5.0後這些功能能夠經過Lock/Condition接口來實現了。

 

注意:使用新的Lock或ReentrantLock,最佳的實踐是結合try/finally塊來使用:在try塊以前使用lock方法,而在finally中使用unlock方法。

     

Volatile變量

        鎖具備兩種主要特性:互斥(mutual exclusion) 和可見性(visibility)。Java 語言中的 volatile 變量能夠被看做是一種 「程度較輕的 synchronized」, Volatile 變量具備 synchronized 的可見性(visibility),可是不具有原子特性或互斥(mutual exclusion)。

 

        要使 volatile 變量提供理想的線程安全,必須同時知足下面兩個條件:

        *   對變量的寫操做不依賴於當前值。

        *   該變量沒有包含在具備其餘變量的不變式中。

        第一個條件的限制使 volatile 變量不能用做線程安全計數器。雖然增量操做(x++)看上去相似一個單獨操做,實際上它是一個由讀取-修改-寫入操做序列組成的組合操做,必須以原子方式執行,而 volatile 不能提供必須的原子特性。定義爲 volatile 類型不可以充分實現類的線程安全;從而仍然須要使用同步。

        關於Volatile變量的更多內容,可參考IBM網站的《Java 理論與實踐: 正確使用 Volatile 變量》

 

ThreadLocal

        ThreadLocal源於一種多線程技術:Thread Local Storage(線程本地存儲技術)。

        ThreadLocal和其它的同步機制都是爲了解決多線程中的對同一變量的訪問衝突,在普通的同步機制中,是經過對象加鎖來實現多個線程對同一變量的安全訪問的。ThreadLocal則爲每個線程維護一個和該線程綁定的變量的副本,從而隔離了多個線程的數據。因此能夠說ThreadLocal是一種利用空間來換取時間的多線程編程解決方案。

        線程本地存儲與同步機制的區別在於:同步是爲了解決多個線程對共享資源的併發訪問,實現了多個線程之間的通訊;而ThreadLocal則隔離多個線程的數據共享。

        因此, ThreadLocal 並不解決共享對象的多線程訪問問題。一般,經過ThreadLocal.get() 獲得的的對象是該線程本身使用的對象,其餘線程是不須要訪問的,也訪問不到的。而各個線程中訪問的是不一樣的對象。
        ThreadLocal實現上主要經過內部ThreadLocalMap來實現的,使用好ThreadLocal的關鍵在使用ThreadLocal類的set()或get()方法時應分清這兩個方法是對那一個活動線程中的ThreadLocalMap進行操做。若是ThreadLocal.set()放入的數據自己是多個線程共享的,那麼線程的ThreadLocal.get()取得的仍是這個共享數據自己,仍然有併發訪問的問題。

        ThreadLocal的正確使用方法是:將ThreadLocal之內部類的形式進行繼承,並覆蓋原來的initialValue()方法,在這裏產生可供線程擁有的本地變量值。

         經過看ThreadLocal的實現代碼有助於理解ThreadLocal,ThreadLocal的相似實現代碼以下:

public class ThreadLocal<T> {   

    private final int threadLocalHashCode = nextHashCode();   

    private static int nextHashCode = 0;   

    private static final int HASH_INCREMENT = 0x61c88647;   

    private static synchronized int nextHashCode() {   

        int h = nextHashCode;   

        nextHashCode = h + HASH_INCREMENT;   

        return h;   

    }   

 

    public ThreadLocal() {   

    }   

 

    public T get() {   

        Thread t = Thread.currentThread();   

        ThreadLocalMap map = getMap(t);   

        if (map != null)   

            return (T)map.get(this);   

  

        // Maps are constructed lazily.  if the map for this thread  doesn't exist, create it, with this ThreadLocal and its   

        // initial value as its only entry.   

        T value = initialValue();   

        createMap(t, value);   

        return value;   

    }   

 

    public void set(T value) {   

        Thread t = Thread.currentThread();   

        ThreadLocalMap map = getMap(t);   

        if (map != null)   

            map.set(this, value);   

        else  

            createMap(t, value);   

    }   

      

    ThreadLocalMap getMap(Thread t) {   

        return t.threadLocals;   

    }   

      

    void createMap(Thread t, T firstValue) {   

        t.threadLocals = new ThreadLocalMap(this, firstValue);   

    }   

    .......   

    static class ThreadLocalMap {   //定製的Hash Map

       ........   

    }   

}  

 

 參考:用J2SE1.5來實現多任務的Java應用程序

         IBM Java多線程與併發編程專題  http://www.ibm.com/developerworks/cn/java/j-concurrent/

相關文章
相關標籤/搜索