Java併發編程從基礎到進階

從Java多線程基礎到Java內存模型;從synchronized關鍵字到Java併發工具包JUC。

我們不生產知識,我們只做知識的搬運工!

基石——Java多線程的基本概念

  1. 線程與進程的不同點:

    1. 起源不同。先有進程後有線程。由於處理器的速度遠遠大於外設,爲了提升程序的執行效率,才誕生了線程。

    2. 概念不同。進程是具有獨立功能的程序運行起來的一個活動,是操作系統分配資源和調度的一個獨立單位;線程是CPU的基本調度單位。

    3. 內存共享方式不同。不同進程之間的內存數據一般是不共享的(除非採用進程間通信IPC);同一個進程中的不同線程往往會共享:

      1. 進程的代碼段
      2. 進程的公有數據
      3. 進程打開的文件描述符
      4. 信號的處理器
      5. 進程的當前目錄
      6. 進程用戶ID和進程組ID
    4. 擁有的資源不同。線程獨有的內容包括:

      1. 線程ID
      2. 寄存組的值
      3. 線程的棧
      4. 錯誤的返回碼
      5. 線程的信號屏蔽嗎
    5. 進程和線程的數量不同。

    6. 線程和進程創建的開銷不同。

      1. 線程的創建、終止時間比進程短
      2. 同一進程內的線程切換時間比進程短
      3. 同一進程的各個線程之間共享內存和文件資源,可以不通過內核進行通信。

    Java中沒有協程的概念,協程往往指程序中的多個線程可以映射到操作系統級別的幾個線程,Java中的線程數目與操作系統中的線程數目是一一對應的。

  2. 創建線程只有一種方式就是構造Thread類。實現線程的執行單元有兩種方式:

    1. 實現Runnable接口的run方法,並把Runnable實例傳遞給Thread類
    2. 重寫Thread的run方法

    從3個角度可以得到實現Runnable接口來完成多線程編程優於繼承Thread類的完成多線程編程:

    1. 代碼架構角度,不易於實現業務邏輯的解耦。run方法中作爲所執行的任務應該與Thread類解耦。
    2. 新建線程的損耗,不易於實現線程池的優化
    3. Java不支持多繼承,不易於實現擴展
  3. 同步與異步:

     同步是指被調用者不會主動告訴被調用者結果,需要調用者不斷的去查看調用結果
     異步是指被調用者會主動告訴被調用者結果,不需要調用者不斷的去查看調用結果
    
  4. 線程的正確啓動與停止:

    1. 線程的正確啓動方法是start()而不是run()。start()方法的本質是請求JVM來運行當前的線程,至於當前線程何時真正運行是由線程調度器決定的。start()方法的內部實現主要是包括三個步驟:一是檢查要啓動的新線程的狀態,二是將該線程加入線程組,三是調用線程的native方法start0()。

    2. 線程的正確停止方法是:使用interrupt()來通知,而不是強制結束指定線程。

       public class JavaDemo implements Runnable {
      
       	@Override
       	public void run() {
       		while (true) {
       			if (Thread.currentThread().isInterrupted()) {
       				break;
       			}
       			System.out.println("go");
       			interrupt();
       		}
       	}
       
       	public void interrupt() {
       		try {
       			Thread.sleep(5000);
       		} catch (InterruptedException e) {
       			System.out.println("出現異常,記錄日誌並且停止");
       			Thread.currentThread().interrupt();
       			e.printStackTrace();
       		}
       	}
       	
       	public static void main(String[] args) throws InterruptedException {
       		Thread thread = new Thread(new JavaDemoSi());
       		thread.start();
       		thread.sleep(1000);
       		thread.interrupt();
       	}
       }
      
  5. 線程的六種生命週期:

    1. NEW:已創建但未調用start()的線程狀態
    2. RUNNABLE:可運行的。在調用了start()方法之後,線程便由NEW狀態轉換成Runnable狀態
    3. BLOCKED:當前線程競爭synchronized修飾的代碼塊,並且當前鎖已經被其他線程持有,此當前線程就由Runnable狀態進入Blocked狀態。
    4. WAITING:線程調用了Object.wait()、Thread.join()、LockSupport.park()方法就會由Rubbable狀態進入Waiting狀態;當線程調用了Object.notify()、Object.notifyAll()、LockSupport.unpark()之後,線程由Waiting狀態可能短時間進入Blocked狀態然後進入Runnable狀態或者直接進入Runnable狀態或者因爲發生異常直接進入Terminated狀態。
    5. TIMEDWAITING:線程調用了Object.wait(time)、Thread.join(time)、LockSupport.parkNanos(time)、LockSupport.partUntil(time)、Tread.sleep(time)方法就會由Rubbable狀態進入TimedWaiting狀態;當線程調用了Object.notify()、Object.notifyAll()、LockSupport.unpark()之後,線程由TimedWaiting狀態可能短時間進入Blocked狀態然後進入Runnable狀態或者直接進入Runnable狀態或者因爲發生異常直接進入Terminated狀態。
    6. TERMINATED:線程的正常結束或者出現異常線程意外終止。
  6. 常見方法:

    1. wait()方法:在同步代碼塊synchronized(object){}中的線程A已經獲取到鎖時,其他線程不能獲取當前鎖從而會阻塞進入BLOCKED狀態;當線程A執行object.wait()時,線程A持有的鎖會釋放,此時其他線程獲取到object鎖;其他線程代碼中執行了object.notify()方法時,線程A會重新獲取到object鎖,可以進行線程的調用。

      注意notify()、notifyAll()方法必須要在wait()方法之後調用,若順序改變則程序會進入永久等待。

    2. park()方法:在線程中調用LockSupport.park()進行線程的掛起,在其他線程中調用LockSupport(已掛起的線程對象)進行線程的喚醒。park()和unpark()是基於許可證的概念存在的,只要調用了unpark()在一次park()中就可以實現線程的一次喚醒(這裏的一次是指線程只要調用了park()就要調用unpark(),不能實現調用多次unpark()後面的park()多次調用就可以直接實現線程的喚醒),park()和unpark()沒有調用順序的限制。

      注意park()、unpark()方法不是基於監視器鎖實現的,與wait()方法不同,park()只會掛起當前線程並不會對鎖進行釋放。在線程中使用synchronized關鍵字的內部調用了park()容易導致死鎖。

  7. 幾個常見特性: 原子性、內存可見性和重排序。

    1. 原子性:

      原子(Atomic)操作指相應的操作是單一不可分割的操作。
      在多線程中,非原子操作可能會受到其他線程的干擾,使用關鍵字synchronized可以實現操作的原子性。synchronized的本質是通過該關鍵字所包括的臨界區的排他性保證在任何一個時刻只有一個線程能夠執行臨界區中的代碼,從而使的臨界區中的代碼實現了原子操作。

    2. 內存可見性:

      CPU在執行代碼時,爲了減少變量訪問的時間消耗會將代碼中訪問的變量值緩存到CPU的緩存區中,代碼在訪問某個變量時,相應的值會從緩存中讀取而不是在主內存中讀取;同樣的,代碼對被緩存過的變量的值的修改可能僅僅是寫入緩存區而不是寫回到內存中。這樣就導致一個線程對相同變量的修改無法同步到其他線程從而導致了內存的不可見性。

      可以使用synchronizedvolatile來解決內存的不可見性問題。兩者又有點不同。synchronized仍然是
      通過將代碼在臨界區中對變量進行改變,然後使得對稍後執行該臨界區中代碼的線程是可見的。volatile不同之處在於,一個線程對一個採用volatile關鍵字修飾的變量的值的更改對於其他使用該變量的線程總是可見的,它是通過將變量的更改直接同步到主內存中,同時其他線程緩存中的對應變量失效,從而實現了變量的每次讀取都是從主內存中讀取。

    3. 指令重排序:

      在CPU多級緩存場景下,當CPU寫緩存時發現緩存區正在被其他CPU佔用,爲了提高CPU處理性能,可能將後面的讀緩存命令優先執行。運行時指令重排要遵循as-if-serial語義,即不管怎麼重排序,單線程程序的執行結果不能改變並且編譯器和處理器不會對存在的數據依賴關係的操作做重排序。

      指令的重排序導致代碼的執行順序改變,這經常會導致一系列的問題,比如在對象的創建過程中,指令的重排序使得我們得到了一個已經分配好的內存而對象的初始化並未完成,從而導致空指針的異常。volatile關鍵字可以禁止指令的重排序從而解決這類問題。

      總之,synchronized可以保證在多線程中操作的原子性和內存可見性,但是會引起上下文切換;而volatile關鍵字僅能保證內存可見性,但是可以禁止指令的重排序,同時不會引起上下文切換。

Java內存模型

首先介紹Java內存模型的特性

  1. Java所有變量都存儲在主內存中
  2. 每個線程都有自己獨立的工作內存,裏面保存該線程的使用到的變量副本(該副本就是主內存中該變量的一份拷貝)
  3. 線程對共享變量的操作都是在自己的內存中完成,而不是在主內存中完成。
  4. 線程對共享變量的操作默認情況下在其他線程中不可見,可以通過將本地線程的變量同步到共享內存中之後將共享變量同步到其他的線程

下面介紹內存模型圖

Aaron Swartz

基於JMM,Java提供了多種除了鎖之外的同步機制來保證線程安全性。Java提供的TreadLocal以及前面概念中提到的volatile就是兩種策略。

下面先介紹volatile關鍵字,ThreadLocal在下文併發工具類中介紹

volatile:

volatile最主要的就是實現了共享變量的內存可見性,其實現的原理是:volatile變量的值每次都會從高速緩存或者主內存中讀取,對於volatile變量,每一個線程不再會有一個副本變量,所有線程對volatile變量的操作都是對同一個變量的操作。

volatile變量的開銷包括讀變量和寫變量兩個方面。volatile變量的讀、寫操作都不會導致上下文的切換,因此volatile的開銷比鎖小。但是volatile變量的值不會暫存在寄存器中,因此讀取volatile變量的成本要比讀取普通變量的成本更高。

volatile常被稱爲"輕量級鎖"。

JUC工具包

Java中的各種鎖(互斥同步保證線程併發安全)

互斥同步是指多個線程對共享資源是獨佔的,當一個線程獲得共享資源時,其他所有的線程都將處於等待獲取狀態,不同線程之間是敵對的。

根據不同的分類標準存在多種鎖類型,對於一種確定的鎖可以同時屬於下面的多種類型:

  1. 多個線程能否共享一把鎖:可以實現共享的稱爲共享鎖;不可以實現共享的稱爲排他鎖。共享鎖又稱爲讀鎖,每一個線程都可以獲取到讀鎖,之後可以查看數據但是無法修改和刪除數據。

    synchronized屬於排他鎖**。 **ReentrantReadWriteLock`同時具備共享鎖和排他鎖,其中讀鎖是共享鎖,寫鎖是排他鎖

  2. 線程要不要鎖住同步資源:鎖住同步資源的稱爲悲觀鎖(又稱爲互斥同步鎖);不鎖住同步資源的稱爲樂觀鎖(又稱爲非互斥同步鎖)。

    優缺點:

    悲觀鎖的性能相對較低:當發生長時間鎖等不到釋放或者直接出現死鎖時,等待鎖的線程永遠得不到執行;同時悲觀鎖存在阻塞和喚醒這兩種狀態都是會消耗資源的;此外使用了悲觀鎖,線程的優先級屬性設置將會失效。

    相對於悲觀鎖而言,樂觀鎖性能較高,但是如果獲取鎖的線程數量過多,那麼樂觀鎖會產生大量的無用自旋等消耗,性能也會因此而下降

    悲觀鎖適用於併發寫入多或者臨界區持鎖時間比較長的情形

    樂觀鎖適用於併發寫入少、併發讀取多的情形

    synchronizedLock都屬於悲觀鎖
    原子類和併發容器工具都採用了樂觀鎖的思想

    樂觀鎖基於CAS算法實現。

    CAS算法:

    CAS(Compare and Swap),即比較並交換。

    CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改爲B,否則什麼都不做。

    當然CAS除了有上面提到的樂觀鎖的缺點外,CAS還容易出現ABA問題。即可能存在其他線程修改過預期值執行過其他操作之後又寫會預期值,這樣反而不會被察覺。解決ABA問題的一個好方式就是增加版本號version字段,通過每次更新操作都修改version字段以及每次更新之前都檢查version字段來保證線程執行的安全性

  3. 同一個線程是否可以重複獲取同一把鎖:可以重複獲取的稱爲可重入鎖;不可以重複獲取的稱爲不可重入鎖

    可重入鎖可以有效的避免死鎖,當一個線程獲取到鎖時,可以繼續獲取該鎖,而不會出現當前線程等待當前線程釋放鎖這一情況的發生。

    synchronizedReentrantLock都屬於可重入鎖

  4. 多個線程競爭時根據是否排隊:通過排隊來獲取的稱爲公平鎖;先嚐試插隊,插隊失敗再排隊的稱爲非公平鎖

    ReentrantLock既可以實現公平鎖又可以實現非公平鎖,通過指定ReentrantLock構造方法中fair的參數值來實現公平與非公平的效果

  5. 是否可以響應中斷:可響應中斷的稱爲可中斷鎖;不可響應中斷的稱爲非可中斷鎖

  6. 等鎖的過程不同:等鎖的過程中如果不停的嘗試而非阻塞稱爲自旋鎖;等鎖的過程中如果阻塞等待稱爲非自旋鎖

同步關鍵字synchronized解析

  1. 作用: synchronized能夠保證在同一時刻最多隻有一個線程執行該代碼,以達到保證併發安全的效果。synchronized是最基本的同步互斥的手段。
  2. 用法:
    1. 對象鎖.
      1. 方法鎖,即默認鎖對象爲this當前實例對象。同一個實例對象下的實例方法共享同一把鎖,不同的實例對象的實例方法鎖不同。

         class SynchronizedDemo1 {
             public synchronized void index1() {
                 //do something...
             }
             public synchronized void index2() {
                 //do something...
             }
         }
         class SynchronizedDemo2 {
             public synchronized void index1() {
                 //do something...
             }
             public synchronized void index2() {
                 //do something...
             }
         }
        

        以上代碼中,SynchronizedDemo1實例對象demo1的方法index1和index2共享同一把鎖,SynchronizedDemo2實例對象demo1的方法index1和index2共享同一把鎖,多個線程訪問同一個對象下的synchronized修飾的方法時是互斥同步的,訪問不同對象的synchronized修飾的方法互不干擾

      2. 同步代碼塊鎖,即自己指定鎖對象。

         class SynchronizedDemo1 {
             public synchronized void index() {
                 synchronized(this){
                     //do something...
                 }
             }
         }
        

        以上代碼中,只有獲得了當前對象鎖的線程才能執行同步代碼塊中的代碼,同步代碼塊的出現是爲了減小方法鎖的粒度,提高性能

    2. 類鎖.
      1. synchronized修飾靜態的方法。多個線程訪問同一類的不同實例對象的靜態方法時,由於靜態方法是類級別的而不是對象級別的,所以即便是不同對象,方法之間的訪問也是互斥同步的

      2. 指定的鎖爲Class對象。

         class SynchronizedDemo1 {
             public synchronized void index() {
                 synchronized(SynchronizedDemo1.class){
                     //do something...
                 }
             }
         }
        

        以上代碼中,只有獲得了當前類的Class對象鎖的線程才能執行同步代碼塊中的代碼,同步代碼塊的出現是爲了減小方法鎖的粒度,提高性能

  3. synchronized是可重入的、不可中斷的。

其他常用的鎖

在jdk1.5之後,併發包中新增了Lock接口(以及相關實現類)用來實現鎖功能,Lock接口提供了與synchronized關鍵字類似的同步功能,但需要在使用時手動獲取鎖和釋放鎖,也正因爲如此,基於Lock接口實現的鎖具備更好的可操作性。

Lock接口中的方法:

  1. lock(): 此方法用於獲取鎖,如果鎖已被其他線程獲取,那麼線程進入等待狀態,與synchronized不同的是:當獲取到鎖並且在執行任務中發生了異常,synchronized會自動釋放鎖而lock()方法獲取到的鎖不會自動釋放。使用lock()必須在try…finally…中手動釋放。
  2. tryLock():由於lock()不能被中斷,所以一旦陷入死鎖,lock()就會陷入永久等待中;tryLock()方法是一種更爲優雅的使用方式,tryLock()用來嘗試獲取鎖,如果當前鎖沒有被其他線程佔用,那麼獲取鎖成功並立刻返回true,否則立刻返回false表示獲取鎖失敗。
ReetrantLock

ReetrantLock 是基於Lock接口最通用的實現,在上文中在介紹鎖分類時也已經多次提到過ReentrantLock,因此也瞭解過其許多特性,由於ReentrantLock非常值得深入探究,在此也不在一文中過多闡述,在此給出一個鏈接進行參看:

[深入ReentrantLock]https://blog.csdn.net/fuyuwei2015/article/details/83719444#commentBox

ReadWriteLock

讀寫鎖是一種改進型的排它鎖。讀寫鎖允許多個線程可以同時讀取(只讀)共享變量。讀寫鎖是分爲讀鎖和寫鎖兩種角色的,讀線程在訪問共享變量的時候必須持有相應讀寫鎖的讀鎖,而且讀鎖是共享的、多個線程可以共同持有的;寫鎖是排他的,以一個線程在持有寫鎖的時候,其他線程無法獲得相應鎖的寫鎖或讀鎖。總之,讀寫鎖通過讀寫鎖的分離從而提高了併發性。
ReadWriteLock接口是對讀寫鎖的抽象,其默認的實現類是ReentrantReadWriteLock。ReadWriteLock定義了兩個方法readLock()和writeLock(),分別用於返回相應讀寫鎖實例的讀鎖和寫鎖。這兩個方法的返回值類型都是Lock。

關於ReentrantReadWriteLock實現,這裏給出一個鏈接參看:
[ReentrantReadWriteLock詳解]https://www.cnblogs.com/xiaoxi/p/9140541.html

讀寫鎖主要用於讀線程持有鎖的時間比較長的情景下。

原子類(非互斥同步保證線程併發安全)

非互斥同步指的是不同的線程不對共享資源進行獨佔,不同的線程都可以訪問共享資源,只不過當多個線程同時對一個共享變量進行修改或刪除時,只有一個線程的操作能成功其他的都會失敗。

Java中的原子類分爲6種,分別有:

  1. AtomicInteger、AtomicLong、AtomicBoolean
  2. AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
  3. AtomicReference、AtomicStampedReference、AtomicMarkableReference
  4. AtomicIntegerFieldupdater、AtomicLongFieldupdater、AtomicReferenceFieldupdater
  5. LongAdder、DoubleAdder
  6. LongAccumulator、DoubleAccumulator

直接使用Java中的原子類進行操作即可在併發情況下保證變量的線程安全,原子類相較於鎖粒度更小,性能更高。原子類也是基於CAS算法來實現的,其都包括compareAndSet()方法即爲先比較當前值是否等於預期的值然後進行數據的修改從而保證了變量的原子性。

需要注意的是累加器LongAdder是Java8開始引入的,相較於AtomicLong,由於LongAdder在每個線程操作的過程中並不會實時的進行數據同步(由於上文所提到的JMM,AtomicLong會實時的進行多個線程之間的數據通信),所以效率更高。而LongAccumulator擴展了LongAdder使得原子變量不僅只能進行累加操作也可以進行其他指定公式的計算

併發容器(結合互斥同步與非互斥同步保證線程併發安全)

Java中併發容器由來已久,當然併發容器的種類也非常多。但是其中一部分諸如Vector、Hashtable、Collections.synchronizedList()、Collections.synchronizedMap()等底層是基於synchronized來實現的併發同步,效率會比較低,所以即使這些容器可以保證線程安全也不再使用。與之相替代的就是下面的幾種併發容器類,由於併發容器在實現上也有許多可學習之處,所以這裏不再在一文中介紹而是會初步引入,並放上我認爲比較不錯的幾個博客鏈接,這樣可以更好的深入理解。

  1. ConcurrentHashMap——線程安全的Map

多個線程往HashMap中同時進行put(),如果有幾個線程計算出的鍵的散列值相同,那麼就會出現key丟失的情況,同樣的,如果此時HashMap容量不夠,多個線層同時擴容,也會只保留一個擴容後的Map,從而導致數據丟失。而ConcurrentHashMap則在底層數據結構的實現上與HashMap又有所區別,避免了HashMap會產生的問題。

關於ConcurrentHashMap的數據結構可以參看:
[ConcurrentHashMap的數據結構]https://blog.csdn.net/weixin_44460333/article/details/86770169#commentBox

  1. CopyOnWriteArrayList——線程安全的List、CopyOnWriteArraySet——線程安全的Set

爲了保證List的線程安全,又要避免因使用Vector、Collections.synchronized等而產生的鎖粒度過大而造成效率降低的問題,CopyOnWriteArrayList、CopyOnWriteArraySet應運而生,CopyOnWriteArrayList和CopyOnWriteArraySet在實現原理上大體一致,這裏只給出CopyOnWriteArrayList的介紹.

關於CopyOnWriteArrayList的數據結構可以參看:
[CopyOnWriteArrayList的數據結構]https://www.cnblogs.com/chengxiao/p/6881974.html

  1. BlockingQueue——阻塞隊列作爲數據共享的通道

BlockingQueue很好的解決了多線程中,如何高效安全“傳輸”數據的問題。通過這些高效並且線程安全的隊列類,爲我們快速搭建高質量的多線程程序帶來極大的便利。在Java中,BlockingQueue是一個接口,它的實現類有ArrayBlockingQueue、DelayQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等,這些阻塞隊列的實現在Java併發編程中經常要用到,其中最常用的就是ArrayBlockingQueue和LinkedBlockingQueue

關於BlockingQueue可以參看:
[BlockingQueue相關]https://segmentfault.com/a/1190000016296278

關於ArrayBlockingQueue可以參看:
[ArrayBlockingQueue相關]https://blog.csdn.net/u014799292/article/details/90167096

關於LinkedBlockingQueue可以參看:
[LinkedBlockingQueue相關]https://blog.csdn.net/tonywu1992/article/details/83419448

  1. ConcurrentLinkedQueue——非阻塞併發隊列(使用鏈表實現的線程安全的LinkedList)

ConcurrentLinkedQueue是一個基於鏈接節點的非阻塞無界線程安全隊列。

關於ConcurrentLinkedQueue的數據結構可以參看:
[ConcurrentLinkedQueue的數據結構]https://blog.csdn.net/qq_38293564/article/details/80798310#commentBox

ThreadLocal(無同步保證線程併發安全)

ThreadLocal,即線程變量,是一個以ThreadLocal對象爲鍵、任意對象爲值的存儲結構。這個結構被附帶在線程上,也就是說一個線程可以根據一個ThreadLocal對象查詢到綁定在這個線程上的一個值。

ThreadLocal採用的是上述策略中的第一種設計思想——採用線程的特有對象.採用線程的特有對象,我們可以保障每一個線程都具有各自的實例,同一個對象不會被多個線程共享,ThreadLocal是維護線程封閉性的一種更加規範的方法,這個類能使線程中的某個值與保存值的對象關聯起來,從而保證了線程特有對象的固有線程安全性。

ThreadLocal類相當於線程訪問其線程特有對象的代理,即各個線程通過這個對象可以創建並訪問各自的線程特有對象,泛型T指定了相應線程持有對象的類型。一個線程可以使用不同的ThreadLocal實例來創建並訪問其不同的線程持有對象。多個線程使用同一個ThreadLocal實例所訪問到的對象時類型T的不同實例。代理的關係圖如下:

Aaron Swartz

ThreadLocal提供了get和set等訪問接口或方法,這些方法爲每一個使用該變量的線程都存有一份獨立的副本,因此get總是能返回由當前執行線程在調用set時設置的最新值。其主要使用的方法如下:

	public T get(): 獲取與當前線程中ThreadLocal實例關聯的線程特有對象。
	public void set(T value):重新關聯當前線程中ThreadLocal實例所對應的線程特有對象。
	protected T initValue():如果沒有調用set(),在初始化threadlocal對象的時候,該方法的返回值就是當前線程中與ThreadLocal實例關聯的線程特有對象。
	public void remove():刪除當前線程中ThreadLocal和線程特有對象的關係。

那麼ThreadLocal底層是如何實現Thread持有自己的線程特有對象的?查看set()方法的源代碼:

Aaron Swartz

Aaron Swartz
可以看到,當我們調用threadlocal的set方法來保存當前線程的特有對象時,threadlocal會取出當前線程關聯的threadlocalmap對象,然後調用ThreadLocalMap對象的set方法來進行當前給定值的保存。

Aaron Swartz

每一個Thread都會維護一個ThreadLocalMap對象,ThreadLocalMap是一個類似Map的數據結構,但是它沒有實現任何Map的相關接口。ThreadLocalMap是一個Entry數組,每一個Entry對象都是一個"key-value"結構,而且Entry對象的key永遠都是ThreadLocal對象。當我們調用ThreadLocal的set方法時,實際上就是以當前ThreadLocal對象本身作爲key,放入到了ThreadLocalMap中。

可能發生內存泄漏:

通過查看Entry結構可知,Entry屬於WeakReference類型,因此Entry不會阻止被引用的ThreadLocal實例被垃圾回收。當一個ThreadLocal實例沒有對其可達的強引用時,這個實例就可以被垃圾回收,即其所在的Entry的key會被置爲null,但是如果創建ThreadLocal的線程一直持續運行,那麼這個Entry對象中的value就有可能一直得不到回收,從而發生內存泄露。

解決內存泄漏的最有效方法就是,在使用完ThreadLocal之後,要注意調用threadlocal的remove()方法釋放內存。

Future

傳統的Runnable來實現任務有兩大缺陷,一個是Runnable中的run()沒有返回值,另一個是Runnable中的run()無法拋出異常。爲了解決上述問題,Callable應運而生,而Future是爲了更好的操作Callable實現業務邏輯而誕生的。

我們可以用Future.get來獲取Callable接口返回的執行結果,還可以通過Future.isDone()來判斷任務是否已經執行完了以及取消這個任務,限時獲取任務的結果等等。

線程池

線程池提供了複用線程的能力,如果不使用線程池,那麼每個任務都會新開一個線程,上文基石中也已經提到Java代碼中的線程數量對應於操作系統的線程數量,這樣對於線程的創建和銷燬都會帶來很大的開銷,此外系統可創建的線程數量是有限的,使用線程池可以有效避免OOM等異常。

線程池的創建一般藉助ThreadPoolExecutor這個類,其中有5個參數比較關鍵,以下說明:

  1. corePoolSize、maxPoolSize、workQueue:線程池中默認存在的線程數量是corePoolSize,當任務多於corePoolSize時,新來的任務會首先存儲在任務存儲隊列workQueue中,當任務數量超出了任務存儲隊列的最大長度,線程池纔會擴大其中的線程數量直到maxPoolSize,當任務數量超出maxPoolSize,線程池執行定義的拒絕策略handler

    1. workQueue的三種常用類型:

      1.SyncbronousQueue:最簡單的直接交換隊列,這隊列長度爲0不能存儲新的任務,適用與任務不太多的場景,此外由於隊列不能存儲任務線程池很容易創建新的線程,所以maxPoolSize要設置的大一點,但是如果設置的maxPoolSize過大,線程創建的過多而不能得到調度從而產生堆積,就會引發OOM。Executors.newCachedThreadPool()、Executors.newScheduledThreadPool()即爲這種類型,其中Executors.newCachedThreadPool()的maxPoolSize這裏設置的爲Integer.MAX_VALUE,corePoolSize默認爲0,keepAliveTime爲60s

      2.LinkedBlockingQueue:無解隊列,這個相較於第一種隊列屬於另一個極端,可以存儲任意數量的任務。此類隊列可以存儲較多數量的任務並且此時maxPoolSize會失效,但是此時也要注意任務過多時會產生堆積出現OOM。Executors.newFixedThreadPool()、Executors.newSingleThreadExecutor()即爲這種類型

      3.ArrayBlockingQueue:有界隊列,可以設置隊列長度,此時maxPoolSize有效

  2. keepAliveTime:如果線程池當前的線程數量多餘corePoolSize,那麼當多餘線程的空閒時間超過keepAliveTime時,它們將被回收。

  3. ThreadFactory:線程池中新創建的線程是由ThreadFactory創建的,默認使用Executors.defaultThreadFactory()

線程池應該手動創建,其中:

當任務屬於CPU密集型時,線程池中的線程數量應該設置爲CPU核心數的1-2倍;當任務屬於資源密集型時,線程池中的線程數量一般設置爲cpu核心數的很多倍,計算方法一般爲num=CPU核心數*(1+平均等待時間/平均工作時間)

線程池停止:

shutdown():調用此方法後,線程池並不會立刻停止而是拒絕接受新的任務並等待線程池中已在執行的線程任務和隊列中的任務執行完畢
shutdownNow():調用此方法後,線程池通過調用terminated()方法來終止正在執行的線程同時將隊列中未被調度的任務以集合的形式返回。

後記

到此爲止,本文要梳理的Java併發相關也告一段落,之所以如此說是因爲Java併發相關確實是值得深入探究的一個領域,本文的定位是基於Java來梳理併發相關的那些事兒,儘可能通過一篇文章來歸納出Java併發中應該掌握的知識點。
本文仍然有很多不足之處,比如文中沒有介紹Java的併發工具類諸如CountdownLatch、Semaphore等,而關於ReentrantLock這種重要的鎖的實現原理AQS本文也沒有介紹,希望在之後的文章中能對本文略過的點進行深入的歸納總結。

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