Java多線程與併發筆記

synchronized

synchronized主要是用於解決線程安全問題的,而線程安全問題的主要誘因有以下兩點:java

  • 存在共享數據(也稱臨界資源)
  • 存在多條線程共同操做這些共享數據

解決線程安全問題的根本方法:算法

  • 同一時刻有且只有一個線程在操做共享數據,其餘線程必須等到該線程處理完數據後再對共享數據進行操做

因此互斥鎖是解決問題的辦法之一,互斥鎖的特性以下:數組

互斥性:即在同一時間只容許一個線程持有某個對象鎖,經過這種特性來實現多線程的協調機制,這樣在同一時間只有一個線程對須要同步的代碼塊(複合操做)進行訪問。互斥性也稱爲操做的原子性
可見性:必須確保在鎖被釋放以前,對共享變量所作的修改,對於隨後得到該鎖的另外一個線程是可見的(即在得到鎖時應得到最新共享變量的值),不然另外一個線程多是在本地緩存的某個副本上繼續操做,從而引發數據不一致問題緩存

而synchronized就能夠實現互斥鎖的特性,不過須要注意的是synchronized鎖的不是代碼,而是對象。安全

根據獲取的鎖能夠分爲兩類:數據結構

  • 對象鎖:獲取對象鎖有兩種用法
    1. 同步代碼塊(synchronized(this),synchronized(類實例對象)),鎖是小括號()中的實例對象
    2. 同步非靜態方法(synchronized method),鎖是當前的實例對象,即this
  • 類鎖:獲取類鎖也有兩種用法
    1. 同步代碼塊(synchronized(類.class)),鎖是小括號()中的類對象(Class對象)
    2. 同步靜態方法(synchronized static method),鎖是當前對象的類對象(Class對象)

對象鎖和類鎖的總結:多線程

  1. 有線程訪問對象的同步塊代碼時,另外的線程能夠訪問該對象的非同步代碼塊
  2. 若鎖住的是同一個對象,一個線程在訪問對象的同步代碼塊時,另外一個訪問對象的同步代碼塊的線程會被阻塞
  3. 若鎖住的是同一個對象,一個線程在訪問對象的同步方法時,另外一個訪問對象同步方法的線程會被阻塞
  4. 若鎖住的是同一個對象,一個線程在訪問對象的同步塊時,另外一個訪問對象同步方法的線程會被阻塞,反之亦然
  5. 同一個類的不一樣對象的對象鎖互不干擾
  6. 類鎖因爲也是一把特殊的對象鎖,所以表現與上述1,2,3,4一致,而因爲一個類只有一把對象鎖,因此同一個類的不一樣對象使用類鎖將會是同步的
  7. 類鎖和對象鎖互補干擾,由於類對象和實例對象不是同一個對象

synchronized底層實現原理

實現synchronized須要依賴兩個基礎概念:併發

  • Java對象頭
  • Monitor

Java對象在內存中的佈局主要分爲三塊區域:app

  • 對象頭
  • 實例數據
  • 對齊填充

synchronized使用的鎖對象是存儲在Java對象頭裏的,對象頭結構以下:
Java多線程與併發筆記框架

因爲對象頭信息是與對象自身定義的數據沒有關係的額外存儲成本,考慮到JVM的空間效率,Mark Word被設計爲非固定的數據結構以便存儲更多有效的數據,它會根據對象自身的狀態賦予本身的存儲空間:
Java多線程與併發筆記

簡單介紹了對象頭,接着咱們來了解一下Monitor,每一個Java對象天生自帶了一把看不見的鎖,它叫作內部鎖或Monitor鎖。Monitor的主要實現代碼在ObjectMonitor.hpp中:
Java多線程與併發筆記

Monitor鎖的競爭、獲取與釋放:
Alt text

而後咱們從字節碼層面上看一下synchronized,將以下代碼經過javac編譯成class文件:

package com.example.demo.thread;

/**
 * @author 01
 * @date 2019-07-20
 **/
public class SyncBlockAndMethod {

    public void syncsTask() {
        synchronized (this) {
            System.out.println("Hello syncsTask");
        }
    }

    public synchronized void syncTask() {
        System.out.println("Hello syncTask");
    }
}

而後經過 javap -verbose 將class文件反編譯成可閱讀的字節碼內容,以下:

Classfile /E:/Java_IDEA/demo/src/main/java/com/example/demo/thread/SyncBlockAndMethod.class
  Last modified 2019年7月20日; size 637 bytes
  MD5 checksum 7600723349daa088a5353acd84c80fa5
  Compiled from "SyncBlockAndMethod.java"
public class com.example.demo.thread.SyncBlockAndMethod
  minor version: 0
  major version: 55
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #6                          // com/example/demo/thread/SyncBlockAndMethod
  super_class: #7                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 3, attributes: 1
Constant pool:
   #1 = Methodref          #7.#18         // java/lang/Object."<init>":()V
   #2 = Fieldref           #19.#20        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #21            // Hello syncsTask
   #4 = Methodref          #22.#23        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = String             #24            // Hello syncTask
   #6 = Class              #25            // com/example/demo/thread/SyncBlockAndMethod
   #7 = Class              #26            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               syncsTask
  #13 = Utf8               StackMapTable
  #14 = Class              #27            // java/lang/Throwable
  #15 = Utf8               syncTask
  #16 = Utf8               SourceFile
  #17 = Utf8               SyncBlockAndMethod.java
  #18 = NameAndType        #8:#9          // "<init>":()V
  #19 = Class              #28            // java/lang/System
  #20 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #21 = Utf8               Hello syncsTask
  #22 = Class              #31            // java/io/PrintStream
  #23 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #24 = Utf8               Hello syncTask
  #25 = Utf8               com/example/demo/thread/SyncBlockAndMethod
  #26 = Utf8               java/lang/Object
  #27 = Utf8               java/lang/Throwable
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{
  public com.example.demo.thread.SyncBlockAndMethod();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0

  public void syncsTask();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter                      // 指向同步代碼塊的開始位置
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #3                  // String Hello syncsTask
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit                       // 指向同步代碼塊的結束位置,monitorenter和monitorexit之間就是同步代碼塊
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit                       // 若代碼發生異常時就會執行這句指令釋放鎖
        20: aload_2
        21: athrow
        22: return
      Exception table:
         from    to  target type
             4    14    17   any
            17    20    17   any
      LineNumberTable:
        line 10: 0
        line 11: 4
        line 12: 12
        line 13: 22
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 17
          locals = [ class com/example/demo/thread/SyncBlockAndMethod, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public synchronized void syncTask();
    descriptor: ()V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED  // 用於標識是一個同步方法,不須要像同步塊那樣須要經過顯式的字節碼指令去標識哪裏須要獲取鎖,哪裏須要釋放鎖。同步方法不管是正常執行仍是發生異常都會釋放鎖
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String Hello syncTask
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 16: 0
        line 17: 8
}
SourceFile: "SyncBlockAndMethod.java"

什麼是重入:

從互斥鎖的設計上來講,當一個線程試圖操做一個由其餘線程持有的對象鎖的臨界資源時,將會處於阻塞狀態,但當一個線程再次請求本身持有對象鎖的臨界資源時,這種狀況屬於重入

爲何會對synchronized嗤之以鼻:

  • 在早期版本中,synchronized屬於重量級鎖效率低下,由於依賴於Mutex Lock實現,由於線程之間的切換須要從用戶態轉換到核心態,開銷較大。不過在Java6之後引入了許多鎖優化機制,synchronized性能已經獲得了很大的提高

鎖優化之自旋鎖:

許多狀況下,共享數據的鎖定狀態持續時間較短,切換線程不值得。因而自旋鎖應運而生,所謂自旋就是經過讓線程執行忙循環等待鎖的釋放,從而不讓出CPU時間片,例如while某個標識變量

缺點:若鎖被其餘線程長時間佔用,將會帶來許多性能上的開銷,因此通常超過指定的自旋次數就會將線程掛起處於阻塞狀態

鎖優化之自適應自旋鎖:

自適應自旋鎖與普通自旋鎖不一樣的就是能夠自適應自旋次數,即自旋次數再也不固定。而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定

鎖優化之鎖消除,鎖消除是JVM另外一種鎖優化,這種優化更完全:

在JIT編譯時,對運行上下文進行掃描,去除不可能存在資源競爭的鎖。這種方式能夠消除沒必要要的鎖,能夠減小毫無心義的請求鎖時間

關於鎖消除,咱們能夠看一個例子,代碼以下:

public class StringBufferWithoutSync {

    public void add(String str1, String str2) {
        //StringBuffer是線程安全,因爲sb只會在append方法中使用,不可能被其餘線程引用
        //所以sb屬於不可能共享的資源,JVM會自動消除內部的鎖
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }

    public static void main(String[] args) {
        StringBufferWithoutSync withoutSync = new StringBufferWithoutSync();
        for (int i = 0; i < 1000; i++) {
            withoutSync.add("aaa", "bbb");
        }
    }
}

鎖優化之鎖粗化,咱們再來了解鎖粗化的概念,有些狀況下可能會須要頻繁且重複進行加鎖和解鎖操做,例如同步代碼寫在循環語句裏,此時JVM會有鎖粗化的機制,即經過擴大加鎖的範圍,以免反覆加鎖和解鎖操做。代碼示例:

public class CoarseSync {

    public static String copyString100Times(String target){
        int i = 0;
        // JVM會將鎖粗化到外部,使得重複的加解鎖操做只須要進行一次
        StringBuffer sb = new StringBuffer();
        while (i < 100){
            sb.append(target);
        }

        return sb.toString();
    }
}

synchronized鎖存在四種狀態:

  • 無鎖、偏向鎖、輕量級鎖、重量級鎖
  • 鎖膨脹的方向:無鎖 -> 偏向鎖 -> 輕量級鎖 -> 重量級鎖
  • 鎖膨脹存在跨級現象,例如直接從無鎖膨脹到重量級鎖

偏向鎖:

大多數狀況下,鎖不存在多線程競爭,老是由同一線程屢次得到,爲了減小同一線程獲取鎖的代價,就會使用偏向鎖

核心思想:
若是一個線程得到了鎖,那麼鎖就進入偏向模式,此時Mark Word的結構也變爲偏向鎖結構,當該線程再次請求鎖時,無需再作任何同步操做,即獲取鎖的過程只須要檢查Mark Word的鎖標記位爲偏向鎖以及當前線程id等於Mark Word的ThreadID便可,這樣就省去了大量有關鎖申請的操做,那麼這個鎖也就偏向於該線程了

偏向鎖不適用於鎖競爭比較激烈的多線程場合

輕量級鎖:

輕量級鎖是由偏向鎖升級而來,偏向鎖運行在一個線程進入同步塊的狀況下,當有第二個線程加入鎖競爭時,偏向鎖就會升級爲輕量級鎖

適用場景:線程交替執行同步塊

若存在線程同一時間訪問同一鎖的狀況,就會致使輕量級鎖膨脹爲重量級鎖

輕量級鎖的加鎖過程:

  1. 在代碼進入同步塊的時候,若是同步對象鎖狀態爲無鎖狀態(鎖標誌位爲「01」狀態),虛擬機首先將在當前線程的棧幀中創建一個名爲鎖記錄(LockRecord)的空間,用於存儲鎖對象目前的Mark Word的拷貝,官方稱之爲Displaced Mark Word。這時候線程堆棧與對象頭的狀態以下圖所示:
    Java多線程與併發筆記

  2. 拷貝對象頭中的Mark Word複製到鎖記錄中
  3. 拷貝成功後,虛擬機將使用CAS操做嘗試將對象的Mark Word更新爲指向Lock Record的指針,並將Lock record裏的owner指針指向object mark word。若是更新成功,則執行步驟4,不然執行步驟5
  4. 若是這個更新動做成功了,那麼這個線程就擁有了該對象的鎖,而且對象Mark Word的鎖標誌位設置爲「00",即表示此對象處於輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態以下圖所示:
    Java多線程與併發筆記

  5. 若是這個更新操做失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,若是是就說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行。不然說明多個線程競爭鎖,輕量級鎖就要膨脹爲重量級鎖,鎖標誌的狀態值變爲「10",Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。而當前線程便嘗試使用自旋來獲取鎖,自旋我們前面講過,就是爲了避免讓線程阻塞,而採用循環去獲取鎖的過程

輕量級鎖的解鎖過程:

  1. 經過CAS操做嘗試把線程中複製的Displaced Mark Word對象替換當前的Mark Word
  2. 若是替換成功,整個同步過程就完成了
  3. 若是替換失敗,說明有其餘線程嘗試過獲取該鎖(此時鎖己膨脹),那就要在釋放鎖的同時,喚醒被掛起的線程

鎖的內存語義:

當線程釋放鎖時,Java內存模型會把該線程對應的本地內存中的共享變量刷新到主內存中;而當線程獲取鎖時,Java內存模型會把該線程對應的本地內存置爲無效,從而使得被監視器保護的臨界區代碼必須從主內存中讀取共享變量
Java多線程與併發筆記

偏向鎖、輕量級鎖、重量級鎖的彙總:
Java多線程與併發筆記


synchronized和ReentrantLock的區別

在JDK1.5以前,synchronized是Java惟一的同步手段,而在1.5以後則有了ReentrantLock類(重入鎖):

  • 位於java.util.concurrent.locks包
  • 和CountDownLatch、FuturaTask、Semaphore同樣基於AQS框架實現
  • 可以實現比synchronized更細粒度的控制,如控制fairness
  • 調用lock以後,必須調用unlock釋放鎖
  • 在JDK6以後性能未必比synchronized高,而且也是可重入的

ReentrantLock公平性的設置:

  • ReentrantLock fairLock = new ReentrantLock(true);
  • 參數爲true時,傾向於將鎖賦予等待時間最久的線程,即設置爲所謂的公平鎖,公平性是減小線程飢餓的一個辦法
  • 公平鎖:獲取鎖的順序按前後調用lock方法的順序,公平鎖需慎用,由於會影響性能
  • 非公平鎖:線程搶佔鎖的順序不必定,與調用順序無關,看運氣
  • synchronized是非公平鎖

ReentrantLock的好處在於將鎖對象化了,所以能夠實現synchronized難以實現的邏輯,例如:

  • 判斷是否有線程,或者某個特定線程,在排隊等待獲取鎖
  • 帶超時的獲取鎖的嘗試
  • 感知有沒有成功獲取鎖

若是說ReentrantLock將synchronized轉變爲了可控的對象,那麼是否能將wait、notify及notifyall等方法對象化,答案是有的,即Condition:

  • 位於java.util.concurrent.locks包
  • 能夠經過ReentrantLock的newCondition方法獲取該Condition對象實例

synchronized和ReentrantLock的區別:

  • synchronized是關鍵字,ReentrantLock是類
  • ReentrantLock能夠對獲取鎖的等待時間進行設置,避免死鎖
  • ReentrantLock能夠獲取各類鎖的信息
  • ReentrantLock能夠靈活地實現多路通知
  • 內部機制:synchronized操做的是Mark Word,而ReentrantLock底層是調用Unsafe類的park方法來加鎖

jmm的內存可見性

Java內存模型(JMM):

Java內存模型(Java Memory Model,簡稱JMM)自己是一種抽象的概念,並不真實存在,它描述的是一組規則或規範,經過這組規範定義了程序中各個變量(包括實例字段,靜態字段和構成數組對象的元素)的訪問方式

Java多線程與併發筆記

JMM中的主內存(即堆空間):

  • 存儲Java實例對象
  • 包括成員變量、類信息、常量、靜態變量等
  • 屬於數據共享的區域,多線程併發操做時會引起線程安全問題

JMM中的工做內存(即本地內存,或線程棧):

  • 存儲當前方法的全部本地變量信息,本地變量對其餘線程不可見
  • 字節碼行號指示器、Native方法信息
  • 屬於線程私有數據區域,不存在線程安全問題

JMM與Java內存區域劃分(即Java內存結構)是不一樣的概念層次:

  • JMM描述的是一組規則,經過這組控制程序中各個變量在共享數據區域和私有數據區域的訪問方式,JMM是圍繞原子性、有序性及可見性展開的
  • 二者類似點:存在共享數據區域和私有數據區域

主內存與工做內存的數據存儲類型以及操做方式概括:

  • 方法裏的基本數據類型本地變量將直接存儲在工做內存的棧幀結構中
  • 引用類型的本地變量,則是其引用存儲在工做內存中,而具體的實例存儲在主內存中
  • 對象的成員變量、static變量、類信息均會被存儲在主內存中
  • 主內存共享的方式是線程各拷貝一份數據到工做內存,操做完成後刷新回主內存

JMM如何解決可見性問題:
Java多線程與併發筆記

指令重排序須要知足的條件:

  • 在單線程環境下不能改變程序運行的結果
  • 存在數據依賴關係的不容許重排序
  • 以上兩點能夠歸結爲:沒法經過happens-before原則推導出來的,才能進行指令的重排序

什麼是Java內存模型中的happens-before:

  • 若是兩個操做不知足下述任意一個happens-before原則,那麼這兩個操做就沒有順序的保障,JVM能夠對這兩個操做進行重排序
  • 若是操做A happens-before 操做B,那麼操做A在內存上所作的操做對操做B都是可見的
  • 若A操做的結果須要對B操做可見,則A與B存在happens-before關係

happens-before的八大原則:

  1. 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操做先行發生於書寫在後面的操做
  2. 鎖定規則:一個unLock操做先行發生於後面對同一個鎖的lock操做
  3. volatile變量規則:對一個變量的寫操做先行發生於後面對這個變量的讀操做(保證了可見性)
  4. 傳遞規則:若是操做A先行發生於操做B,而操做B又先行發生於操做C,則能夠得出操做A先行發生於操做C
  5. 線程啓動規則:Thread對象的start()方法先行發生於此線程的每個動做
  6. 線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生
  7. 線程終結規則:線程中全部的操做都先行發生於線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行
  8. 對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始

volatile:

  • 是JVM提供的輕量級同步機制
  • JVM保證被volatile修飾的共享變量對全部線程老是可見的
  • 禁止指令的重排序優化
  • 使用volatile不能保證線程安全,須要變量的操做知足原子性

volatile變量爲什麼當即可見?簡單來講:

  • 當寫一個volatile變量時,JMM會把該線程對應的工做內存中的共享變量值刷新到主內存中
  • 當讀取一個volatile變量時,JMM會把該線程對應的工做內存置爲無效,那麼就須要從主內存中從新讀取該變量

volatile變量如何禁止重排序優化:

  • 對此咱們須要先了解內存屏障(Memory Barrier),其做用有二:
    1. 保證特定操做的執行順序
    2. 保證某些變量的內存可見性
  • 經過插入內存屏障指令來禁止對內存屏障先後的指令執行重排序優化
  • 強制刷出各類CPU的緩存數據,所以任何CPU上的線程都能讀取到這些數據的最新版本

volatile和synchronized的區別:

  1. volatile本質是在告訴JVM當前變量在寄存器(工做內存)中的值是不肯定的,須要從主存中讀取;synchronized則是鎖定當前變量,只有當前線程能夠訪問該變量,其餘線程被阻塞住直到該線程完成變量操做爲止
  2. volatile僅能使用在變量級別;synchronized則可使用在變量、方法和類級別
  3. volatile僅能實現變量的修改可見性,不能保證原子性;而synchronized則能夠保證變量修改的可見性和原子性
  4. volatile不會形成線程的阻塞;synchronized可能會形成線程的阻塞
  5. volatile標記的變量不會被編譯器優化;synchronized標記的變量能夠被編譯器優化

CAS

CAS(Compare and Swap)是一種線程安全性的方法:

  • 支持原子更新操做,適用於計數器,序列發生器等場景
  • 屬於樂觀鎖機制,號稱lock-free
  • CAS操做失敗時由開發者決定是繼續嘗試,仍是執行別的操做

CAS思想:

  • 包含三個操做數:內存位置(V)、預期原值(A)和新值(B)

CAS多數狀況下對開發者來講是透明的:

  • J.U.C的atomic包提供了經常使用的原子性數據類型以及引用、數組等相關原子類型和更新操做工做,是不少線程安全程序的首選
  • Unsafe類雖然提供CAS服務,但因可以操縱任意內存地址讀寫而有隱患
  • Java9之後,可使用Variable Handle API來代替Unsafe

缺點:

  • 若循環時間長,則開銷很大
  • 只能保證一個共享變量的原子操做
  • 存在ABA問題,能夠經過使用AtomicStampedReference來解決,但因爲是經過版本標記來解決因此存在必定程度的性能損耗

Java線程池

利用Executors建立不一樣的線程池知足不一樣場景的需求:

  1. newFixedThreadPool(int nThreads):指定工做線程數量的線程池
  2. newCachedThreadPool():處理大量短期工做任務的線程池,特色:
  3. 試圖緩存線程並重用,當無緩存線程可用時,就會建立新的工做線程
  4. 若是線程閒置的時間超過閾值,則會被終止並移出緩存
  5. 系統長時間閒置的時候,不會消耗什麼資源
  6. newSingleThreadExecutor():建立惟一的工做者線程來執行任務,若是線程異常結束,會有另外一個線程取代它
  7. newSingleThreadScheduledExecutor()與newScheduledThreadPool(int corePoolSize):定時或者週期性的工做調度,二者的區別在於單一工做線程仍是多個線程
  8. JDK8新增的newWorkStealingPool():內部會構建ForkJoinPool ,利用working-stealing算法,並行地處理任務,不保證處理順序
    • working-stealing算法:某個線程從其餘線程的任務隊列裏竊取任務來執行

Fork/Join框架(JDK7提供):

  • 是一個能夠把大任務分割成若干個小任務並行執行,最終彙總每一個小任務結果後獲得大任務結果的框架

Java多線程與併發筆記

爲何要使用線程池:

  1. 減低資源消耗,避免頻繁地建立和銷燬線程
  2. 提升線程的可管理性,例如可控的線程數量,線程狀態的監控和統一建立/銷燬線程

Executor的框架:
Java多線程與併發筆記

J.U.C的三個Executor接口:

  • Executor:運行新任務的簡單接口,將任務提交和任務執行細節解耦
  • ExecutorService:具有管理執行器和任務生命週期的方法,提交任務機制更完善
  • ScheduleExecutorService:支持Future和按期執行任務

線程池執行任務流程圖:
Java多線程與併發筆記

ThreadPoolExecutor的七個構造器參數:

  • int corePoolSize:核心線程數
  • int maximumPoolSize:最大線程數
  • long keepAliveTime:線程空閒存活時間
  • TimeUnit unit:存活時間的單位
  • BlockingQueue&lt;Runnable&gt; workQueue:任務等待隊列
  • ThreadFactory threadFactory:線程建立工廠,用於建立新線程
  • RejectedExecutionHandler handler:任務拒絕策略
    • AbortPolicy:直接拋出異常,這是默認策略
    • CallerRunsPolicy:使用調用者所在的線程來執行任務
    • DiscardOldestPolicy:丟棄隊列中最靠前的任務,並執行當前任務
    • DiscardPolicy:直接丟棄提交的任務
    • 另外能夠實現RejectedExecutionHandler接口來自定義handler

新任務提交execute執行後的判斷:

  • 若是運行的線程少於corePoolSize ,則建立新線程來處理任務,即便線程池中的其餘線程是空閒的;
  • 若是線程池中的線程數量大於等於corePoolSize且小於maximumPoolSize,則只有當workQueue滿時才建立新的線程去處理任務;
  • 若是設置的corePoolSize和maximumPoolSize相同,則建立的線程池的大小是固定的,這時若是有新任務提交,若workQueue未滿,則將請求放入workQueue中,等待有空閒的線程去從workQueue中取任務並處理;
  • 若是運行的線程數量大於等於maximumPoolSize,這時若是workQueue已經滿了,則經過handler所指定的策略來處理任務;

execute執行流程圖:
Java多線程與併發筆記

線程池的狀態:

  • RUNNING:能接受新提交的任務,而且也能處理阻塞隊列中的任務
  • SHUTDOWN:再也不接受新提交的任務,但能夠處理存量任務(調用shutdown方法)
  • STOP:再也不接受新提交的任務,也不處理存量任務(調用shutdownNow方法)
  • TIDYING:全部的任務都已終止
  • TERMINATED:terminated() 方法執行完後進入該狀態

線程池狀態轉換圖:
Java多線程與併發筆記

線程池中工做線程的生命週期:
Java多線程與併發筆記

關於線程池大小如何選定參考:

  • CPU密集型任務:線程數 = 按照CPU核心數或者CPU核心數 + 1設定
  • I/O密集型任務:線程數 = CPU核心數 * (1 + 平均等待時間 / 平均工做時間)
相關文章
相關標籤/搜索