JUC多線程

1.概述

1.1.併發與並行

  • 併發:指兩個或多個事件在同一個時間段內發生。
  • 並行:指兩個或多個事件在同一時刻發生(同時發生)。

8hiHKO.png

1.2.線程與進程

基本概述

  • 進程:是指一個內存中運行的應用程序,每個進程都有一個獨立的內存空間,一個應用程序可以同時運行多個進程;進程也是程序的一次執行過程,是系統運行程序的基本單位;系統運行一個程序即是一個進程從創建、運行到消亡的過程。

  • 線程:線程是進程中的一個執行單元,負責當前進程中程序的執行,一個進程中至少有一個線程。一個進程中是可以有多個線程的,這個應用程序也可以稱之爲多線程程序。

    • 與進程不同的是同類的多個線程共享進程的方法區資源,但每個線程有自己的程序計數器虛擬機棧本地方法棧
    • 所以系統在產生一個線程,或是在各個線程之間作切換工作時,負擔要比進程小得多,也正因爲如此,線程也被稱爲輕量級進程

    簡而言之:一個程序運行後至少有一個進程,一個進程中可以包含多個線程

注意

在 java中,每次程序運行至少啓動2個線程。一個是 main 線程,一個是垃圾收集線程。因爲每當使用 java 命令執行一個類的時候,實際上都會啓動一個 JVM,每一個 JVM 其實在就是在操作系統中啓動了一個進程。

線程的分類

  • 單線程:同一時間只能幹一件事.(多件事只能等一個處理完成後才能開始處理下一個)
  • 多線程:同一時間能幹多件事情。(可以輔助線程的並行理解)
  • 主線程:程序啓動系統自動創建並執行 main 方法的線程。主線程的執行入口:main方法 (說起主線程在這裏順便提一下 守護線程)
  • 守護線程:指爲其他線程提供服務的線程,也稱爲守護線程。JVM 的垃圾回收線程就是一個後臺線程。用戶線程和守護線程的區別在於,是否等待主線程依賴於主線程結束而結束
  • 子線程:除了主線程以爲的其他線程,子線程的執行入口:run方法

線程調度

  • 分時調度

    所有線程輪流使用 CPU 的使用權,平均分配每個線程佔用 CPU 的時間。

  • 搶佔式調度

    優先讓優先級高的線程使用 CPU,如果線程的優先級相同,那麼會隨機選擇一個(線程隨機性),Java使用的爲搶佔式調度。

大部分操作系統都支持多進程併發運行,現在的操作系統幾乎都支持同時運行多個程序。比如:現在我們上課一邊使用編輯器,一邊使用錄屏軟件,同時還開着畫圖板,dos窗口等軟件。此時,這些程序是在同時運行,”感覺這些軟件好像在同一時刻運行着“。

實際上,CPU(中央處理器)使用搶佔式調度模式在多個線程間進行着高速的切換。對於CPU的一個核而言,某個時刻,只能執行一個線程,而 CPU的在多個線程間切換速度相對我們的感覺要快,看上去就是在同一時刻運行。
其實,多線程程序並不能提高程序的運行速度,但能夠提高程序運行效率,讓CPU的使用率更高。

上下文切換

當前任務在執行完 CPU 時間片切換到另一個任務之前會先保存自己的狀態,以便下次再切換回這個任務時,可以再加載這個任務的狀態。任務從保存到再加載的過程就是一次上下文切換

上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統來說意味着消耗大量的 CPU 時間,事實上,可能是操作系統中時間消耗最大的操作。

1.3.線程的生命週期(狀態)

當線程被創建並啓動以後,它既不是一啓動就進入了執行狀態,也不是一直處於執行狀態。在線程的生命週期中,有幾種狀態呢?在API中java.lang.Thread.State 這個枚舉中給出了六種線程狀態:

線程狀態 導致狀態發生條件
NEW(新建) 線程剛被創建,但是並未啓動。還沒調用 start 方法。
Runnable(可運行) 線程可以在 java 虛擬機中運行的狀態,可能正在運行自己代碼,也可能沒有,這取決於操 作系統處理器。
Blocked(鎖阻塞) 當一個線程試圖獲取一個對象鎖,而該對象鎖被其他的線程持有,則該線程進入Blocked 狀 態;當該線程持有鎖時,該線程將變成 Runnable 狀態。
Waiting(無限等待) 一個線程在等待另一個線程執行一個(喚醒)動作時,該線程進入 Waiting 狀態。進入這個 狀態後是不能自動喚醒的,必須等待另一個線程調用 notify 或者 notifyAll 方法才能夠喚醒。
Timed Waiting(計時等待) 同 waiting 狀態,有幾個方法有超時參數,調用他們將進入Timed Waiting 狀態。這一狀態將一直保持到超時期滿或者接收到喚醒通知。帶有超時參數的常用方法有 Thread.sleep 、Object.wait。
Teminated(被終止) 因爲 run 方法正常退出而死亡,或者因爲沒有捕獲的異常終止了run 方法而死亡。

8WYz28.jpg

提示

  • Waiting(無限等待) 狀態中wait方法是空參的,而 Timed Waiting(計時等待) 中wait方法是帶參的。
    • 如果沒有得到(喚醒)通知,那麼線程就處於Timed Waiting 狀態,直到倒計時完畢自動醒來
    • 如果在倒計時期間得到(喚醒)通知,那麼線程從Timed Waiting 狀態立刻喚醒。

1.4.線程死鎖

概述

多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由於線程被無限期地阻塞,因此程序不可能正常終止。

產生條件

  1. 互斥條件:該資源任意一個時刻只由一個線程佔用。
  2. 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
  3. 不剝奪條件:線程已獲得的資源在末使用完之前不能被其他線程強行剝奪,只有自己使用完畢後才釋放資源。
  4. 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係。

如何避免

我們只要破壞產生死鎖的四個條件中的其中一個就可以了。

  • 破壞互斥條件
    • 這個條件我們沒有辦法破壞,因爲我們用鎖本來就是想讓他們互斥的(臨界資源需要互斥訪問)。
  • 破壞請求與保持條件
    • 一次性申請所有的資源。
  • 破壞不剝奪條件
    • 佔用部分資源的線程進一步申請其他資源時,如果申請不到,可以主動釋放它佔有的資源。
  • 破壞循環等待條件
    • 靠按序申請資源來預防。按某一順序申請資源,釋放資源則反序釋放。破壞循環等待條件。

2.多線程的實現

Java 多線程實現方式主要有四種:

  • 繼承 Thread 類
  • 實現 Runnable 接口
  • 實現 Callable 接口通過 FutureTask 包裝器來創建 Thread 線程、
  • 使用 ExecutorService、Callable、Future 實現有返回結果的多線程。

其中前兩種方式線程執行完後都沒有返回值,後兩種是帶返回值的。此處參考

2.1.繼承 Thread 類創建線程

java.lang.Thread,Thread類本質上是實現了 Runnable 接口的一個實例,代表一個線程的實例。啓動線程的唯一方法就是通過Thread 類的start()實例方法。

  • start()方法是一個 native 方法,它將啓動一個新線程,並執行 run() 方法。
  • 這種方式實現多線程很簡單,通過自己的類直接 extend Thread,並複寫 run() 方法,就可以啓動新線程並執行自己定義的 run() 方法。
public class MyThread extends Thread {  
  public void run() {  
   System.out.println("MyThread.run()");  
  }  
}  
 
MyThread myThread1 = new MyThread();  
MyThread myThread2 = new MyThread();  
myThread1.start();  
myThread2.start();  

構造方法

  • public Thread() :分配一個新的線程對象。
  • public Thread(String name):分配一個指定名字的新的線程對象。
  • public Thread(Runnable target):分配一個帶有指定目標新的線程對象。
  • public Thread(Runnable target,String name) :分配一個帶有指定目標新的線程對象並指定名字。

常用方法

  • public String getName(): 獲取當前線程名稱。
  • public void start():導致此線程開始執行; Java虛擬機調用此線程的 run 方法。
  • public void run() :此線程要執行的任務在此處定義代碼。
  • public static void sleep(long millis) :使當前正在執行的線程以指定的毫秒數暫停(暫時停止執行)。
  • public static Thread currentThread():返回對當前正在執行的線程對象的引用。

2.2.實現 Runnable 接口創建線程

java.lang.Runnable,如果自己的類已經 extends 另一個類,就無法直接 extends Thread,此時,可以實現一個 Runnable 接口

步驟

  1. 定義 Runnable 接口的實現類,並重寫該接口的 run() 方法,該 run() 方法的方法體同樣是該線程的線程執行體。
  2. 創建 Runnable 實現類的實例,並以此實例作爲 Thread 的 target 來創建 Thread 對象,該 Thread 對象纔是真正的線程對象。
  3. 調用線程對象的start()方法來啓動線程。

代碼實現

public class MyRunnable implements Runnable{
    @Override
    public void run() {
    	for (int i = 0; i < 20; i++) {
    		System.out.println(Thread.currentThread().getName()+" "+i);
    	}
    }
}

public class Demo {
    public static void main(String[] args) {
    //創建自定義類對象 線程任務對象
    MyRunnable mr = new MyRunnable();
    //創建線程對象
    Thread t = new Thread(mr, "小強");
    t.start();
    for (int i = 0; i < 20; i++) {
        System.out.println("旺財 " + i);
        }
    }
}

小結

  • 通過實現 Runnable 接口,使得該類有了多線程類的特徵。run() 方法是多線程程序的一個執行目標。所有的多線程代碼都在 run 方法裏面。Thread 類實際上也是實現了 Runnable 接口的類。
  • 在啓動的多線程的時候,需要先通過 Thread類 的構造方法Thread(Runnable target) 構造出對象,然後調用Thread 對象的 start() 方法來運行多線程代碼。
  • 實際上所有的多線程代碼都是通過運行 Thread 的 start() 方法來運行的。因此,不管是繼承 Thread類 還是實現 Runnable 接口來實現多線程,最終還是通過 Thread 的對象的 API 來控制線程的。
  • Runnable 對象僅僅作爲 Thread 對象的 target ,Runnable 實現類裏包含的 run() 方法僅作爲線程執行體
    而實際的線程對象依然是 Thread 實例,只是該 Thread 線程負責執行其 target 的 run() 方法。

2.3.實現 Callable 接口

實現 Callable 接口通過 FutureTask 包裝器來創建 Thread 線程

步驟

  1. 創建 Callable 接口的實現類 ,並實現 Call 方法
  2. 創建 Callable 實現類的實現,使用 FutureTask 類包裝 Callable 對象,該 FutureTask 對象封裝了 Callable 對象的 Call 方法的返回值
  3. 使用 FutureTask 對象作爲 Thread 對象的 target 創建並啓動線程
  4. 調用 FutureTask 對象的 get() 來獲取子線程執行結束的返回值
  • Callable接口(也只有一個方法)定義如下
public interface Callable<V>   { 
  V call() throws Exception;   } 
class Tickets<Object> implements Callable<Object>{
 
    //重寫call方法
    @Override
    public Object call() throws Exception {
        // TODO Auto-generated method stub
        System.out.println(Thread.currentThread().getName()+"-->我是通過實現Callable接口通過FutureTask包裝器來實現的線程");
        return null;
    }   
}
 public class ThreadDemo03 {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
 
        Callable<Object> oneCallable = new Tickets<Object>();
        FutureTask<Object> oneTask = new FutureTask<Object>(oneCallable);
 
        Thread t = new Thread(oneTask);
 
        System.out.println(Thread.currentThread().getName());
 
        t.start(); 
    } 
}

2.4.線程池創建線程

線程池的構建:

  • 通過 ThreadPoolExecutor 構造函數實現(推薦)
  • 通過工具類 Executors 來實現 我們可以創建三種類型的 ThreadPoolExecutor
    • FixedThreadPool
    • SingleThreadExecutor
    • CachedThreadPool

步驟

此處只展示使用,後面會詳細介紹,Runnable+Executors

  1. 實現 Runable 或Callable(有返回值)接口,重寫 run 方法
  2. 使用 ExectorService 的相關方法創建 想要的 線程池,這裏使用了newFixedThreadPool()
  3. 調用 execute 方法,執行任務

上代碼


public class ThreadDemo05{
 
    private static int POOL_NUM = 10;     //線程池數量
 
    public static void main(String[] args) throws InterruptedException {
        // TODO Auto-generated method stub
        ExecutorService executorService = Executors.newFixedThreadPool(5);  
        for(int i = 0; i<POOL_NUM; i++)  
        {  
            RunnableThread thread = new RunnableThread();
 
            //Thread.sleep(1000);
            executorService.execute(thread);  
        }
        //關閉線程池
        executorService.shutdown(); 
    }   
 
}
 
class RunnableThread implements Runnable  
{     
    @Override
    public void run()  
    {  
        System.out.println("通過線程池方式創建的線程:" + Thread.currentThread().getName() + " ");  
 
    }  
}  

2.5.小結

使用建議

  • 前面兩種可以歸結爲一類:無返回值,原因很簡單,通過重寫 run 方法,run 方式的返回值是 void,所以沒有辦法返回結果
  • 後面兩種可以歸結成一類:有返回值,通過 Callable 接口,就要實現 call 方法,這個方法的返回值是Object,所以返回的結果可以放在 Object 對象中

3.線程池

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

8RvjsI.png

好處

  1. 降低資源消耗。減少了創建和銷燬線程的次數,每個工作線程都可以被重複利用,可執行多個任務。
  2. 提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
  3. 提高線程的可管理性。可以根據系統的承受能力,調整線程池中工作線線程的數目,防止因爲消耗過多的內存,而把服務器累趴下(每個線程需要大約1MB內存,線程開的越多,消耗的內存也就越大,最後死機)。

線程池狀態

線程池的5種狀態:Running、ShutDown、Stop、Tidying、Terminated。推薦閱讀

  1. RUNNING運行狀態

    • 狀態說明:線程池處在 RUNNING 狀態時,能夠接收新任務,以及對已添加的任務進行處理。
    • 狀態切換:線程池的初始化狀態是 RUNNING。換句話說,線程池被一旦被創建,就處於RUNNING 狀態,並且線程池中的任務數爲 0!
  2. SHUTDOWN關閉狀態

    • 狀態說明:線程池處在SHUTDOWN狀態時,不接收新任務,但能處理已添加的任務。
    • 狀態切換:調用線程池的shutdown()接口時,線程池由RUNNING -> SHUTDOWN。
  3. STOP停止狀態

    • 狀態說明:線程池處在STOP狀態時,不接收新任務,不處理已添加的任務,並且會中斷正在處理的任務。
    • 狀態切換:調用線程池的shutdownNow()接口時,線程池由(RUNNING or SHUTDOWN ) -> STOP。
  4. TIDYING整理狀態

    • 狀態說明:當所有的任務已終止,ctl 記錄的”任務數量”爲0,線程池會變爲TIDYING狀態。當線程池變爲TIDYING狀態時,會執行鉤子函數terminated()

      terminated()在ThreadPoolExecutor類中是空的,若用戶想在線程池變爲TIDYING時,進行相應的處理;可以通過重載 terminated()函數來實現。

    • 狀態切換:當線程池在SHUTDOWN狀態下,阻塞隊列爲空並且線程池中執行的任務也爲空時,就會由 SHUTDOWN -> TIDYING。
      當線程池在STOP狀態下,線程池中執行的任務爲空時,就會由STOP -> TIDYING。

  5. TERMINATED已終止狀態

    • 狀態說明:線程池徹底終止,就變成 TERMINATED 狀態。
    • 狀態切換:線程池處在 TIDYING 狀態時,執行完 terminated() 之後,就會由 TIDYING -> TERMINATED。

3.1. Executor 框架

此處參考

簡介

Executor 框架是 Java5 之後引進的,在 Java 5 之後,通過 Executor 來啓動線程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用線程池實現,節約開銷)外,還有關鍵的一點:有助於避免 this 逃逸問題。

  • this 逃逸是指在構造函數返回之前,其他線程就持有該對象的引用。調用尚未構造完全的對象的方法可能引發令人疑惑的錯誤。

Executor 框架不僅包括了線程池的管理,還提供了線程工廠、隊列以及拒絕策略等,Executor 框架讓併發編程變得更加簡單。

3 部分結構

  1. 任務(Runnable /Callable)
  • 執行任務需要實現的 Runnable 接口 或 Callable接口。Runnable 接口或 Callable 接口 實現類都可以被 ThreadPoolExecutorScheduledThreadPoolExecutor 執行。
  1. 任務的執行(Executor)

8hFmR0.jpg

  • 任務執行機制的核心接口 Executor ,以及繼承自 Executor 接口的 ExecutorService 接口。
  • ThreadPoolExecutorScheduledThreadPoolExecutor 這兩個關鍵類實現了 ExecutorService 接口。
  • 我們需要更多關注的是 ThreadPoolExecutor 這個類,這個類在我們實際使用線程池的過程中,使用頻率還是非常高的。
  • ScheduledThreadPoolExecutor 主要用來在給定的延遲後運行任務,或者定期執行任務。 (瞭解)
//AbstractExecutorService實現了ExecutorService接口
public class ThreadPoolExecutor extends AbstractExecutorService

    //ScheduledExecutorService實現了ExecutorService接口
public class ScheduledThreadPoolExecutor
        extends ThreadPoolExecutor
        implements ScheduledExecutorService
  1. 異步計算的結果(Future)
  • Future 接口以及 Future 接口的實現類 FutureTask 類都可以代表異步計算的結果。
  • 當我們把 Runnable接口 或 Callable 接口 的實現類提交給 ThreadPoolExecutorScheduledThreadPoolExecutor 執行。(調用 submit()方法時會返回一個 FutureTask 對象)

3.2.框架的使用

8WAjVH.png

  1. 主線程首先要創建實現 Runnable 或者 Callable 接口的任務對象。
  2. 把創建完成的實現 Runnable/Callable接口的 對象
    • 直接交給 ExecutorService執行: ExecutorService.execute(Runnable command)
    • 或者提交給 ExecutorService 執行
      • ExecutorService.submit(Runnable task)
      • 或者ExecutorService.submit(Callable <T> task)
  3. 如果執行ExecutorService.submit(…),ExecutorService 將返回一個實現 Future 接口的對象
    • 我們剛剛也提到過了執行 execute()方法和 submit()方法的區別,submit()會返回一個 FutureTask 對象。
    • 由於 FutureTask 實現了 Runnable,我們也可以創建 FutureTask,然後直接交給 ExecutorService 執行。
  4. 最後,主線程可以執行 FutureTask.get()方法來等待任務執行完成。主線程也可以執行 FutureTask.cancel(boolean mayInterruptIfRunning)來取消此任務的執行。

3.3. ThreadPoolExcutor (推薦)

線程池實現類 ThreadPoolExecutor是 Executor 框架最核心的類。

3.3.1.構造方法

ThreadPoolExecutor 類中提供的四個構造方法。我們來看最長的那個,其餘三個都是在這個構造方法的基礎上產生(其他幾個構造方法說白點都是給定某些默認參數的構造方法比如默認制定拒絕策略是什麼)

 /**
     * 用給定的初始參數創建一個新的ThreadPoolExecutor。
     */
    public ThreadPoolExecutor(int corePoolSize,//線程池的核心線程數量
                              int maximumPoolSize,//線程池的最大線程數
                              long keepAliveTime,//當線程數大於核心線程數時,多餘的空閒線程存活的最長時間
                              TimeUnit unit,//時間單位
                              BlockingQueue<Runnable> workQueue,//任務隊列,用來儲存等待執行任務的隊列
                              ThreadFactory threadFactory,//線程工廠,用來創建線程,一般默認即可
                              RejectedExecutionHandler handler//拒絕策略,當提交的任務過多而不能及時處理時,我們可以定製策略來處理任務
                               ) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

三個重要參數

  • corePoolSize : 核心線程數線程數定義了最小可以同時運行的線程數量。
  • maximumPoolSize : 當隊列中存放的任務達到隊列容量的時候,當前可以同時運行的線程數量變爲最大線程數。
  • workQueue: 當新任務來的時候會先判斷當前運行的線程數量是否達到核心線程數,如果達到的話,信任就會被存放在隊列中。

其它參數

  • keepAliveTime:當線程池中的線程數量大於 corePoolSize 的時候,如果這時沒有新的任務提交,核心線程外的線程不會立即銷燬,而是會等待,直到等待的時間超過了keepAliveTime纔會被回收銷燬;
  • unit : keepAliveTime參數的時間單位。
  • threadFactory :executor 創建新線程的時候會用到。
  • handler :飽和策略。關於飽和策略下面單獨介紹一下。

各參數示意圖

8WZ2Kf.jpg

飽和策略

如果當前同時運行的線程數量達到最大線程數量並且隊列也已經被放滿了任時,ThreadPoolTaskExecutor定義一些策略:

  • ThreadPoolExecutor.AbortPolicy:拋出 RejectedExecutionException來拒絕新任務的處理。
  • ThreadPoolExecutor.CallerRunsPolicy調用執行自己的線程運行任務,也就是直接在調用 execute 方法的線程中運行(run)被拒絕的任務,如果執行程序已關閉,則會丟棄該任務。
    • 因此這種策略會降低對於新任務提交速度,影響程序的整體性能。
    • 另外,這個策略喜歡增加隊列容量。如果您的應用程序可以承受此延遲並且你不能丟棄任何一個任務請求的話,你可以選擇這個策略。
  • ThreadPoolExecutor.DiscardPolicy: 不處理新任務,直接丟棄掉
  • ThreadPoolExecutor.DiscardOldestPolicy: 此策略將丟棄最早的未處理的任務請求。

舉例

  • Spring 通過 ThreadPoolTaskExecutor 或者我們直接通過 ThreadPoolExecutor 的構造函數創建線程池的時候,
  • 當我們不指定 RejectedExecutionHandler 飽和策略的話來配置線程池的時候默認使用的是 ThreadPoolExecutor.AbortPolicy
  • 在默認情況下,ThreadPoolExecutor將拋出 RejectedExecutionException 來拒絕新來的任務 ,這代表你將丟失對這個任務的處理。
  • 對於可伸縮的應用程序,建議使用 ThreadPoolExecutor.CallerRunsPolicy。當最大池被填滿時,此策略爲我們提供可伸縮隊列。

workQueue

  • SynchronousQueue
    • 這是一個內部沒有任何容量的阻塞隊列,任何一次插入操作的元素都要等待相對的刪除/讀取操作,否則進行插入操作的線程就要一直等待,反之亦然。
  • LinkedBlockingQueue
    • 一個由數組支持的有界阻塞隊列。此隊列按 FIFO(先進先出)原則對元素進行排序。
  • ArrayBlockingQueue
    • 基於鏈表結構,無限隊列。這個容量就是 Integer.MAX_VALUE

3.3.2.使用示例

我們來示例Callable+ThreadPoolExecutor

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        Thread.sleep(1000);
        //返回執行當前 Callable 的線程名字
        return Thread.currentThread().getName();
    }
}
public class CallableDemo {

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;

    public static void main(String[] args) {

        //使用阿里巴巴推薦的創建線程池的方式
        //通過ThreadPoolExecutor構造函數自定義參數創建
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        List<Future<String>> futureList = new ArrayList<>();
        Callable<String> callable = new MyCallable();
        for (int i = 0; i < 10; i++) {
            //提交任務到線程池
            Future<String> future = executor.submit(callable);
            //將返回值 future 添加到 list,我們可以通過 future 獲得 執行 Callable 得到的返回值
            futureList.add(future);
        }
        for (Future<String> fut : futureList) {
            try {
                System.out.println(new Date() + "::" + fut.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
        //關閉線程池
        executor.shutdown();
    }
}

線程池原理示意

8WQnJg.png

3.4.工具類 Executors (不推薦)

常見的線程池:

  1. FixedThreadPool:返回固定數量線程
  2. SingleThreadExcutor:返回一個線程
  3. CashedThreadPool:按線程數分配,空閒的線程

3.4.1. FixedThreadPool

FixedThreadPool 被稱爲可重用固定線程數的線程池。通過 Executors 類中的相關源代碼來看一下相關實現:

  /**
     * 創建一個可重用固定數量線程的線程池
     */
    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }
  • 從上面源代碼可以看出新創建的 FixedThreadPoolcorePoolSizemaximumPoolSize 都被設置爲 nThreads,這個 nThreads 參數是我們使用的時候自己傳遞的。

爲什麼不推薦

FixedThreadPool 使用無界隊列 LinkedBlockingQueue(隊列的容量爲 Intger.MAX_VALUE)作爲線程池的工作隊列會對線程池帶來如下影響 :

  1. 當線程池中的線程數達到 corePoolSize 後,新任務將在無界隊列中等待,因此線程池中的線程數不會超過 corePoolSize;
  2. 由於使用無界隊列時 maximumPoolSize 將是一個無效參數,因爲不可能存在任務隊列滿的情況。所以,通過創建 FixedThreadPool的源碼可以看出創建的 FixedThreadPoolcorePoolSizemaximumPoolSize 被設置爲同一個值。
  3. 由於 1 和 2,使用無界隊列時 keepAliveTime 將是一個無效參數;
  4. 運行中的 FixedThreadPool(未執行 shutdown()shutdownNow())不會拒絕任務,在任務比較多的時候會導致 OOM(內存溢出)。

3.4.2. SingleThreadExector

SingleThreadExecutor 是隻有一個線程的線程池,下面看看SingleThreadExecutor 的實現:

/**
     *返回只有一個線程的線程池
     */
    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }
  • 從上面源代碼可以看出新創建的 SingleThreadExecutorcorePoolSizemaximumPoolSize 都被設置爲 1 .其他參數和 FixedThreadPool 相同。

爲什麼不推薦使用

  • SingleThreadExecutor 使用無界隊列 LinkedBlockingQueue 作爲線程池的工作隊列(隊列的容量爲 Intger.MAX_VALUE)。
  • SingleThreadExecutor 使用無界隊列作爲線程池的工作隊列會對線程池帶來的影響與 FixedThreadPool 相同。
  • 說簡單點就是可能會導致 OOM,

3.4.3. CachedThreadPool

CachedThreadPool 是一個會根據需要創建新線程的線程池。下面通過源碼來看看 CachedThreadPool 的實現:

/**
     * 創建一個線程池,根據需要創建新線程,但會在先前構建的線程可用時重用它。
     */
    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }
  • CachedThreadPoolcorePoolSize 被設置爲空(0),maximumPoolSize被設置爲 Integer.MAX.VALUE,即它是無界的,
  • 這也就意味着如果主線程提交任務的速度高於 maximumPool 中線程處理任務的速度時,CachedThreadPool 會不斷創建新的線程。極端情況下,這樣會導致耗盡 cpu 和內存資源。

爲什麼不推薦使用

CachedThreadPool允許創建的線程數量爲 Integer.MAX_VALUE ,可能會創建大量線程,從而導致 OOM。

4.線程安全

線程安全問題都是由全局變量及靜態變量引起的

  • 若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;
  • 若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則的話就可能影響線程安全。

4.1. synchronized 同步

同步代碼塊

  • 同步代碼塊: synchronized 關鍵字可以用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。
  • 同步鎖:對象的同步鎖只是一個概念,可以想象爲在對象上標記了一個鎖.
    1. 鎖對象 可以是任意類型。
    2. 多個線程對象 要使用同一把鎖。
  • 在任何時候,最多允許一個線程擁有同步鎖,誰拿到鎖就進入代碼塊,其他的線程只能在外等着(BLOCKED)。
synchronized(同步鎖){
	需要同步操作的代碼
}

同步方法

  • 同步方法:使用synchronized修飾的方法,就叫做同步方法,保證A線程執行該方法的時候,其他線程只能在方法外等着。
  • 同步鎖
    • 對於非 static 方法,同步鎖就是 this 。
    • 對於 static 方法,我們使用當前方法所在類的字節碼對象(類名.class)。
public synchronized void method(){
	可能會產生線程安全問題的代碼
}

4.2. ReentrantLock鎖

ReentrantLock輕量級鎖)也可以叫對象鎖,可重入鎖,互斥鎖。synchronized重量級鎖,JDK前期的版本lock比synchronized更快,在JDK1.5之後synchronized引入了偏向鎖,輕量級鎖和重量級鎖。以致兩種鎖性能旗鼓相當,看個人喜歡。

  • java.util.concurrent.locks.Lock 機制提供了比synchronized代碼塊和synchronized方法更廣泛的鎖定操作,
  • 同步代碼塊/同步方法具有的功能 Lock 都有,除此之外更強大,更體現面向對象。

Lock 鎖也稱同步鎖,加鎖與釋放鎖方法化了:

  • public void lock():加同步鎖。
  • public void unlock():釋放同步鎖。
  • ReentrantLock.tryLock():它表示用來嘗試獲取鎖
    • 如果獲取成功,則返回true,
    • 如果獲取失敗(即鎖已被其他線程獲取),則返回 false ,
    • 這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待。
  • ReentrantLock.tryLock(long timeout,TimeUnit unit):和tryLock()方法是類似的
    • 這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。
    • 如果如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。
public class Ticket implements Runnable{ 
    private int ticket = 100;
    Lock lock = new ReentrantLock();
    /*
    * 執行賣票操作
    */ @Override
    public void run() {
        //每個窗口賣票的操作
        //窗口 永遠開啓
        while(true){
    		lock.lock(); 
            if(ticket>0){//有票 可以賣
                //出票操作
                //使用sleep模擬一下出票時間
                try {
                Thread.sleep(50);
                } catch (InterruptedException e) {
                // TODO Auto‐generated catch block e.printStackTrace();
                }
                //獲取當前線程對象的名字
                String name = Thread.currentThread().getName();
                System.out.println(name+"正在賣:"+ticket‐‐);
   		 }
   		 lock.unlock();
    	}
    }	
}

4.3.等待/喚醒機制

此處參考

  • synchronized關鍵字與 wait()notify() / notifyAll() 方法相結合可以實現等待/通知機制

    • 對象名.wait():在其他線程調用此對象的 notify() 方法或 notifyAll() 方法前,導致當前線程等待。
    • 對象名.notify():喚醒在此對象監視器上等待的單個線程。
    • 對象名.notifyAll():喚醒在此對象監視器上等待的所有線程。
  • ReentrantLock類當然也可以實現,但是需要藉助於Condition 接口與 new Condition() 方法。

    • void await() :造成當前線程在接到信號或被中斷之前一直處於等待狀態。
    • void signal() :喚醒一個等待線程。
    • void signalAll(): 喚醒所有等待線程。
    class BoundedBuffer {
                final Lock lock = new ReentrantLock(); //創建一個Lock的子類對象 lock
                final Condition notFull = lock.newCondition(); //調用lock的newCondition方法,創建一個condition子類對象 notFull
                final Condition notEmpty = lock.newCondition(); //調用lock的newCondition方法,創建一個condition子類對象 notEmpty
    
                public void put(Object x) throws InterruptedException { //
                    lock.lock(); //獲取鎖
                    try {
                        while (判斷語句)
                          notFull.await(); //判斷成功,線程等待於notFull下。
    
                        操作代碼
    
                        notEmpty.signal(); //喚醒notEmpty下的等待線程。
                    } finally { //保證其後語句執行。
                         lock.unlock(); //釋放鎖。
                    }
                }
    
                public Object take() throws InterruptedException {
                    lock.lock();
                    try {
                      while ()
                        notEmpty.await();
                      操作代碼
                      notFull.signal();
    
                    } finally {
                          lock.unlock();
                    }
                }
              }
    

4.4.兩者的區別

  1. synchronized 依賴於 JVM 而 ReentrantLock 依賴於 API
    • synchronized 是依賴於 JVM 實現的,虛擬機團隊在 JDK1.6 爲 synchronized 關鍵字進行了很多優化,但是這些優化都是在虛擬機層面實現的,並沒有直接暴露給我們。
    • ReentrantLock 是 JDK 層面實現的(也就是 API 層面,需要 lock() 和 unlock() 方法配合 try/finally 語句塊來完成),所以我們可以通過查看它的源代碼,來看它是如何實現的。
  2. ReentrantLock 比 synchronized 增加了一些高級功能
    • ReentrantLock提供了一種能夠中斷等待鎖的線程的機制,通過lock.lockInterruptibly()來實現這個機制。也就是說正在等待的線程可以選擇放棄等待,改爲處理其他事情。
    • ReentrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。 ReentrantLock默認情況是非公平的,可以通過 ReentrantLock類的ReentrantLock(boolean fair)構造方法來制定是否是公平的。
    • 可實現選擇性通知(鎖可以綁定多個條件) 在使用 notify()/notifyAll() 方法進行通知時,被通知的線程是由 JVM 選擇的,用 ReentrantLock 類結合 Condition 實例可以實現“選擇性通知”

5.拓展

5.1. ConcurrentHashMap 鎖

我們發現 HashTable 雖然能保證線程安全但是效率低下,而 HashMap 雖然效率高於 HashTable 但是是非線程安全的。

HashMap

HashMap:它根據鍵的 hashCode 值存儲數據,大多數情況下可以直接定位到它的值,因而具有很快的訪問速度,但遍歷順序卻是不確定的。

  • HashMap最多隻允許一條記錄的鍵爲null**,允許多條記錄的值爲null**。
  • HashMap非線程安全,即任一時刻可以有多個線程同時寫HashMap,可能會導致數據的不一致。如果需要滿足線程安全,
    • 可以用 Collections的synchronizedMap方法使HashMap具有線程安全的能力,
    • 或者使用ConcurrentHashMap。

HashTable

Hashtable 是遺留類,很多映射的常用功能與 HashMap 類似,不同的是它承自 Dictionary 類,並且是線程安全的,任一時間只有一個線程能寫Hashtable,併發性不如 ConcurrentHashMap,因爲 ConcurrentHashMap 引入了分段鎖。Hashtable 不建議在新代碼中使用,不需要線程安全的場合可以用 HashMap替換,需要線程安全的場合可以用 ConcurrentHashMap 替換。

  • 確保同一時間只有一個線程對同步方法的佔用,避免多個線程同時對數據的修改,確保線程的安全性。
  • HashTable 對 get,put,remove 方法都使用了同步操作,這就造成如果兩個線程都只想使用 get 方法去讀取數據時,因爲一個線程先到進行了鎖操作,另一個線程就不得不等待,這樣必然導致效率低下,而且競爭越激烈,效率越低下。

併發又安全的 ConcurrentHashMap

ConcourrentHashMap 保證線程安全的方法是:分段鎖技術

  • 在hashMap 的基礎上,ConcurrentHashMap 將數據分爲多個segment(默認16個),然後每次操作對一個segment 加鎖

    • 由於所有訪問HashTable的線程都必須競爭同一把鎖,而ConcurrentHashMap 將數據分到多個segment 中(默認16,也可在申明時自己設置,不過一旦設定就不能更改,擴容都是擴充各個segment 的容量)
    • 每個segment 都有一個自己的鎖,只要多個線程訪問的不是同一個segment 就沒有鎖爭用,就沒有堵塞,也就是允許16個線程併發的更新而儘量沒有鎖爭用。
  • ConcurrentHashMap 的 segment 就類似一個HashTable,但比HashTable 更加優化,

    • 前面說過 HashTable 對 get,put,remove 方法都會使用鎖,
    • 而 ConcurrnetHashMap 中get 方法是不涉及到鎖的。在併發讀取時,除了key 對應的 value 爲 null 外,並沒有用到鎖,所以對於讀操作無論多少線程併發都是安全高效的。

5.2. CountDownLatch

CountDownLatch 是在 java1.5 被引入,存在於 java.util.cucurrent 包下。是一個同步工具類,用來協調多個線程之間的同步請參考

  • CountDownLatch這個類使一個線程等待其他線程各自執行完畢後再執行。

  • 是通過一個計數器來實現的,計數器的初始值是線程的數量。每當一個線程執行完畢後,計數器的值就 -1,當計數器的值爲0時,表示所有線程都執行完畢,然後在閉鎖上等待的線程就可以恢復工作了。

應用場景

  1. 某一線程在開始運行前等待n個線程執行完畢。一個典型應用場景就是啓動一個服務時,主線程需要等待多個組件加載完畢,之後再繼續執行。
    • 將CountDownLatch的計數器初始化爲new CountDownLatch(n),每當一個任務線程執行完畢,就將計數器減1, 當計數器的值變爲0時,在CountDownLatch上await()的線程就會被喚醒。
  2. 實現多個線程開始執行任務的最大並行性。注意是並行性,不是併發,強調的是多個線程在某一時刻同時開始執行。
    • 做法是初始化一個共享的 CountDownLatch(1),將其計算器初始化爲1,多個線程在開始執行任務前首先countdownlatch.await(),當主線程調用 countDown()時,計數器變爲0,多個線程同時被喚醒。

不足

  • CountDownLatch是一次性的,計算器的值只能在構造方法中初始化一次,之後沒有任何機制再次對其設置值
  • 當CountDownLatch使用完畢後,它不能再次被使用

常用方法

  • CountDownLatch(int count):構造方法,創建一個值爲count 的計數器。
  • await():阻塞當前線程,將當前線程加入阻塞隊列。
  • await(long timeout, TimeUnit unit):在timeout的時間之內阻塞當前線程,時間一過則當前線程可以執行,
  • countDown():對計數器進行遞減1操作,當計數器遞減至0時,當前線程會去喚醒阻塞隊列裏的所有線程。
  • getCount():獲取 當前 count 值,是個會變的東西,在循環裏使用時需注意

使用示例

public class TestCountDownLatch {

    public static void main(String[] args){
		//CountDownLatch 爲唯一的、共享的資源
        final CountDownLatch latch = new CountDownLatch(5);
		
        LatchDemo latchDemo = new LatchDemo(latch);

        long begin = System.currentTimeMillis();

        for (int i = 0; i <5 ; i++) {
            new Thread(latchDemo).start();
        }
        try {
            //多線程運行結束前一直等待
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        
        System.out.println("耗費時間:"+(end-begin));

    }
}

class LatchDemo implements  Runnable{

    private CountDownLatch latch;

    public LatchDemo(CountDownLatch latch){
        this.latch=latch;
    }
    public LatchDemo(){
        super();
    }

    @Override
    public void run() {
        //當前對象唯一,使用當前對象加鎖,避免多線程問題
        synchronized (this){
            try {
                for (int i = 0; i < 50000; i++) {
                    if (i%2==0){
                        System.out.println(i);
                    }
                }
            }finally {
                //保證肯定執行
                latch.countDown();
            }
        }
    }
}

5.3. ReadWriteLock 讀寫鎖

我們編程想要實現的最好效果是,可以做到讀和讀互不影響讀和寫互斥寫和寫互斥,提高讀寫的效率,如何實現呢? 推薦閱讀

java併發包中 ReadWriteLock 是一個接口,

  • ReadWriteLock 管理一組鎖,一個是隻讀的鎖,一個是寫鎖。
  • Java 併發庫中ReetrantReadWriteLock 實現了 ReadWriteLock 接口並添加了可重入的特性。

主要有兩個方法,如下:

  • Lock readLock();
  • Lock writeLock();

進入條件

  • 線程進入讀鎖的前提條件:
    • 沒有其他線程的寫鎖
    • 沒有寫請求或者有寫請求,但調用線程和持有鎖的線程是同一個。
  • 線程進入寫鎖的前提條件:
    • 沒有其他線程的讀鎖
    • 沒有其他線程的寫鎖

重要特性

  • 公平選擇性:支持非公平(默認)和公平的鎖獲取方式,吞吐量還是非公平優於公平。
  • 可重進入:讀鎖和寫鎖都支持線程重進入。
  • 鎖降級:遵循獲取寫鎖、獲取讀鎖再釋放寫鎖的次序,寫鎖能夠降級成爲讀鎖。

使用示例

public class TestReadWriteLock {

    public static void main(String[] args){
        ReadWriteLockDemo rwd = new ReadWriteLockDemo();
		//啓動100個讀線程
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    rwd.get();
                }
            }).start();
        }
        //寫線程
        new Thread(new Runnable() {
            @Override
            public void run() {
                rwd.set((int)(Math.random()*101));
            }
        },"Write").start();
    }
}

class ReadWriteLockDemo{
	//模擬共享資源--Number
    private int number = 0;
	// 實際實現類--ReentrantReadWriteLock,默認非公平模式
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    //讀
    public void get(){
    	//使用讀鎖
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+" : "+number);
        }finally {
            readWriteLock.readLock().unlock();
        }
    }
    //寫
    public void set(int number){
        readWriteLock.writeLock().lock();
        try {
            this.number = number;
            System.out.println(Thread.currentThread().getName()+" : "+number);
        }finally {
            readWriteLock.writeLock().unlock();
        }
    }
}

5.4.線程八鎖

  • 一個對象裏面如果有多個 synchronized 方法,某一個時刻內,只要一個線程去調用其中的一個 synchronized 方法了,其他的線程都只能等待,換句話說,某一時刻內,只能有唯一一個線程去訪問這些 synchronized 方法。

  • 鎖的是當前對象 this,被鎖定後,其他線程都不能進入到當前對象的其他的 synchronized 方法。

  • 加個普通方法後發現和同步鎖無關。

  • 換成靜態同步方法後,情況又變化

  • 所有的非靜態同步方法用的都是同一把鎖,即實例對象本身,或者說 this 對象,

    • 如果一個實例對象的非靜態同步方法獲取鎖後,該實例對象的其他非靜態同步方法必須等待獲取鎖的方法釋放鎖後才能獲取鎖。
    • 如果別的對象的非靜態同步方法與該實例對象的非靜態同步方法獲取不同的鎖,則不需要等待。
  • 所有的靜態同步方法用的也是同一把鎖,即類對象本身,所以靜態同步方法與非靜態同步方法之間是不會有競態條件的,但是一個靜態同步方法獲取Class實例的鎖後,其他靜態同步方法都必須等待該方法釋放鎖才能獲取鎖。

  • 所謂線程八鎖實際上對應於是否加上 synchronized,是否加上 static 等8種常見情況,

代碼如下

/*
 * 題目:判斷打印的 "one" or "two" ?
 * 
 * 1. 兩個普通同步方法,兩個線程,標準打印, 打印? //one  two
 * 2. 新增 Thread.sleep() 給 getOne() ,打印? //one  two
 * 3. 新增普通方法 getThree() , 打印? //three  one   two
 * 4. 兩個普通同步方法,兩個 Number 對象,打印?  //two  one
 * 5. 修改 getOne() 爲靜態同步方法,打印?  //two   one
 * 6. 修改兩個方法均爲靜態同步方法,一個 Number 對象?  //one   two
 * 7. 一個靜態同步方法,一個非靜態同步方法,兩個 Number 對象?  //two  one
 * 8. 兩個靜態同步方法,兩個 Number 對象?   //one  two
 * 
 * 線程八鎖的關鍵:
 * ①非靜態方法的鎖默認爲  this,  靜態方法的鎖爲 對應的 Class 實例
 * ②某一個時刻內,只能有一個線程持有鎖,無論幾個方法。
 */
public class TestThread8Monitor {

    public static void main(String[] args) {
        Number number = new Number();
        Number number2 = new Number();

        new Thread(new Runnable() {
            @Override
            public void run() {
                number.getOne();
            } 
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
//              number.getTwo();
                number2.getTwo();
            }
        }).start();

        /*new Thread(new Runnable() {
            @Override
            public void run() {
                number.getThree();
            }
        }).start();*/

    }

}

class Number{

    public static synchronized void getOne(){//Number.class
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
        }

        System.out.println("one");
    }

    public synchronized void getTwo(){//this
        System.out.println("two");
    }

    public void getThree(){
        System.out.println("three");
    }

}

5.5. volatile

在併發編程中,我們通常會遇到以下三個問題:原子性問題,可見性問題,有序性問題。 參考

原子性

即一個操作或者多個操作,要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。原子性就像數據庫裏面的事務一樣,他們是一個團隊,同生共死。

可見性

可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

  • 對於可見性,Java提供了volatile關鍵字來保證可見性
  • 當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。
  • 通過 synchronized 和 Lock 也能夠保證可見性

有序性

即程序執行的順序按照代碼的先後順序執行。

  • 在 Java 裏面,可以通過 volatile 關鍵字來保證一定的“有序性”。
  • 另外可以通過 synchronized 和 Lock 來保證有序性。

volatile

  • volatile 是一個類型修飾符。volatile 的作用是作爲指令關鍵字,確保本條指令不會因編譯器的優化而省略。

volatile 的特性

  • 保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。(實現可見性)
  • 禁止進行指令重排序。(實現有序性)
  • volatile 只能保證對單次讀/寫的原子性。i++ 這種操作不能保證原子性
  • 更多細節:參考

5.6.悲觀鎖與樂觀鎖

此處參考

悲觀鎖

總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完後再把資源轉讓給其它線程)。

  • 傳統的關係型數據庫裏邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。
  • Java 中synchronizedReentrantLock等獨佔鎖就是悲觀鎖思想的實現。

樂觀鎖

總是假設最好的情況,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號機制和CAS算法實現。

  • 樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像數據庫提供的類似於write_condition機制,其實都是提供的樂觀鎖。
  • 在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。

使用場景

  • 樂觀鎖適用於寫比較少的情況下(多讀場景)
  • 一般多寫的場景下用悲觀鎖就比較合適。

樂觀鎖的實現

  • 版本號機制
    • 一般是在數據表中加上一個數據版本號 version字段,表示數據被修改的次數,當數據被修改時,version值會加一。當線程A要更新數據值時,在讀取數據的同時也會讀取 version值,在提交更新時,若剛纔讀取到的version值爲當前數據庫中的 version值相等時才更新,否則重試更新操作,直到更新成功。
  • CAS 算法

樂觀鎖的缺點(同時也是CAS的缺點)

  1. ABA 問題

如果一個變量V初次讀取的時候是A值,並且在準備賦值的時候檢查到它仍然是A值,那我們就能說明它的值沒有被其他線程修改過了嗎?很明顯是不能的,因爲在這段時間它的值可能被改爲其他值,然後又改回A,那CAS操作就會誤認爲它從來沒有被修改過。這個問題被稱爲CAS操作的 "ABA"問題。

JDK 1.5 以後的 AtomicStampedReference類就提供了此種能力,其中的 compareAndSet方法就是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。

因此,在使用CAS前要考慮清楚“ABA”問題是否會影響程序併發的正確性,如果需要解決ABA問題,改用傳統的互斥同步可能會比原子類更高效。

  1. 循環時間長開銷大

    自旋CAS(也就是不成功就一直循環執行直到成功)如果長時間不成功,會給CPU帶來非常大的執行開銷。 如果JVM能支持處理器提供的 pause 指令那麼效率會有一定的提升,pause 指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使 CPU 不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出循環的時候因內存順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。

  2. 只能保證一個共享變量的原子操作

    CAS 只對單個共享變量有效,當操作涉及跨多個共享變量時 CAS 無效。但是從 JDK 1.5開始,提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象裏來進行 CAS 操作.所以我們可以使用鎖或者利用AtomicReference類把多個共享變量合併成一個共享變量來操作。

5.7. CAS 算法

即 compare and swap(比較與交換),是一種有名的無鎖算法。無鎖編程,即不使用鎖的情況下實現多線程之間的變量同步,也就是在沒有線程被阻塞的情況下實現變量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。 參考

CAS 算法會先對一個內存變量(位置) V 和一個給定的值進行比較 A ,如果相等,則用一個新值 B 去修改這個內存變量(位置)。上述過程會作爲一個原子操作完成 (intel處理器通過 cmpxchg 指令系列實現)。CAS 原子性保證了新值的計算是基於上一個有效值,期間如果內存變量(位置) V 被其他線程更新了,本線程的 CAS 更新操作將會失敗。CAS 操作必須告訴調用者成功與否,可以返回一個 boolean 值來表示,或者返回一個從內存變量讀到的值 (應該是上一次有效值)

CAS 操作數有三個:

  • 內存變量(位置) V,表示被更新的變量
  • 線程上一次讀到的舊值 A
  • 用來覆蓋 V 的新值 B

CAS 表示:“我認爲現在 V 的值還是之前我讀到的舊值 A,若是則用新值 B 覆蓋內存變量 V,否則不做任何動作並告訴調用者操作失敗”。CAS 是一項樂觀鎖技術,他在更新的時候總是希望能成功 (沒有衝突),但也能檢測出來自其他線程的衝突和干擾

++i 自增操作

incrementAndGet

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

這裏採用了 CAS 操作,每次從中讀取數據都會將此數據和 +1 後的結果進行 CAS 操作,如果成功則返回結果否則重試到成功爲止,而這裏的compareAndSet利用 JNI( JNI: Java Native Interface爲 JAVA 本地調用,允許java 調用其他語言。)來完成CPU的指令操作

compareAndSet 的代碼

public final boolean compareAndSet(int expect, int update) {   
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

整體的過程就是這樣子的,利用 CPU的CAS指令,同時藉助 JNI 來完成Java的非阻塞算法。其它原子操作都是利用類似的特性完成的。

compareAndSwapInt (native)類似這樣的邏輯:

unsafe.compareAndSwapInt(this, valueOffset, expect, update);

if (this == expect) {
  this = update
 return true;
} else {
return false;
} 

CAS是通過調用 JNI 代碼實現的,而 compareAndSwapInt 就是就是借用 CAS 來調用 CPU底層指令實現的,調用了Atomic::cmpxchg方法

5.8. Atomic 原子類

Atomic 翻譯成中文是原子的意思。在化學上,我們知道原子是構成一般物質的最小單位,在化學反應中是不可分割的。在我們這裏 Atomic 是指一個操作是不可中斷的。即使是在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程干擾。

所以,所謂原子類說簡單點就是具有原子/原子操作特徵的類。

併發包 java.util.concurrent 的原子類都存放在java.util.concurrent.atomic

JUC 包中的原子類主要分爲4類:

基本類型:使用原子的方式更新基本類型

  • AtomicInteger:整形原子類
  • AtomicLong:長整型原子類
  • AtomicBoolean:布爾型原子類

數組類型:使用原子的方式更新數組裏的某個元素

  • AtomicIntegerArray:整形數組原子類
  • AtomicLongArray:長整形數組原子類
  • AtomicReferenceArray:引用類型數組原子類

引用類型

  • AtomicReference:引用類型原子類
  • AtomicStampedReference:原子更新引用類型裏的字段原子類
  • AtomicMarkableReference:原子更新帶有標記位的引用類型,該類將 boolean 標記與引用關聯起來,不能解決ABA的問題,只是會降低ABA問題發生的機率

對象的屬性修改類型

  • AtomicIntegerFieldUpdater:原子更新整形字段的更新器
  • AtomicLongFieldUpdater:原子更新長整形字段的更新器
  • AtomicStampedReference原子更新帶有版本號的引用類型。該類將整數值與引用關聯起來,可用於解決原子的更新數據和數據的版本號,可以解決使用 CAS 進行原子更新時可能出現的 ABA 問題。

暫時碼到這裏

留個參考地址

5.9. ThreadLocal

通常情況下,我們創建的變量是可以被任何一個線程訪問並修改的。

ThreadLocal類主要解決的就是讓每個線程綁定自己的值,可以將ThreadLocal類形象的比喻成存放數據的盒子,盒子中可以存儲每個線程的私有數據。

如果你創建了一個ThreadLocal變量,那麼訪問這個變量的每個線程都會有這個變量的本地副本,這也是ThreadLocal變量名的由來。他們可以使用 get() 和 set() 方法來獲取默認值或將其值更改爲當前線程所存的副本的值,從而避免了線程安全問題。

特點:

  • ThreadLocal 可以像Map 一樣存儲數據(它的 key 就是當前線程對象)
  • 一般 ThreadLocal 定義的變量都是 static 類型
  • 當線程銷燬後,ThreadLocal 中保存到的數據將自動的被 jvm 釋放。

6.常見的區分

6.1 Thread vs Runnable

如果一個類繼承 Thread ,則不適合資源共享。但是如果實現了Runable接口的話,則很容易的實現資源共享。

實現 Runnable 接口比繼承 Thread 類所具有的優勢:

  1. 適合多個相同的程序代碼的線程去共享同一個資源。
  2. 可以避免 java 中的單繼承的侷限性。
  3. 增加程序的健壯性,實現解耦操作,代碼可以被多個線程共享,代碼和線程獨立。
  4. 線程池只能放入實現 Runable 或 Callable 類線程,不能直接放入繼承 Thread 的類。

6.2 Runnable vs Callable

  • Runnable 接口不會返回結果或拋出檢查異常,但是 Callable 接口可以。所以,如果任務不需要返回結果或拋出異常推薦使用 Runnable 接口,這樣代碼看起來會更加簡潔。
  • 工具類 Executors 可以實現 Runnable 對象和 Callable 對象之間的相互轉換。
    • Executors.callable(Runnable task)
    • Executors.callable(Runnable task,Object resule)

6.3. sleep vs wait

  • 兩者最主要的區別在於:sleep 方法沒有釋放鎖,而 wait 方法釋放了鎖
  • 兩者都可以暫停線程的執行。
  • wait 通常被用於線程間交互/通信,sleep 通常被用於暫停執行
  • wait() 方法被調用後,線程不會自動甦醒,需要別的線程調用同一個對象上的 notify() 或者 notifyAll() 方法。
  • sleep() 方法執行完成後,線程會自動甦醒。或者可以使用 wait(long timeout) 超時後線程會自動甦醒。

6.4. start vs run

  • 調用 start 方法方可啓動線程並使線程進入就緒狀態,
  • 而 run 方法只是 thread 的一個普通方法調用,還是在主線程裏執行。

所以,我們調用 start() 方法時會自動執行 run() 方法,但不能直接調用 run() 方法

6.5. excute vs submit

  • execute() 方法用於提交不需要返回值的任務,所以無法判斷任務是否被線程池執行成功與否
  • submit() 方法用於提交需要返回值的任務。線程池會返回一個 Future 類型的對象,通過這個 Future 對象可以判斷任務是否執行成功,並且可以通過 Future 的 get() 方法來獲取返回值,get()方法會阻塞當前線程直到任務完成,而使用 get(long timeout,TimeUnit unit)方法則會阻塞當前線程一段時間後立即返回,這時候有可能任務沒有執行完。

6.6. shutdown 關閉

  • shutdown() :關閉線程池,線程池的狀態變爲 SHUTDOWN。線程池不再接受新任務了,但是隊列裏的任務得執行完畢。
  • shutdownNow() :關閉線程池,線程的狀態變爲 STOP。線程池會終止當前正在運行的任務,並停止處理排隊的任務並返回正在等待執行的 List。
  • isShutDown 當調用 shutdown() 方法後返回爲 true。
  • isTerminated 當調用 shutdown() 方法後,並且所有提交的任務完成後返回爲 true

以後在拓展:插個眼

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