JAVA多線程和線程池

目錄

1、線程狀態

(1) 新建狀態

(2) 就緒狀態

(3) 運行狀態

(4) 阻塞狀態

(5) 死亡狀態

2、線程優先級

3、同步工具synchronized、wait、notify

4、創建線程

(1) 實現 Runnable 接口

(2) 繼承 Thread 類

(3) 通過 Callable 和 Future 創建線程

5、三種創建方式的區別

6、線程池

(1) 什麼是線程池

(2) 爲什麼要有線程池

(3) 線程池可以幹什麼

(4) 線程池的創建

7、線程池的實現類(ThreadPoolExecutor)

(1) 直接提交隊列

(2) 有界的任務隊列

(3) 無界的任務隊列

(4) 優先任務隊列

(5) 幾種常見的包裝線程池類

(6) 拒絕策略

8、ThreadLocal 類

9、線程安全

10、synchronized 和 lock 的區別


1、線程狀態

線程狀態轉換

(1) 新建狀態

        使用 new 關鍵字和 Thread 類或其子類建立一個線程對象後,該線程對象就處於新建狀態。它保持這個狀態直到程序 start() 這個線程。

(2) 就緒狀態

        當線程對象調用了start()方法之後,該線程就進入就緒狀態。就緒狀態的線程處於就緒隊列中,要等待JVM裏線程調度器的調度。

(3) 運行狀態

        如果就緒狀態的線程獲取 CPU 資源,就可以執行 run(),此時線程便處於運行狀態。處於運行狀態的線程最爲複雜,它可以變爲阻塞狀態、就緒狀態和死亡狀態。

(4) 阻塞狀態

        如果一個線程執行了sleep(睡眠)、suspend(掛起)等方法,失去所佔用資源之後,該線程就從運行狀態進入阻塞狀態。在睡眠時間已到或獲得設備資源後可以重新進入就緒狀態。可以分爲三種:

  • 等待阻塞,運行狀態中的線程執行 wait() 方法,使線程進入到等待阻塞狀態。wait()釋放鎖

  • 同步阻塞,線程在獲取 synchronized 同步鎖失敗(因爲同步鎖被其他線程佔用)。

  • 其他阻塞,通過調用線程的 sleep() 或 join() 發出了 I/O 請求時,線程就會進入到阻塞狀態。當sleep() 狀態超時,join() 等待線程終止或超時,或者 I/O 處理完畢,線程重新轉入就緒狀態。sleep()不釋放鎖

(5) 死亡狀態

        一個運行狀態的線程完成任務或者其他終止條件發生時,該線程就切換到終止狀態。

2、線程優先級

        每一個 Java 線程都有一個優先級,這樣有助於操作系統確定線程的調度順序。Java 線程的優先級是一個整數,其取值範圍是1(Thread.MIN_PRIORITY)到10(Thread.MAX_PRIORITY)。默認情況下,每一個線程都會分配一個優先級 NORM_PRIORITY(5)。具有較高優先級的線程對程序更重要,並且應該在低優先級的線程之前分配處理器資源。但是,線程優先級不能保證線程執行的順序,而且非常依賴於平臺。

3、同步工具synchronized、wait、notify

        他們是應用於同步問題的人工線程調度工具。講其本質,首先就要明確monitor的概念,Java中的每個對象都有一個監視器,來監測併發代碼的重入。在非多線程編碼時該監視器不發揮作用,反之如果在synchronized 範圍內,監視器發揮作用。

        wait/notify必須存在於synchronized塊中。並且,這三個關鍵字針對的是同一個監視器(某對象的監視器)。這意味着wait之後,其他線程可以進入同步塊執行。

        當某代碼並不持有監視器的使用權時去wait或notify,會拋出java.lang.IllegalMonitorStateException。也包括在synchronized塊中去調用另一個對象的wait/notify,因爲不同對象的監視器不同,同樣會拋出此異常。

        synchronized單獨使用:

  • 代碼塊:如下,在多線程環境下,synchronized塊中的方法獲取了lock實例的monitor,如果實例相同,那麼只有一個線程能執行該塊內容

public class Thread1 implements Runnable { 
    Object lock; 
    public void run() { 
        synchronized(lock){ 
            //TODO 
        }
    }
}
  • 直接用於方法: 相當於上面代碼中用lock來鎖定的效果,實際獲取的是Thread1類的monitor。更進一步,如果修飾的是static方法,則鎖定該類所有實例。

public class Thread1 implements Runnable { 
    public synchronized void run() {
        //TODO 
    }
}

        多線程的內存模型:main memory(主存)、working memory(線程棧),在處理數據時,線程會把值從主存load到本地棧,完成操作後再save回去(volatile關鍵詞的作用:每次針對該變量的操作都激發一次load and save)。

        針對多線程使用的變量如果不是volatile或者final修飾的,很有可能產生不可預知的結果(另一個線程修改了這個值,但是之後在某線程看到的是修改之前的值)。其實道理上講同一實例的同一屬性本身只有一個副本。但是多線程是會緩存值的,本質上,volatile就是不去緩存,直接取值。在線程安全的情況下加volatile會犧牲性能。

4、創建線程

        Java 提供了三種創建線程的方法:實現 Runnable 接口、繼承 Thread 類、通過 Callable 和 Future 創建線程。

線程創建方法

(1) 實現 Runnable 接口

public class Test {
    public static void main(String[] args) { 
        MyRunnable runnable = new MyRunnable(); 
        Thread thread = new Thread(runnable); 
        thread.start();
    } 
} 
class MyRunnable implements Runnable{ 
    public MyRunnable() {
        //TODO
    } 
    @Override public void run() { 
        //TODO
    } 
}

(2) 繼承 Thread 類

public class Test { 
    public static void main(String[] args) { 
        MyThread thread = new MyThread(); 
        thread.start(); 
    } 
} 
class MyThread extends Thread{ 
    public MyThread(){ 
        //TODO
    } 
    @Override public void run() { 
        //TODO
    } 
}

        Thread類相關方法:

//當前線程客轉讓CPU控制權,讓別的就緒狀態線程運行(切換)
public static Thread.yield()

//暫停一段時間
public static Thread.slepp()

//在一個項城中調用other.join(),將等待other線程執行完後才繼續本線程
public join()

//後兩個函數皆可以被打斷
public interrupte()

        關於中斷:

        它並不像stop方法那樣會中斷一個正在運行的線程。線程會不時地檢測中斷標識位,以判斷線程是否應該被中斷(中斷標識值是否爲true)。終端只會影響到wait狀態、sleep狀態和join狀態。被打斷的線程會拋出InterruptedException。Thread.interrupted()檢查當前線程是否發生中斷,返回boolean。synchronized在獲鎖的過程中是不能被中斷的。

        中斷是一個狀態!interrupt()方法只是將這個狀態置爲true而已。所以說正常運行的程序不去檢測狀態,就不會終止,而wait等阻塞方法會去檢查並拋出異常。

        interrupt():設置當前中斷標誌位爲true;

        interrupted():檢查當前線程是否發生中斷(即中斷標誌位是否爲true)

        設置中斷標誌位後,只能通過wait()、sleep()、join()判斷標誌位,若標誌位爲true,會拋出InterruptedException異常,捕獲異常後,手動中斷線程或進行其他操作。

        不能使用try/catch來捕獲異常!需使用自定義異常處理器捕獲異常,步驟如下:

1.定義異常處理器。實現 Thread.UncaughtExceptionHandler的uncaughtException方法

//第一步:定義符合線程異常處理器規範的"異常處理器",實現Thread.UncaughtExceptionHandler規範

class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{
    //Thread.UncaughtExceptionHandler.uncaughtException()會在線程因未捕獲的異常而臨近死亡時被調用
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("caught:"+e);
    }
}
2.定義使用該異常處理器的線程工廠

//第二步:定義線程工廠。線程工廠用來將任務附着給線程,並給該線程綁定一個異常處理器

class HanlderThreadFactory implements ThreadFactory{
    @Override
    public Thread newThread(Runnable r) {
        System.out.println(this+"creating new Thread");
        Thread t = new Thread(r);
        System.out.println("created "+t);
        t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler()); //設定線程工廠的異常處理器
        System.out.println("eh="+t.getUncaughtExceptionHandler());
        return t;
    }
}

        三、四步爲測試:

3.定義一個任務,讓其拋出一個異常

//第三步:我們的任務可能會拋出異常。顯示的拋出一個exception

class ExceptionThread implements Runnable{
    @Override
    public void run() {
        Thread t = Thread.currentThread();
        System.out.println("run() by "+t);
        System.out.println("eh = "+t.getUncaughtExceptionHandler());
        throw new RuntimeException();
    }
}
4.調用實驗

//第四步:使用線程工廠創建線程池,並調用其execute方法

public class ThreadExceptionUncaughtExceptionHandler{
    public static void main(String[] args){
        ExecutorService exec = Executors.newCachedThreadPool(new HanlderThreadFactory());
        exec.execute(new ExceptionThread());
    }
}

(3) 通過 Callable 和 Future 創建線程

        上述兩種創建線程的方法,在執行完任務之後無法獲取執行結果。如果需要獲取執行結果,就必須通過共享變量或者使用線程通信的方式來達到效果。而Callable和Future可以在任務執行完畢之後得到任務執行結果。通過以下四種方法創建線程:

  • 創建 Callable 接口的實現類,並實現 call() 方法,該 call() 方法將作爲線程執行體,並且有返回值。

  • 創建 Callable 實現類的實例,使用 FutureTask 類來包裝 Callable 對象,該 FutureTask 對象封裝了該 Callable 對象的 call() 方法的返回值。

  • 使用 FutureTask 對象作爲 Thread 對象的 target 創建並啓動新線程。

  • 調用 FutureTask 對象的 get() 方法來獲得子線程執行結束後的返回值。

5、三種創建方式的區別

        (1) 繼承Thread類創建的線程可以擁有自己獨立的類成員變量,但是實現Runnable接口創建線程共享實現接口類的成員變量。兩中方式創建線程都要重寫run方法,run方法是線程的執行體。(Thread和runnable均可以實現單獨資源和共享資源)

        Eg.

        a、Thread類:

        啓動兩個線程,每個線程擁有單獨的成員變量

class MyThread extends Thread{
    //TODO 
} 
new MyThread().start(); 
new MyThread().start();

        啓動兩個線程,兩個線程共享成員變量

MyThread m = new MyThread(); 
new Thread(m).start(); 
new Thread(m).start();

        b、Runnable接口:

        啓動兩個線程,共同享有成員變量

class MyThread implements Runnable { 
    //TODO
}

MyThread m = new MyThread(); 
new Thread(m).start(); 
new Thread(m).start();

        啓動兩個線程,每個線程擁有單獨的成員變量

MyThread myThread = new MyThread(); 
MyThread myThread2 = new MyThread(); 
new Thread(myThread).start(); 
new Thread(myThread2).start();

        (2) 在繼承Thread類創建進程中可以通過使用this獲得當前進程的對象,但是在實現Runnable接口創建線程的途徑中可以使用Thread.currentThread()方式來獲得當前進程。

        (3) 第三種方式是較爲複雜的一種。Callable接口是一個與Runnable接口十分相似的接口。在Runnable中run方法爲線程的執行體,但是在Callable接口中call方法是線程的執行體。下面是兩個接口實現執行體的不同:

  • call方法有返回值,但是run方法沒有

  • call方法可以生命拋出異常

        所以可以說Callable接口是Runnable接口的增強版本。

        (4)  FutureTask類實現了Runnable和Future接口。和Callable一樣都是泛型。

        (5)  Future接口是對Callable任務的執行結果進行取消,查詢是否完成,獲取結果的。下面是這個接口的幾個重要方法:

  • boolean cancel(boolean myInterruptRunning),試圖取消Future與Callable關聯的任務

  •  V get(), 返回Callable任務中call方法的返回值,調用該方法會導致程序阻塞,必須等到子線程結束纔會有返回值。這裏V表示泛型

  • V get(long timeout, TimeUnit  unit), 返回Callable中call方法的返回值,該方法讓程序最多阻塞timeout毫秒的時間,或者直到unit時間點。如果在指定的時間Callable的任務沒有完成就會拋出異常TimeoutEexception

  • boolean  isCancelled(), 如果Callable中的任務被取消,則返回true,否則返回false

  • boolean isDone(),如果Callable中的任務被完成,則返回true,否則返回false

6、線程池

(1) 什麼是線程池

        線程池,其實就是一個容納多個線程的容器,其中的線程可以反覆使用,省去了頻繁創建線程對象的操作,無需反覆創建線程而消耗過多資源

(2) 爲什麼要有線程池

        在java中,如果每個請求到達就創建一個新線程,開銷是相當大的。在實際使用中,創建和銷燬線程花費的時間和消耗的系統資源都相當大,甚至可能要比在處理實際的用戶請求的時間和資源要多的多。除了創建和銷燬線程的開銷之外,活動的線程也需要消耗系統資源。如果在一個jvm裏創建太多的線程,可能會使系統由於過度消耗內存或"切換過度"而導致系統資源不足。爲了防止資源不足,需要採取一些辦法來限制任何給定時刻處理的請求數目,儘可能減少創建和銷燬線程的次數,特別是一些資源耗費比較大的線程的創建和銷燬,儘量利用已有對象來進行服務。

(3) 線程池可以幹什麼

        線程池主要用來解決線程生命週期開銷問題和資源不足問題。通過對多個任務重複使用線程,線程創建的開銷就被分攤到了多個任務上了,而且由於在請求到達時線程已經存在,所以消除了線程創建所帶來的延遲。這樣,就可以立即爲請求服務,使用應用程序響應更快;另外,通過適當的調整線程中的線程數目可以防止出現資源不足的情況。

(4) 線程池的創建

        線程池都是通過線程池工廠創建,再調用線程池中的方法獲取線程,再通過線程去執行任務方法。

  • Executors:線程池創建工廠類

  • public static ExecutorService newFixedThreadPool(int nThreads):返回線程池對象

  • ExecutorService:線程池類

  • Future<?> submit(Runnable task):獲取線程池中的某一個線程對象,並執行

  • Future 接口:用來記錄線程任務執行完畢後產生的結果。線程池創建與使用

        a、使用Runnable接口創建線程池

  • 創建線程池對象

  • 關閉線程池

  • 提交 Runnable 接口子類對象

  • 創建 Runnable 接口子類對象

public static void main(String[] args) {
    //創建線程池對象  參數5,代表有5個線程的線程池
    ExecutorService service = Executors.newFixedThreadPool(5);

    //創建Runnable線程任務對象
    TaskRunnable task = new TaskRunnable();
        
    //從線程池中獲取線程對象
    service.submit(task);
    System.out.println("----------------------");
        
    //再獲取一個線程對象
    service.submit(task);
        
    //關閉線程池
    service.shutdown();
}

        b、使用Callable接口創建線程池

        ExecutorService:線程池類

        <T> Future<T> submit(Callable<T> task):獲取線程池中的某一個線程對象,並執行線程中的 call() 方法

        Future 接口:用來記錄線程任務執行完畢後產生的結果。線程池創建與使用

  • 創建線程池對象

  • 創建 Callable 接口子類對象

  • 提交 Callable接口子類對象

  • 關閉線程池

public static void main(String[] args) {
       
    ExecutorService service = Executors.newFixedThreadPool(3);
    TaskCallable c = new TaskCallable();
        
    //線程池中獲取線程對象,調用run方法
    service.submit(c);
        
    //再獲取一個
    service.submit(c);
        
    //關閉線程池
    service.shutdown();
}

7、線程池的實現類(ThreadPoolExecutor)

        ThreadPoolExecutor 類繼承了 AbstractExecutorService 類,而 AbstractExecutorService 類實現了 ExecutorService 接口。所以上述線程池創建的方法可以將創建的線程池(例如newFixedThreadPool)賦給 ExecutorService。

        ThreadPoolExecutor 構造函數如下:

ThreadPoolExecutor ( int corePoolSize, // 線程池中的線程數量

                    int maximumPoolSize, // 線程池中的最大線程數量

                    long keepAliveTime, // 當線程池線程數量超過corePoolsize時,多餘的空閒線程的存活時間,即超過corePoolSize的空閒線程,在keepAliveTime時間內會被銷燬
                    
                    TimeUnit unit, // keepAliveTime的單位

                    BlockingQueue<Runnable> workQueue, // 任務隊列,被提交但尚未被執行的任務

                    ThreadFactory threadFactory, // 線程工廠,用於創建線程,一般用默認的線程工廠即可

                    RejectedExecutionHandler handler) // 拒絕策略。當任務太多來不及處理時,如何拒絕任務

        關鍵參數:workQueue

(1) 直接提交隊列

        由 SynchronousQueue 實現,一種無緩衝的等待隊列。

        SynchronousQueue 沒有容量,即沒有等待隊列,總是將新任務提交給線程去執行,當沒有空閒線程時,就新增一個線程,當線程數達到最大值maximumPoolSize時,即無法再新增線程時,則執行拒絕策略。

(2) 有界的任務隊列

        由 ArrayBlockingQueue 實現,其內部維護了一個定長數組,用於儲存隊列,其內部還保存着兩個整形變量,分別標識着隊列的頭部和尾部在數組中的位置。ArrayBlockingQueue 在生產者放入數據和消費者獲取數據時,都是共用同一個鎖對象,由此也意味着兩者無法真正並行運行,這點尤其不同於 LinkedBlockingQueue;按照實現原理來分析,ArrayBlockingQueue 完全可以採用分離鎖,從而實現生產者和消費者操作的完全並行運行。之所以沒這樣去做,也許是因爲 ArrayBlockingQueue 的數據寫入和獲取操作已經足夠輕巧,以至於引入獨立的鎖機制,除了給代碼帶來額外的複雜性外,其在性能上完全佔不到任何便宜。 ArrayBlockingQueue 和LinkedBlockingQueue 間還有一個明顯的不同之處在於,前者在插入或刪除元素時不會產生或銷燬任何額外的對象實例,而後者則會生成一個額外的 Node 對象。這在長時間內需要高效併發地處理大批量數據的系統中,其對於GC的影響還是存在一定的區別。而在創建ArrayBlockingQueue 時,我們還可以控制對象的內部鎖是否採用公平鎖,默認採用非公平鎖。

        當有新任務需要執行時,如果線程池的實際線程數小於 corePoolSize,則會新增一個線程,若大於 corePoolSize,則會將新任務加入等待隊列。若隊列已滿,則在總線程數不大於maximumPoolSize 的前提下,新增一個線程,若大於 maximumPoolSize,則執行拒絕策略。也就是說,只有當等待隊列滿了的時候,纔可能將線程數增加到 corePoolSize 以上。也就是說,除非系統非常繁忙,否則線程數量基本維持在 corePoolSize。

(3) 無界的任務隊列

        由 LinkedBlockingQueue 實現,其內部維護了一個鏈表(如果沒有指定長度,則默認容量爲無窮大),LinkedBlockingQueue 之所以能夠高效的處理併發數據,是因爲其對於生產者端和消費者端分別採用了獨立的鎖來控制數據同步,這也意味着在高併發的情況下生產者和消費者可以並行地操作隊列中的數據,以此來提高整個隊列的併發性能。作爲開發者,我們需要注意的是,如果構造一個 LinkedBlockingQueue 對象,而沒有指定其容量大小,LinkedBlockingQueue 會默認一個類似無限大小的容量(Integer.MAX_VALUE),這樣的話,如果生產者的速度一旦大於消費者的速度,也許還沒有等到隊列滿阻塞產生,系統內存就有可能已被消耗殆盡了。

        與有界隊列相比,除非系統資源耗盡,否則無界隊列不存在任務入隊失敗的情況,即無界隊列的長度是無窮大。當有新的任務需要執行時,若線程池的實際數量小於corePoolSize,則會新增一個線程,且線程池的最大線程數爲corePoolSize。若生產者的速度遠小於消費者的速度,則等待隊列會快速增長,直至系統資源耗盡。

(4) 優先任務隊列

        由 PriorityBlockingQueue 實現,其內部維護了一個數組,優先級的判斷通過構造函數傳入的 Comparator 對象來決定,需要注意的是 PriorityBlockingQueue 並不會阻塞數據生產者,而只會在沒有可消費的數據時,阻塞數據的消費者。因此使用的時候要特別注意,生產者生產數據的速度絕對不能快於消費者消費數據的速度,否則時間一長,會最終耗盡所有的可用堆內存空間。在實現 PriorityBlockingQueue 時,內部控制線程同步的鎖採用的是公平鎖。

這是一個有優先級的無界隊列。

(5) 幾種常見的包裝線程池類

  • newFixedThreadPool:設置 corePoolSize 和 maximumPoolSize 相等,使用無界的任務隊列(LinkedBlockingQueue)

  • newSignalThreadExecutor:newFixedThreadPool 的一種特殊形式,即 corePoolSize爲1

  • newCachedThreadPool:設置 corePoolSize 爲0,maximumPoolSize 爲無窮大,使用直接提交隊列(SynchronousQueue)

(6) 拒絕策略

  • AbortPolicy:直接拋出異常,阻止系統正常工作

  • CallerRunsPolicy:由調用線程直接執行當前任務,可能會造成任務提交線程(即調用線程)的性能急劇下降

  • DiscardOldestPolicy:丟棄等待隊列頭的一個任務,並再次提交該任務

  • DiscardPolicy:丟棄提交任務,即什麼都不做

8、ThreadLocal 類

        用處:保存線程的獨立變量。對一個線程類(繼承自Thread),當使用ThreadLocal維護變量時,ThreadLocal爲每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。常用於用戶登錄控制,如記錄session信息。

        實現:每個Thread都持有一個TreadLocalMap類型的變量(該類是一個輕量級的Map,功能與map一樣,區別是桶裏放的是entry而不是entry的鏈表。功能還是一個map。)以本身爲key,以目標爲value。主要方法是get()和set(T a),set之後在map裏維護一個threadLocal -> a,get時將a返回。ThreadLocal是一個特殊的容器。

        ThreadLocal和線程同步機制都是爲了解決多線程中相同變量的訪問衝突問題。同步機制採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而後者爲每一個線程都提供了一份變量,因此可以同時訪問而互不影響。

        #########################################################################

        每個Thread持有一個ThreadLocalMap,key-value,key爲弱引用,value爲強引用,解決方法間共享和實例線程間隔離;靜態變量只能解決方法間共享

        某一個類的多個實例線程,多個類的多個實例線程

        ThreadLocal解決的是某類的多個實例線程間的隔離問題,ThreadLocal屬於一個類,是一個類的私有變量。

        普通的私有變量可以保證不同類之間相互隔離,不能保證同一個類不同實例間的相互隔離,而ThreadLocal卻可以。例如,

public class ThreadRunnableDemo implement Runnable{ 
    private int a; 
    private ThreadLocal<String> b; 
}

        (1) 分別使用不同 Runnable 生成 Thread

Thread t1 = new Thread(new ThreadRunnableDemo ()); 
Thread t2 = new Thread(new ThreadRunnableDemo ());

        (2) 用同一個 Runnable 生成 Thread

ThreadRunnableDemo t = new ThreadRunnableDemo (); 
Thread t1 = new Thread(t); 
Thread t2 = new Thread(t);

        對於上述兩種情況:

        a、(1) 中可保證變量a是線程間隔離的(即線程安全的);而 (2) 中卻不可以,因爲他們用同一個Runnable去生成Thread,由於變量a是屬於Runnable的,所以會產生線程安全問題。

        b、(1)(2)中 ThreadLocal 是線程安全的,即使使用同一個Runnable生成Thread,他們也各自使用一個變量副本。

9、線程安全

        Synchronized、volatile、原子類、Lock鎖

10、synchronized 和 lock 的區別

        Synchronized是關鍵字;不支持等待超時中斷;讀操作之間仍然是互斥的,不能同時進行;不需要手動釋放鎖

        Lock是一個類;可以只等待一定的時間或者能夠響應中斷;讀操作之間不互斥,可以同時進行;需要手動釋放鎖

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章