主頁 > 後端開發 > Java并發編程學習 + 原理分析(建議收藏)

Java并發編程學習 + 原理分析(建議收藏)

2021-04-27 20:46:28 後端開發

個人博客歡迎訪問

作者總結不易,點贊,關注支持一下,

Doug Lea是一個無私的人,他深知分享知識和分享蘋果是不一樣的,蘋果會越分越少,而自己的知識并不會因為給了別人就減少了,知識的分享更能激蕩出不一樣的火花,

目錄

  • 執行緒介紹
    • 程式、行程、執行緒基本概念
      • 行程與執行緒的區別
      • 什么是背景關系切換
      • 串行、并行和并發有什么區別
      • 使用多執行緒的優缺點
      • 何時需要多執行緒
      • 并發編程的三要素
  • 執行緒實作
    • 執行緒的創建和使用
      • 執行緒的創建和啟動
      • 擴展問題
        • run()和start()有什么區別
        • 為什么我們呼叫 start() 方法時會執行 run() 方法,為什么我們不能直接呼叫 run() 方法?
      • Thread類
      • API中創建執行緒的三種方式
        • 繼承Thread類
        • 實體
        • 實作Runnable介面
        • 靜態代理&動態代理
          • -靜態代理
          • -動態代理
            • JDK動態代理
            • CGILIB動態代理
        • 實作Callable介面
        • 實體
        • Callable和Runnable關系
        • Callable和Runnable區別
      • 小結
      • 初始并發問題
      • 龜兔賽跑模擬多執行緒
  • 執行緒狀態
    • 執行緒的五種狀態
      • 執行緒的狀態轉換
      • 執行緒的方法
      • ~~stop~~
      • sleep
        • sleep的作用
          • 模擬網路延遲
          • 模擬倒計時
      • yield
      • join
      • state
      • priority
      • 守護執行緒
      • 守護執行緒和用戶執行緒有什么區別
    • 中斷執行緒
  • 執行緒同步
    • 執行緒不安全案例
    • 同步方法
      • 同步方法
      • 同步塊
    • 死鎖
      • 死鎖的案例
      • 產生死鎖的條件
      • 如何避免執行緒死鎖
      • 排查死鎖
      • 案例
    • AQS
      • 屬性
      • 內部結構
        • 同步佇列
          • 結構圖
        • 條件佇列
          • 結構圖
      • 條件等待佇列處理程序
      • LockSupport
    • 共享鎖與排它鎖
      • 排它鎖
        • lock
        • acquire
        • addWaiter
        • acquireQueued
        • CAS自旋volatile變數
        • shouldParkAfterFailedAcquire
        • parkAndCheckInterrupt
        • 總結
          • acquireQueued的具體流程
          • acquire的具體流程
        • release(int)
        • tryRelease(int)
        • unparkSuccessor(Node)
        • 擴展問題
      • 共享鎖
        • 共享鎖執行原理
        • acquireShared
        • doAcquireShared
        • setHeadAndPropagate
        • 總結
        • releaseShared
        • doReleaseShared
        • Mutex(互斥鎖)
    • 公平鎖和非公平鎖
      • 公平鎖和非公平鎖在代碼層面怎么體現呢
      • 非公平鎖圖解
      • 非公平鎖原始碼
        • nonfairTryAcquire
        • tryRelease
      • 公平鎖圖解
      • 公平鎖原始碼
        • tryAcquire
        • hasQueuedPredecessors
      • 非公平鎖和公平鎖的區別
    • 樂觀鎖和悲觀鎖
      • 樂觀鎖
        • 樂觀鎖的問題
      • 悲觀鎖
        • synchronized對物件進行加鎖
        • synchronized對方法進行加鎖
        • synchronized 應用在同步塊上
        • 小結
      • CAS和synchronized的使用場景
    • 可重入鎖和不可重組鎖
      • 可重入鎖
      • 不可重組鎖
    • transient
    • 競態條件和資料競爭
  • 執行緒協作
    • 執行緒通信
      • wait()和sleep()方法的區別
      • 解決方法
        • **方式1**:生產者消費者(管程法)
        • **方式2**:(信號燈法)
    • 執行緒池
      • 創建執行緒池
  • JUC并發編程
    • 傳統synchronized方法
    • 傳統Lock方法
      • tryLock()和lock()的區別
    • 執行緒之間通信問題
      • 生產者和消費者問題
        • synchronized版
          • 虛假喚醒
        • JUC版
          • Condition實作精準喚醒
    • synchronized
      • 實作原理
      • 鎖的物件問題
      • synchronized優化
      • synchronized和Lock對比
      • synchronized 和 ReentrantLock 區別是什么?
    • 集合不安全類
      • ArrayList
        • 測驗ArrayList的執行緒不安全性
        • 解決方案
        • CopyOnWriteArrayList原理
      • Set
        • 測驗Set的執行緒不安全性
        • 解決方案
      • Map
        • 測驗HashMap的執行緒不安全性
        • 解決方案
        • Hashtable
        • Hashtable和HashMap對比
    • 同步容器和并發容器
      • 同步容器
        • 同步器的問題
      • 并發容器
    • 常用輔助類
      • CountDownLatch
      • CyclicBarrier
      • 在 Java 中 CyclicBarrier和 CountDownLatch有什么區別
      • Semaphore
    • 讀寫鎖(ReadWriteLock)
      • ReadWriteLock 是什么
    • 阻塞佇列
      • 四組API
      • SynchronousQueue
    • 執行緒池
      • 創建執行緒池的三大方法
        • 三大方法原始碼分析
      • 引數分析
      • 四個拒絕策略
      • 自定義執行緒池
      • 總結
      • 執行緒池的作業流程
    • 四大函式式介面
      • 函式式介面
      • 斷定性介面
      • 消費型介面
      • 供給性介面
    • 流式編程
    • Future
      • 什么是Future模式
      • Java中的Future
      • 什么是FutureTask
      • ForkJoin
      • CompletableFuture
    • JMM
      • 作業記憶體的八種操作
      • JMM和底層實作原理
        • 現代計算機記憶體模型
        • Java記憶體模型
        • JVM對Java記憶體模型的實作
        • 重排序
        • as-if-serial
      • volatile關鍵字
        • volatile關鍵字的原理和實作機制
        • volatile關鍵字使用場景
        • Happen-Before
      • synchronized和volatile的區別
    • 單例模式
      • 餓漢式
      • 懶漢式
        • 普通懶漢式
        • 同步方法的懶漢式
        • 雙重檢查懶漢式
        • 靜態內部類(推薦)
      • Enum
      • 總結
        • 餓漢式和懶漢式區別
        • 單例模式的優缺點
    • CAS
      • 什么是CAS
      • 實體
      • CAS存在的三大問題
        • ABA問題實作
        • ABA問題解決
  • 并發編程面試題

執行緒介紹

程式、行程、執行緒基本概念

  • 程式(program)是為完成特定任務,用某種編程語言撰寫的一組指令的集合,即指一段靜態的代碼,靜態物件,

  • 行程(process)一個在記憶體中運行的應用程式,每個行程都有自己的獨立的一塊記憶體空間,一個行程可以有多個執行緒,比如Windows系統中,一個運行的*.exe就是一個行程,是一個動態的程序:有它自身的產生、存在和消亡的程序 -----生命周期

    • 如:運行中的QQ,運行中的MP3播放器程式是靜態的,行程是動態的,
    • 行程作為資源分配的單位,系統在運行時會為每個行程分配不同的記憶體區域,
  • 執行緒(thread)行程中的一個執行單元(控制單元),負責當前行程中程式的執行,是一個程式內部的一條執行路徑,一個行程至少有一個執行緒,一個行程可以運行多個執行緒,多個執行緒之間共享資料

    • 若一個行程同一時間并行執行多個執行緒,就是支持多執行緒的

    • 執行緒作為調度和執行的單元,每個執行緒擁有獨立的運行堆疊和程式計數器(PC),執行緒切換的開銷小,

    • 一個行程中的多個執行緒共享相同的記憶體單元/記憶體地址空間,它們從同一個堆中分配物件,可以訪問相同的變數和物件,這就使得執行緒間通信更簡便、高效,但是多個執行緒操作共享的系統資源可能就會帶來安全隱患,

    行程與執行緒

  • 單核CPU和多核CPU的理解

    • 單核CPU,其實是一種假的多執行緒,因為在一個時間單元內,也只能執行一個執行緒的任務,例如:雖然有多車道,但是收費站只有一個作業人員在收費,只有收了費才能通過,那么CPU就好比收費人員,如果有某個人不想交錢,那么收費人員就可以把它“掛起”(晾著他,等他想通了,準備好了錢,再去收費),但是因為CPU時間單元特別短,因此感覺不出來,
    • 如果是多核的話,才能更好地發揮多執行緒的效率,(現在的服務器都是多核的),
    • 一個Java應用程式java.exe,其實至少有三個執行緒:main()主執行緒、gc()垃圾回收執行緒,例外處理執行緒,當然如果發生例外,會影響主執行緒,

行程與執行緒的區別

執行緒具有許多傳統行程所具有的特征,故又稱為輕型行程(Light—Weight Process)或行程元;而把傳統的行程稱為重新行程(Heavy—Weight Process),它相當于只有一個執行緒的任務,在引入執行緒的作業系統中,通常一個行程有若干個執行緒,至少包含一個執行緒,

區別行程執行緒
根本區別行程是作業系統資源分配的基本單元執行緒是處理器任務調度和執行的基本單元
資源開銷每個行程都有獨立的代碼和資料空間(程式背景關系),程式之間切換會有較大的開銷執行緒可以看做輕量級的行程,同一執行緒共享代碼和資料空間,每個執行緒都有自己獨立的運行堆疊和程式計數器(PC),執行緒之間切換的開銷小
包含關系如果一個行程中有多個執行緒,則執行程序不是一條線的,而是多條線共同完成的執行緒是行程的一部分,所以執行緒也被稱為輕權行程或者輕量級行程
記憶體分配一個行程崩潰后,在保護模式下不會對其他行程產生影響一個執行緒崩潰整個行程都會死掉,所以多行程比多執行緒健壯
執行程序每個獨立的行程有程式運行的入口、順序執行序列和程式出口執行緒不能獨立執行,必須依存在應用程式中,由應用程式提供多個執行緒執行控制,兩者均可并發執行

什么是背景關系切換

多執行緒編程中一般執行緒的個數大于CPU的核心的個數,而一個CPU核心在任意時刻只能被一個執行緒使用,為了讓這些執行緒能得到有效的執行,CPU采取的策略是為每個執行緒分配時間片并輪轉的形式,當一個執行緒的時間片用完的時候就會重新處于就緒狀態讓給其他執行緒使用,這個程序就屬于依次背景關系交換,

概括來說就是:當前任務執行完CPU時間片切換到另一個任務之前會保存自己的狀態,以便下次在切換回這個任務時,可以再加載這個任務的狀態,任務從保存到再加載的程序就是一次背景關系切換

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

Linux 相比與其他作業系統(包括其他類 Unix 系統)有很多的優點,其中有一項就是,其背景關系切換和模式切換的時間消耗非常少,

串行、并行和并發有什么區別

并行:時間單位內,多個CPU同時執行多個任務,真正意義上的同時進行,比如多個人呢同時做不同的事情,

并發:一個CPU(采用時間片)按細分的時間片輪流交替執行,同時執行多個任務,從邏輯上看來這些任務是同時執行的,

串行:有n個任務,由一個執行緒按順序執行,由于任務、方法都在一個執行緒執行所以不存在執行緒不安全情況,也就不存在臨界區的問題,

做一個形象的比喻:

并發 = 兩個佇列和一臺咖啡機,

并行 = 兩個佇列和兩臺咖啡機,

串行 = 一個佇列和一臺咖啡機,

使用多執行緒的優缺點

背景:以單核CPU為例,只使用單個執行緒先后完成多個任務(呼叫多個方法),肯定比用多個執行緒來完成用的時間更短,為何仍需要多執行緒呢?

多執行緒程式的優點:

  • 提高應用程式的回應,對圖形化界面更有意義,可增強用戶體驗,
  • 提高計算機系統CPU的利用率,
  • 改善程式結構,將既長又復雜的行程分為多個執行緒,獨立運行,便于理解和修改,

并發編程的缺點

并發編程的目的就是為了能提高程式的執行效率,提高程式運行速度,但是并發編程并不總是能提高程式運行速度的,而且并發編程可能會遇到很多問題,比如**:記憶體泄漏、背景關系切換、執行緒安全、死鎖**等問題

何時需要多執行緒

  • 程式需要同時執行兩個或多個任務,
  • 程式需要實作一些需要等待的任務時,如:用戶輸入、檔案讀寫操作、網路操作、搜索等,
  • 需要一些后臺運行程式時,

并發編程的三要素

  • 原子性:原子,即一個不可再分割的顆粒,原子性指的是一個或者多個操作(復合操作)要么全部執行成功要么全部執行失敗,
  • 可見性:一個執行緒對共享變數的修改,另一個執行緒能夠立刻看到,(由于CPU和記憶體之間有快取的存在)
  • 有序性:程式執行的順序按照代碼的先后順序執行,(處理器可能會對指令進行重排序)

出現執行緒安全問題的原因:

  • 執行緒切換帶了的原子性問題
  • 快取導致可見性問題
  • 編譯優化帶來的有序性問題

解決辦法

  • 原子性:Atomic開頭的原子類、synchronized、Lock
  • 可見性:CAS、synchronized、Lock
  • 有序性:Happens-Before規則

執行緒實作

執行緒的創建和使用

執行緒的創建和啟動

  • Java語言的JVM允許程式運行多個執行緒,他通過java.lang.Thread類來體現
  • Thread類的特性
    • 每個執行緒都是通過某個特定的Thread物件的run()方法來完成操作的,經常把run()方法的主體稱為執行緒體
    • 通過該Thread()物件的start()方法來啟動這個執行緒,而非直接呼叫run()

擴展問題

run()和start()有什么區別

start和run
  • 每個執行緒都是通過某個特定的Thread物件所對應的方法run()來完成其操作的,run()稱為方法的執行緒體,通過呼叫Thread類的start()方法來啟動一個執行緒,
  • start()方法用于啟動一個執行緒,run()方法用于執行執行緒的代碼,run()可以重復呼叫,而start()只能呼叫一次,
  • start()方法來啟動一個執行緒,真正實作了多執行緒運行,呼叫start()方法無需等待run方法體代碼執行完畢,可以直接繼續執行其他的代碼; 此時執行緒是處于就緒狀態,并沒有運行, 然后通過此Thread類呼叫方法run()來完成其運行狀態, run()方法運行結束, 此執行緒終止,然后CPU再調度其它執行緒,
  • run()方法是在本執行緒里的,只是執行緒里的一個函式,而不是多執行緒的, 如果直接呼叫run(),其實就相當于是呼叫了一個普通函式而已,直接待用run()方法必須等待run()方法執行完畢才能執行下面的代碼,所以執行路徑還是只有一條,根本就沒有執行緒的特征,所以在多執行緒執行時要使用start()方法而不是run()方法,

為什么我們呼叫 start() 方法時會執行 run() 方法,為什么我們不能直接呼叫 run() 方法?

  • 這是另一個非常經典的 java 多執行緒面試問題,而且在面試中會經常被問到,很簡單,但是很多人都會答不上來!
  • new 一個 Thread,執行緒進入了新建狀態,呼叫 start() 方法,會啟動一個執行緒并使執行緒進入了就緒狀態,當分配到時間片后就可以開始運行了, start() 會執行執行緒的相應準備作業,然后自動執行 run() 方法的內容,這是真正的多執行緒作業,
  • 而直接執行 run() 方法,會把 run 方法當成一個 main 執行緒下的普通方法去執行,并不會在某個執行緒中執行它,所以這并不是多執行緒作業,
  • 總結: 呼叫 start 方法方可啟動執行緒并使執行緒進入就緒狀態,而 run 方法只是 thread 的一個普通方法呼叫,還是在主執行緒里執行,

Thread類

構造器

  • Thread():創建新的Thread物件
  • Thread(String threadName):創建執行緒并指定執行緒實體名
  • Thread(Runnable target):指定創建執行緒的目標物件,它實作了Runnable介面中的run方法
  • Thread(Runnable target, String name):創建新的Thread物件

API中創建執行緒的三種方式

JDK1.5之前創建新執行的執行緒有兩種方式:

  • 繼承Thread類的方式
  • 實作Runnable介面的方式
  • 實作Callable介面

繼承Thread類

步驟

  1. 自定義執行緒類繼承Thread類
  2. 重寫run()方法,撰寫執行緒執行體
  3. 創建執行緒物件,呼叫start()方法啟動執行緒
/**
 * @author :zsy
 * @date :Created 2021/4/12 10:11
 * @description:繼承Thread類創建執行緒
 */
//執行緒開啟不一定立即執行,由CPU調度執行
public class TestThread1 extends Thread {
    @Override
    public void run() {
        //run方法執行緒體
        super.run();
        for (int i = 0; i < 20; i++) {
            System.out.println("testThread1---->" + i);
        }
    }

    public static void main(String[] args) {
        //main執行緒,主執行緒

        //創建一個執行緒物件
        TestThread1 thread1 = new TestThread1();

        //呼叫start()方法開啟執行緒
        thread1.start();

        //呼叫run()方法開啟執行緒
        //thread1.run();
        for (int i = 0; i < 20; i++) {
            System.out.println("main---->" + i);
        }
    }
}

實體

實作多執行緒同步下載圖片

/**
 * @author :zsy
 * @date :Created 2021/4/12 17:27
 * @description:練習Thread,實作多執行緒同步下載圖片
 */
public class TestThread2 extends Thread{

    private String url;//網圖地址
    private String name;//保存檔案名

    public TestThread2(String url, String name) {
        this.name = name;
        this.url = url;
    }

    @Override
    public void run() {
        super.run();
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url, name);
        System.out.println("下載的圖片名稱--->" + name);
    }

    public static void main(String[] args) {
        //創建執行緒物件
        TestThread2 t1 = new TestThread2("https://picsum.photos/id/237/300/200", "237.jpg");
        TestThread2 t2 = new TestThread2("https://picsum.photos/id/337/300/200", "337.jpg");
        TestThread2 t3 = new TestThread2("https://picsum.photos/id/437/300/200", "437.jpg");

        //啟動執行緒
        t1.start();
        t2.start();
        t3.start();
    }
}

//下載器
class WebDownloader {

    //下載方法
    public void downloader(String url, String name) {
        try {
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO例外,downloader方法出現例外");
        }
    }
}

執行結果
下載的圖片名稱--->237.jpg
下載的圖片名稱--->437.jpg
下載的圖片名稱--->337.jpg

實作Runnable介面

步驟

  1. 定義MyRunnable類實作Runnable介面
  2. 實作run()方法,撰寫執行緒執行體
  3. 創建執行緒物件,呼叫start()方法啟動執行緒

推薦使用Runnable物件,因為Java單繼承的局限性

/**
 * @author :zsy
 * @date :Created 2021/4/12 18:05
 * @description:實作Runnable介面創建多執行緒
 */
public class TestThread3 implements Runnable{
    @Override
    public void run() {
        //run方法執行緒體
        for (int i = 0; i < 20; i++) {
            System.out.println("testThread1---->" + i);
        }
    }

    public static void main(String[] args) {
        //創建runnable介面的實體物件
        TestThread3 thread3 = new TestThread3();
        
        /*
        //創建執行緒物件,通過執行緒物件來開啟我們的執行緒,代理
        Thread thread = new Thread(thread3);
        //呼叫start()方法開啟執行緒
        thread.start();
        */
        
        new Thread(thread3).start();

        //呼叫run()方法開啟執行緒
        //thread1.run();
        for (int i = 0; i < 20; i++) {
            System.out.println("main---->" + i);
        }
    }
}

靜態代理&動態代理

-靜態代理
  • 代理類需要自己手動實作,自己創建一個java類,表示代理類,
  • 代理的目標類是確定的
  • 特點
    • 實作簡單
    • 容易理解
  • 缺點
    • 當目標類和代理類很多的時候,代理類也需要成倍的增加,
    • 當你的介面中功能增加了,或者修改了,會影響眾多實作類,代理類都需要修改
/**
 * @author :zsy
 * @date :Created 2021/4/12 21:45
 * @description:賣優盤介面
 */
public interface UsbSell {
    float sell();
}


/**
 * @author :zsy
 * @date :Created 2021/4/12 21:47
 * @description:金士頓廠商
 */
public class UsbKingFactory implements UsbSell {
    //定義廠家出廠價格
    @Override
    public float sell() {
        return 85.0f;
    }
}

/**
 * @author :zsy
 * @date :Created 2021/4/12 21:48
 * @description:淘寶代理商
 */
public class Taobao implements UsbSell {
    //明確代理的目標物件
    private UsbKingFactory factory = new UsbKingFactory();
    @Override
    public float sell() {
        float price = factory.sell();
        //增加價格
        price += 15f;
        return price;
    }
}

/**
 * @author :zsy
 * @date :Created 2021/4/12 21:50
 * @description:用戶類
 */
public class User {
    //通過淘寶購買金士頓U盤
    public float buyUsb() {
        Object factory = new UsbKingFactory();
        InvocationHandler handler = new MyHandler(factory);
        UsbSell proxy = (UsbSell) Proxy.newProxyInstance(factory.getClass().getClassLoader(),
                factory.getClass().getInterfaces(), handler);
        return proxy.sell();
    }
}

/**
 * @author :zsy
 * @date :Created 2021/4/12 21:51
 * @description:測驗
 */
public class Test {
    public static void main(String[] args) {
        //創建用戶
        User user = new User();

        //購買U盤
        System.out.println(user.buyUsb());
    }
}
-動態代理
JDK動態代理
  • 在程式執行程序中,使用JDK反射機制,創建代理物件,并動態的指定代理目標類
  • 動態代理是一種創建java物件的能力,讓你不用創建TaoBao類,就能創建代理類物件
  • 實作步驟
    • 創建介面,定義目標類要完成的功能
    • 創建目標類實作介面
    • 創建InvocationHandler介面的實作類,在invoke方法中完成代理類的功能
      • 呼叫目標方法
      • 增強功能
    • 使用Proxy類的靜態方法,創建代理物件, 并把回傳值轉為介面型別,
/**
 * @author :zsy
 * @date :Created 2021/4/12 21:45
 * @description:賣優盤介面
 */
public interface UsbSell {
    float sell();
}


/**
 * @author :zsy
 * @date :Created 2021/4/12 21:47
 * @description:金士頓廠商
 */
public class UsbKingFactory implements UsbSell {
    //定義廠家出廠價格
    @Override
    public float sell() {
        return 85.0f;
    }
}

/**
 * @author :zsy
 * @date :Created 2021/4/12 21:53
 * @description:動態代理類
 */
public class MyHandler implements InvocationHandler {

    //代理類(不確定)
    private Object target = null;

    public MyHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        Object res = method.invoke(target, args);

        //獲取到金士頓廠家的U盤價格
        if (res != null) {
            float price = (float) res;
            System.out.println("從廠家拿到的U盤價格" + price);
            price += 15f;
            res = price;
        }
        return res;
    }
}


/**
 * @author :zsy
 * @date :Created 2021/4/12 21:50
 * @description:用戶類
 */
public class User {
    //通過淘寶購買金士頓U盤
    public float buyUsb() {
        Object factory = new UsbKingFactory();
        InvocationHandler handler = new MyHandler(factory);
        UsbSell proxy = (UsbSell) Proxy.newProxyInstance(factory.getClass().getClassLoader(),
                factory.getClass().getInterfaces(), handler);
        return proxy.sell();
    }
}


/**
 * @author :zsy
 * @date :Created 2021/4/12 21:57
 * @description:測驗動態代理
 */
public class Test {
    public static void main(String[] args) {
        //創建用戶
        User user = new User();
        System.out.println("用戶最終購買U盤的價格" + user.buyUsb());
    }
}
CGILIB動態代理
  • 原理
    • CGILIB通過繼承目標類,創建它的子類,在子類中重寫父類中同名的方法,實作功能的增強
  • 因為CGILIB是繼承,重寫方法,所以要求目標類不能是final的, 方法也不能是final的,cglib的要求目標類比較寬松, 只要能繼承就可以了,

實作Callable介面

步驟

  1. 實作Callable介面,需要回傳值型別
  2. 重寫call方法,需要拋出例外
  3. 創建目標物件
  4. 創建執行服務 ExectorService service = Exectors.newFixedThreadPoll(1);
  5. 提交執行 Future<> r1 = service.submit(thread);
  6. 獲取解過 boolean res1 = r1.get();
  7. 關閉服務 service.shutdownNow();

實體

實作多執行緒同步下載圖片

/**
 * @author :zsy
 * @date :Created 2021/4/12 21:01
 * @description:實作Callable介面
 */
public class TestCallable implements Callable<Boolean> {

    private String url;//網圖地址
    private String name;//保存檔案名

    public TestCallable(String url, String name) {
        this.name = name;
        this.url = url;
    }

    @Override
    public Boolean call() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url, name);
        System.out.println("下載的圖片名稱--->" + name);
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //創建執行緒物件
        TestCallable t1 = new TestCallable("https://upload-images.jianshu.io/upload_images/20068213-872f9137d7826e87.png?imageMogr2/auto-orient/strip|imageView2/1/w/360/h/240", "1.jpg");
        TestCallable t2 = new TestCallable("https://profile.csdnimg.cn/C/2/6/2_qq_45796208", "2.jpg");
        TestCallable t3 = new TestCallable("https://img-home.csdnimg.cn/images/20210412010711.png", "3.jpg");

        //創建執行服務
        ExecutorService service = Executors.newFixedThreadPool(3);

        //執行提交
        Future<Boolean> r1 = service.submit(t1);
        Future<Boolean> r2 = service.submit(t2);
        Future<Boolean> r3 = service.submit(t3);

        //獲取結果
        boolean re1 = r1.get();
        boolean re2 = r2.get();
        boolean re3 = r3.get();

        //關閉服務
        service.shutdownNow();
    }
}

//下載器
class WebDownloader {

    //下載方法
    public void downloader(String url, String name) {
        try {
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO例外,downloader方法出現例外");
        }
    }
}
  1. 有回傳值
  2. 可以拋出例外
  3. 方法不同 run()\ call()

Callable和Runnable關系

FutureTask繼承圖

public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}

因此可以通過Runnable的方式創建實作了Callable介面的執行緒

創建執行緒實作Callable介面

/**
 * @author :zsy
 * @date :Created 2021/4/19 22:03
 * @description:通過FutureTast創建Callable執行緒
 */
public class CallableTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyThread myThread = new MyThread();
        //使用FutureTask對Callable物件進行包裝
        FutureTask task = new FutureTask(myThread);

        new Thread(task, "A").start();
        new Thread(task, "B").start();
        boolean res = (boolean) task.get();
        System.out.println(res);
    }
}

class MyThread implements Callable<Boolean> {

    @Override
    public Boolean call() throws Exception {
        System.out.println("call");
        return true;
    }
}
call
true

啟動兩個執行緒,存在快取,會產生阻塞,結果可能需要等待

Callable和Runnable區別

  • Runnable 介面 run 方法無回傳值;Callable 介面 call 方法有回傳值,是個泛型,和Future、FutureTask配合可以用來獲取異步執行的結果
  • Runnable 介面 run 方法只能拋出運行時例外,且無法捕獲處理;Callable 介面 call 方法允許拋出例外,可以獲取例外資訊

Callalbe介面支持回傳執行結果,需要呼叫FutureTask.get()得到,此方法會阻塞主行程的繼續往下執行,如果不呼叫不會阻塞

小結

  • 繼承Thread類
    • 子類繼承Thread類具備多執行緒能力
    • 啟動執行緒:子類物件.start()
    • 不建議使用:避免OOP單繼承局限性
  • 實作Runnable介面
    • 實作介面Runnable具有多執行緒能力
    • 啟動執行緒:傳入目標物件+Thread物件.start()
    • 推薦使用:避免單繼承的局限性,靈活方便,方便同一個物件被多個執行緒使用

初始并發問題

/**
 * @author :zsy
 * @date :Created 2021/4/12 20:10
 * @description:多執行緒操作同一個物件,模擬買火車票的例子
 */

//發現問題:多個執行緒操作同一個物件情況下,執行緒不安全
public class TestThread4 implements Runnable{

    private int ticketNums = 10;

    @Override
    public void run() {
        while(true) {

            //模擬延時
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (ticketNums <= 0) break;
            System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums-- + "張票");
        }
    }

    public static void main(String[] args) {
        TestThread4 ticket = new TestThread4();

        //統一個物件被多個執行緒使用
        new Thread(ticket, "小明").start();
        new Thread(ticket, "小華").start();
        new Thread(ticket, "黃牛黨").start();
    }
}


小華拿到了第10張票
小明拿到了第9張票
黃牛黨拿到了第10張票
黃牛黨拿到了第8張票
小明拿到了第7張票
小華拿到了第7張票
黃牛黨拿到了第6張票
小華拿到了第6張票
小明拿到了第6張票
黃牛黨拿到了第5張票
小明拿到了第5張票
小華拿到了第5張票
小華拿到了第4張票
黃牛黨拿到了第3張票
小明拿到了第4張票
黃牛黨拿到了第2張票
小明拿到了第1張票

發現問題:多個執行緒操作同一個物件情況下,執行緒不安全,資料紊亂

龜兔賽跑模擬多執行緒

/**
 * @author :zsy
 * @date :Created 2021/4/12 20:32
 * @description:模擬龜兔賽跑
 */
public class Race implements Runnable{

    //勝利者
    private static String winner;

    @Override
    public void run() {
        //模擬跑步
        for (int i = 0; i <= 100; i++) {
            //模擬兔子休息
            //每走10步休息1ms
            if (Thread.currentThread().getName().equals("兔子") && i % 10 == 0) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //判斷是否已經有了勝利者
            if (hasWinner(i)) break;

            System.out.println(Thread.currentThread().getName() + "------->跑了" + i + "步");
        }
    }

    //判斷是否有勝利者
    public boolean hasWinner(int step) {
        if (winner != null) {
            return true;
        }
        if (step >= 100) {
            winner = Thread.currentThread().getName();
            System.out.println("比賽結束,勝利者是" + winner);
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
        Race race = new Race();

        new Thread(race, "兔子").start();
        new Thread(race, "烏龜").start();
    }

}

執行緒狀態

執行緒的五種狀態

執行緒的生命周期

執行緒的狀態轉換

執行緒狀態轉換圖

執行緒的方法

方法說明
setPriority( int newPriority )更改執行緒優先級
static void sleep( long millis )在指定的毫秒數內讓當前正在執行的執行緒休眠
void join()等待該執行緒終止
static void yield()暫停當前正在執行的執行緒物件,并執行其他執行緒
void interrupt中斷執行緒,不用此方法
boolean isAlive()測驗執行緒是否處于活動狀態

stop

  • 不推薦使用JDK提供的stop()、destroy()方法
  • 推薦執行緒自己停下來
  • 建議使用一個標志位進行終止變數,當flag = false,則終止執行緒運行
/**
 * @author :zsy
 * @date :Created 2021/4/13 14:31
 * @description:測驗停止執行緒
 */
public class TestStop implements Runnable{

    //定義標志
    private boolean flag = true;

    @Override
    public void run() {
        int i = 0;
        while(flag) {
            System.out.println(Thread.currentThread().getName() + "run....." + i++);
        }
        if (!flag){
            System.out.println(Thread.currentThread().getName() + "執行緒停止");
        }
    }

    public void stop() {
        this.flag = false;
    }

    public static void main(String[] args) {
        TestStop testStop = new TestStop();
        new Thread(testStop,"T1").start();
        for (int i = 0; i < 300; i++) {
            System.out.println("main------>" + i);
            if (i == 200) {
                testStop.stop();
            }
        }
    }
}

sleep

  • sleep(時間)指定當前執行緒阻塞的毫秒數
  • sleep存在例外InterruptedException
  • sleep時間達到后執行緒進入就緒狀態
  • sleep可以模擬網路延時,倒計時等
  • 每一個物件都有一把鎖,sleep不會釋放鎖

sleep的作用

模擬網路延遲
模擬倒計時
/**
 * @author :zsy
 * @date :Created 2021/4/13 14:55
 * @description:模擬倒計時
 */
public class TestSleep1 {

    //列印當前系統時間
    public static void main(String[] args) {
        /*try {
            tenDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/
        Date nowTime = new Date();
        while(true) {
            nowTime = new Date();
            System.out.println(new SimpleDateFormat("HH:mm:ss SSS").format(nowTime));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }


    //模擬倒計時
    public static void tenDown() throws InterruptedException {
        int num = 10;
        while(true) {
            if (num <= 0) break;
            Thread.sleep(1000);
            System.out.println(num--);
        }
    }
}

yield

  • 禮讓執行緒,讓當前正在執行的執行緒暫停,但不阻塞
  • 將執行緒從運行狀態轉為就緒狀態
  • 讓CPU重新調度,禮讓不一定成功
/**
 * @author :zsy
 * @date :Created 2021/4/13 15:09
 * @description:yiled方法
 */
public class TestYield implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "-----> Start");
        Thread.yield();
        System.out.println(Thread.currentThread().getName() + "------>End");
    }

    public static void main(String[] args) {
        TestYield testYield = new TestYield();
        new Thread(testYield, "A").start();
        new Thread(testYield, "B").start();

    }
}

B-----> Start
A-----> Start
B------>End
A------>End

join

  • Join合并執行緒,待此執行緒執行完成之后,在執行其他執行緒,其他執行緒阻塞
/**
 * @author :zsy
 * @date :Created 2021/4/13 15:21
 * @description:join
 */
public class TestJoin implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println("執行緒vip------>" + i);
        }
    }

    public static void main(String[] args) {
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);
        thread.start();
        for (int i = 0; i < 300; i++) {
            if(i == 0) {
                try {
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("main-------->" + i);
        }
    }
}

state

  • NEW

    尚未啟動的執行緒處于此狀態

  • RUNNABLE

    在Java虛擬機中執行的執行緒處于此狀態

  • BLOCKED

    被阻塞等待監視器鎖定的執行緒處于此狀態,

  • WAITING

    正在等待另一個執行緒執行特定動作的執行緒處于此狀態,

  • TIMED_WAITING

    正在等待另一個執行緒執行動作達到指定等待時間的執行緒處于此狀態,

  • TERMINATED

    已退出的執行緒處于此狀態

/**
 * @author :zsy
 * @date :Created 2021/4/13 15:40
 * @description:測驗執行緒狀態
 */
public class TestState {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("END");
        });
        Thread.State state = thread.getState();
        System.out.println(state);//NEW
        thread.start();

        state = thread.getState();
        System.out.println(state);//RUNNABLE

        while(thread.getState() != Thread.State.TERMINATED) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            state = thread.getState();
            System.out.println(state);
        }
    }
}

NEW
RUNNABLE
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
END
TERMINATED

priority

  • Java提供一個執行緒調度器來監控程式中啟動后進入就緒狀態的所有執行緒,執行緒調度器按照優先級決定應該調度那個執行緒來執行
  • 執行緒優先級用數字表示,范圍從1~10
    • Thread.MIN_PRIORITY = 1
    • Thread.MAX_PRIORITY = 10
    • Thread.NORM_PRIORITY = 5
  • 使用以下方法改變和獲取優先級
    • getPriority()
    • setPriority()

守護執行緒

  • 執行緒分為守護執行緒用戶執行緒

    thread.setDaemon(true); //默認是false 表示是用戶執行緒,正常都是用戶執行緒,,
    
  • 虛擬機必須確保用戶執行緒執行完畢

  • 虛擬機不用等待守護執行緒執行完畢

  • 如:后臺記錄操作日志,監控記憶體,垃圾回收等…

守護執行緒和用戶執行緒有什么區別

守護執行緒和用戶執行緒

  • 用戶 (User) 執行緒:運行在前臺,執行具體的任務,如程式的主執行緒、連接網路的子執行緒等都是用戶執行緒
  • 守護 (Daemon) 執行緒:運行在后臺,為其他前臺執行緒服務,也可以說守護執行緒是 JVM 中非守護執行緒的 “傭人”,一旦所有用戶執行緒都結束運行,守護執行緒會隨 JVM 一起結束作業

比較明顯的區別之一是用戶執行緒結束,JVM 退出,不管這個時候有沒有守護執行緒運行,而守護執行緒不會影響 JVM 的退出,

注意事項:

  1. setDaemon(true)必須在start()方法前執行,否則會拋出 IllegalThreadStateException 例外
  2. 在守護執行緒中產生的新執行緒也是守護執行緒
  3. 不是所有的任務都可以分配給守護執行緒來執行,比如讀寫操作或者計算邏輯
  4. 守護 (Daemon) 執行緒中不能依靠 finally 塊的內容來確保執行關倍訓清理資源的邏輯,因為我們上面也說過了一旦所有用戶執行緒都結束運行,守護執行緒會隨 JVM 一起結束作業,所以守護 (Daemon) 執行緒中的 finally 陳述句塊可能無法被執行,

中斷執行緒

  • 執行緒執行完畢會自行結束
  • 在執行緒處于阻塞,期限等待或無限期等待時,呼叫執行緒的interrupt()會拋出interruptedException例外從而提前終止執行緒
  • 若執行緒沒有處于阻塞狀態,呼叫interrupte()將執行緒標記為中斷,此時呼叫interrupted()判斷執行緒是否處于中斷狀態,來提前終止執行緒
  • 執行緒池呼叫shutdown()等待所有執行緒執行完畢之后關閉

執行緒同步

  • 問題的提出:
    • 多個執行緒執行的不確定性引起執行結果的不穩定性
    • 多個執行緒對賬本的共享,會造成操作的不完整性,會破壞資料,
  • 由于同一個行程的多個執行緒共享同一塊存盤空間,在帶來方便的同時,也帶來了訪問沖突問題,為了保證資料在方法中被訪問時的正確性,在訪問時加入鎖機制synchronized,當一個執行緒獲得物件的排它鎖,獨占資源,其他執行緒必須等待,使用后釋放鎖即可,存在問題:
    • 一個執行緒持有鎖會導致其他所需要此鎖的執行緒掛起
    • 在多個執行緒競爭下,加鎖,釋放鎖會導致比較多的背景關系切換和調度延時,引起性能問題
    • 如果一個優先級高的執行緒等待一個優先級的執行緒釋放鎖,會導致優先級倒置,引起性能問題

執行緒不安全案例

/**
 * @author :zsy
 * @date :Created 2021/4/13 16:51
 * @description:模擬執行緒不安全案例
 */
public class UnsafeBank {
    public static void main(String[] args) {
        Account account = new Account("慈善基金", 100.0f);
        Drawing ming = new Drawing(account, 50.0f, "小明");
        Drawing hua = new Drawing(account,  100.0f, "小華");
        ming.start();
        hua.start();
    }
}

//賬戶類
class Account {
    public String name;
    public float balance;

    public Account(String name, float balance) {
        this.name = name;
        this.balance = balance;
    }


}

//模擬取款
class Drawing extends Thread{
    private Account account;
    private float drawingMoney;

    public Drawing(Account account, float drawingMoney, String name) {
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {
        super.run();
        if (account.balance < drawingMoney) {
            System.out.println("余額不足");
            return;
        }

        //模擬網路延時
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.account.balance = this.account.balance - drawingMoney;
        System.out.println(this.getName() + "取出了" + drawingMoney);
        System.out.println(this.account.name + "余額" + this.account.balance);
    }
}

小明取出了50.0
慈善基金余額-50.0
小華取出了100.0
慈善基金余額-50.0

同步方法

  • synchronized 方法 和 synchronized 塊
同步方法: public  synchronized void method(int args){}
  • synchronized 方法控制“物件”的訪問,每個物件對應一把鎖,每個 synchronized 方法都必須獲得呼叫該方法的物件的鎖才能夠執行,否則執行緒會阻塞,方法一旦執行,就獨占該鎖,知道方法回傳才能釋放鎖,后面被阻塞的執行緒才能獲得這個鎖,繼續執行

缺陷: 若將一個大的方法申明為 synchronized 將會影響效率

同步方法

// synchronized 同步方法,鎖的是this
    private synchronized void buy() throws InterruptedException {
        //判斷是否有票
        if(ticketNums<=0){
            flag = false;
            return;
        }
        //模擬延時
        Thread.sleep(100);
        //買票
        System.out.println(Thread.currentThread().getName()+"拿到"+ticketNums--);
    }

同步塊

  • 同步塊:synchronized(Obj) {}
  • Obj稱為同步監視器
    • Obj可以是任何物件,但是推薦使用共享資源作為同步監視器
    • 同步方法中無需指定同步監視器,因為同步方法的同步監視器就是this,就是這個物件本身,或者是class[反射]
  • 同步監視器的執行程序
    1. 第一個執行緒訪問,鎖定同步監視器,執行其中代碼
    2. 第二個執行緒訪問,發現同步監視器被鎖定,無法訪問
    3. 第一個執行緒訪問完畢,解鎖同步監視器
    4. 第二個執行緒訪問,發現同步監視器沒有鎖,然后鎖定并訪問
//模擬取款
class Drawing extends Thread{
    private Account account;
    private float drawingMoney;

    public Drawing(Account account, float drawingMoney, String name) {
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {
        super.run();
        synchronized (account) {//鎖的是用戶
            if (account.balance < drawingMoney) {
                System.out.println("余額不足");
                return;
            }

            //模擬網路延時
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.account.balance = this.account.balance - drawingMoney;
            System.out.println(this.getName() + "取出了" + drawingMoney);
            System.out.println(this.account.name + "余額" + this.account.balance);
        }
    }
}

死鎖

多個執行緒各自占有一些共享資源,并且互相等待其他執行緒占有的資源才能運行,而導致兩個或者多個執行緒都在等待對方釋放資源,都停止運行的情形,某一個同步塊同時擁有“兩個以上物件的鎖”時,可能會發生死鎖問題

死鎖的案例

/**
 * @author :zsy
 * @date :Created 2021/4/13 19:54
 * @description:死鎖
 */
public class DeadLock {

    public static void main(String[] args) {
        Makeup makeup1 = new Makeup(0, "灰姑娘");
        Makeup makeup2 = new Makeup(1, "白雪公主");
        makeup1.start();
        makeup2.start();
    }
}

class Lipstick {

}

class Mirror {

}

class Makeup extends Thread {

    //只有一份使用static關鍵字
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    int choice;
    String name;

    public Makeup (int choice, String name){
        this.choice = choice;
        this.name = name;
    }

    @Override
    public void run() {
        super.run();
        try {
            makeup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //化妝
    public void makeup() throws InterruptedException {
        if (choice == 0) {
            synchronized (lipstick) {
                System.out.println(this.getName() + "拿到了口紅");
                Thread.sleep(1000);
                synchronized (mirror) {
                    System.out.println(this.getName() + "拿到了鏡子");
                }
            }
        } else {
            synchronized (mirror) {
                System.out.println(this.getName() + "拿到了鏡子");
                Thread.sleep(2000);
                synchronized (lipstick) {
                    System.out.println(this.getName() + "拿到了口紅");
                }
            }
        }
    }
}

分析:造成原因:某一個同步塊同時拿到了兩個物件的鎖

解決方法:同一個同步塊中只允許拿到一個物件的鎖

死鎖

public void makeup() throws InterruptedException {
        if (choice == 0) {
            synchronized (lipstick) {
                System.out.println(this.getName() + "拿到了口紅");
                Thread.sleep(1000);
            }
            synchronized (mirror) {
                System.out.println(this.getName() + "拿到了鏡子");
            }
        } else {
            synchronized (mirror) {
                System.out.println(this.getName() + "拿到了鏡子");
                Thread.sleep(2000);
            }
            synchronized (lipstick) {
                System.out.println(this.getName() + "拿到了口紅");
            }
        }
    }

產生死鎖的條件

  • 互斥條件:一個資源每次只能被一個執行緒使用
  • 請求與保持條件:一個執行緒因請求資源而阻塞時,對已獲得的資源保持不放
  • 不剝奪條件:執行緒已獲得的資源,在未使用完之前,不能強行剝奪
  • 回圈等待條件:若干個執行緒之間形成一種頭尾相接的回圈等待資源關系

上面四個產生死鎖的必要條件,只要想辦法破其中的任意一個或者多個條件就可以避免死鎖的發生,

如何避免執行緒死鎖

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

排查死鎖

jps : JavaVirtual Machine Process Status Tool

jps -l 定位行程號

jps –v :輸出jvm引數

jps –q :僅僅顯示java行程號

jstack 行程號:查看死鎖資訊

  • 從JDK5.0開始,Java提供了更強大的執行緒同步機制-----通過顯示定義同步鎖物件來實作同步,同步鎖使用Lock物件充當
  • java.util.concurrent.locks.Lock介面是控制多個執行緒對共享資源進行訪問的工具,鎖提供了對共享資源的獨占訪問,每次只能有一個執行緒對Lock物件加鎖,執行緒開始訪問共享資源之前應先獲得Lock物件
  • ReentrantLock類實作了Lock,它擁有與synchronized相同的并發性和記憶體語意,在實作執行緒安全的控制中,比較常用的是ReentrantLock,可以顯示加鎖、釋放鎖,

案例

/**
 * @author :zsy
 * @date :Created 2021/4/13 20:23
 * @description:鎖
 */
public class TestLock {
    public static void main(String[] args) {
        TestLock2 testLock2 = new TestLock2();
        new Thread(testLock2).start();
        new Thread(testLock2).start();
        new Thread(testLock2).start();
    }
}

class TestLock2 implements Runnable{
    int ticketNum = 10;

    private final Lock lock = new ReentrantLock();

    @Override
    public void run() {
        lock.lock();
        try {
            while (true) {
                if (ticketNum <= 0) break;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(ticketNum--);
            }
        }finally {
            lock.unlock();
        }

    }
}

AQS

(AbstartQueueSynchronizer)抽象佇列同步器,它的定位是為Java中幾乎所有的鎖和同步器提供一個基礎的框架,AQS是基于FIFO的佇列實作的,并且內部維護了一個狀態變數state,通過原子更新這個狀態變數state既可以實作加鎖解鎖操作,

屬性

volatile int waitStatus;

Node節點另外一個重要的成員是waitStatus,它表示節點等待在佇列中的狀態:

  • CANCELLED:表示執行緒取消了等待,如果取得鎖的程序中發生了一些例外,則可能出現取消的情況,比如等待程序中出現了中斷例外或者出現了timeout,進入該狀態的節點將不會在變化
  • SIGNAL:表示后續節點需要被喚醒,
  • CONDITION:執行緒等待在條件變數佇列中,當其他執行緒呼叫了Condition的signal()方法后,CONDITION狀態的結點將從等待佇列轉移到同步佇列中,等待獲取同步鎖,
  • PROPAGATE:在共享模式下,無條件傳播releaseShared狀態,前繼結點不僅會喚醒其后繼結點,同時也可能會喚醒后繼的后繼結點,
    0: 初始狀態
  • 其中CANCELLED=1,SIGNAL=-1,CONDITION=-2,PROPAGATE=-3 ,在具體的實作中,就可以簡單的通過waitStatus釋放小于等于0,來判斷是否是CANCELLED狀態,

內部結構

在AbstractQueuedSynchronizer內部,有一個佇列,我們把它叫做同步等待佇列,它的作用是保存等待在這個鎖上的執行緒(由于lock()操作引起的等待),此外,為了維護等待在條件變數上的等待執行緒,AbstractQueuedSynchronizer又需要再維護一個條件變數等待佇列,也就是那些由Condition.await()引起阻塞的執行緒,

下面的類圖展示了代碼層面的具體實作:

img

可以看到,無論是同步等待佇列,還是條件變數等待佇列,都使用同一個Node類作為鏈表的節點,對于同步等待佇列,Node中包括鏈表的上一個元素prev,下一個元素next和執行緒物件thread,對于條件變數等待佇列,還使用nextWaiter表示下一個等待在條件變數佇列中的節點,

同步佇列

結構圖

v2-237a75dd55c58af0080e76d1e2025da8_b.jpg

它維護了一個volatile int state(代表共享資源)和一個FIFO執行緒等待佇列(多執行緒爭用資源被阻塞時會進入此佇列),這里volatile是核心關鍵詞,

static final class Node {
    /** Marker to indicate a node is waiting in shared mode */
    static final Node SHARED = new Node();
    /** Marker to indicate a node is waiting in exclusive mode */
    static final Node EXCLUSIVE = null;
    /** waitStatus value to indicate thread has cancelled */
    static final int CANCELLED =  1;
    /** waitStatus value to indicate successor's thread needs unparking */
    static final int SIGNAL    = -1;
    /** waitStatus value to indicate thread is waiting on condition */
    static final int CONDITION = -2;

    static final int PROPAGATE = -3;
    volatile Node next;
    volatile Thread thread;
    Node nextWaiter;
    final boolean isShared() {
        return nextWaiter == SHARED;
    }
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }
    Node() {    // Used to establish initial head or SHARED marker
    }
    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }
    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

條件佇列

由于一個重入鎖可以生成多個條件變數物件,因此,一個重入鎖就可能有多個條件變數等待佇列,實際上,每個條件變數物件內部都維護了一個等待串列,其邏輯結構如下所示:

結構圖

img

參考:Java 并發高頻面試題:聊聊你對 AQS 的理解?

image-20210421215948408

條件等待佇列處理程序

img

Condition物件的signal()通知
signal()通知的時候,是在條件等待佇列中,按照FIFO進行,首先從第一個節點下手:

img

LockSupport

從上面內容我們注意到,執行緒的阻塞和喚醒都使用到了LockSupport的方法,LockSupport是用來創建鎖和其他同步類的基本執行緒阻塞原語,

LockSupport定義了一組以park開頭的方法用來阻塞執行緒,以及unpark來喚醒一個被阻塞的執行緒,

LockSupport提供的方法:

  • public static void park() : 如果沒有可用許可,則掛起當前執行緒
  • public static void unpark(Thread thread):給thread一個可用的許可,讓它得以繼續執行
  • 和陳述句的執行順序無關

這里寫圖片描述

內部都是通過Unsafe實作的

共享鎖與排它鎖

參考:Java并發面試問題,談談你對AQS的理解

排它鎖

lock

具體的加鎖程序

Tip:簡述一下lock的程序,默認為非公平鎖(排他鎖)每次只獲取1,首先嘗試修改state修改成功,獲取當前執行資格,開始執行,修改失敗,acquire(1)去獲取資源,首先嘗試獲取,tryAcquire(),被四個不同的類重寫,嘗試獲取失敗,將當前執行緒封裝成節點,放入同步等待佇列,并且自旋的去嘗試獲取鎖(acquireQueued),這里可以說是整個加鎖的核心了,因為第一個節點是正在運行的執行緒,所以從第二個節點才能去嘗試獲取鎖,獲取鎖的程序中,難免一些執行緒會出現例外中斷,自旋的程序中,洗掉這些已經取消等待的節點,并且將其余節點的waitStatus置為SINGAL等待喚醒,如果回圈程序中,第二個節點獲取到了資源,則跳出回圈,

final void lock() {
    //直接CAS交換state和自己的acquires
    //交換成功,那到當前運行時間片
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        //排他鎖獲取資源
        acquire(1);
}

acquire

請求鎖

public final void acquire(int arg) {
    //嘗試獲得許可, arg為許可的個數,對于重入鎖來說,每次請求1個,
    if (!tryAcquire(arg) &&
    // 如果tryAcquire 失敗,則先使用addWaiter()將當前執行緒加入同步等待佇列
    // 然后繼續嘗試獲得鎖
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
}

進入一步看一下tryAcquire()函式,該函式的作用是嘗試獲得一個許可,對于AbstractQueuedSynchronizer來說,這是一個未實作的抽象函式,

image-20210422140755270

protected boolean tryAcquire(int arg) {
	throw new UnsupportedOperationException();
}

具體實作在子類中,在重入鎖,讀寫鎖,信號量等實作中, 都有各自的實作,

如果tryAcquire()成功,則acquire()直接回傳成功,如果失敗,就用addWaiter()將當前執行緒加入同步等待佇列,

addWaiter

private Node addWaiter(Node mode) {
    //Node中維護當前執行緒物件
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    //將節點加到佇列尾端,這是一個快速的辦法,可能失敗
    //復雜的嘗試,獲取性能
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //如果快速加入失敗,將node加到佇列末尾
    enq(node);
    return node;
}

接著, 對已經在佇列中的執行緒請求鎖,使用acquireQueued()函式,從函式名字上可以看到,其引數node,必須是一個已經在佇列中等待的節點,它的功能就是為已經在佇列中的Node請求一個許可,

這個函式大家要好好看看,因為無論是普通的lock()方法,還是條件變數的await()都會使用這個方法,

acquireQueued

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;//標記是否成功拿到資源
    try {
        boolean interrupted = false;//標記等待程序中是否被中斷過
        
        //又是一個“自旋”!
        for (;;) {
            final Node p = node.predecessor();//拿到前驅
            //如果前驅是head,即該結點已成老二,那么便有資格去嘗試獲取資源(可能是老大釋放完資源喚醒自己的,當然也可能被interrupt了),
            if (p == head && tryAcquire(arg)) {
                setHead(node);//拿到資源后,將head指向該結點,所以head所指的標桿結點,就是當前獲取到資源的那個結點或null,
                p.next = null; // setHead中node.prev已置為null,此處再將head.next置為null,就是為了方便GC回收以前的head結點,也就意味著之前拿完資源的結點出隊了!
                failed = false; // 成功獲取資源
                return interrupted;//回傳等待程序中是否被中斷過
            }
            
            //如果自己可以休息了,就通過park()進入waiting狀態,直到被unpark(),如果不可中斷的情況下被中斷了,那么會從park()中醒過來,發現拿不到資源,從而繼續進入park()等待,
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;//如果等待程序中被中斷過,哪怕只有那么一次,就將interrupted標記為true
        }
    } finally {
        if (failed) // 如果等待程序中沒有成功獲取資源(如timeout,或者可中斷的情況下被中斷了),那么取消結點在佇列中的等待,
            cancelAcquire(node);
    }
}

CAS自旋volatile變數

什么是自旋

當一個執行緒拿不到鎖的時候,可以不放棄CPU,空轉,不斷重試,也就是所謂的自旋,對于多CPU或者多核,自旋就很有用了,因為沒有執行緒切換的開銷,

AtomicInteger類的getAndIncrement()的源代碼:

public final int getAndIncrement() {
    for (;;) {
		int current = get();  // 取得AtomicInteger里存盤的數值
		int next = current + 1;  // 加1
		if (compareAndSet(current, next))   // 呼叫compareAndSet執行原子更新操作
			return current;
	}
}
  • compareAndSet()方法首先判斷當前值是否等于current
  • 如果當前值等于current,說明AtomicInteger的值沒有被修改
  • 如果不等于,說明被修改,這時會再次進入回圈進行等待

compareAndSwapInt 基于的是CPU 的 CAS指令來實作的,所以基于 CAS 的操作可認為是無阻塞的,一個執行緒的失敗或掛起不會引起其它執行緒也失敗或掛起,并且由于 CAS 操作是 CPU 原語,所以性能比較好,

他所利用的是基于沖突檢測的樂觀并發策略, 可以想象,這種樂觀在執行緒數目非常多的情況下,失敗的概率會指數型增加,

predecessor拿到佇列的頭節點

final Node predecessor() throws NullPointerException {
    Node p = prev;
    if (p == null)
        throw new NullPointerException();
    else
        return p;
}

shouldParkAfterFailedAcquire

此方法主要用于檢查狀態,看看自己是否真的可以去休息了,萬一佇列前邊的執行緒都放棄了,自己直接可以執行

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;//拿到前驅的狀態
    if (ws == Node.SIGNAL)
        //如果已經告訴前驅拿完號后通知自己一下,那就可以安心休息了
        return true;
    if (ws > 0) {
        /*
         * 如果前驅放棄了,那就一直往前找,直到找到最近一個正常等待的狀態,并排在它的后邊,
         * 注意:那些放棄的結點,由于被自己“加塞”到它們前邊,它們相當于形成一個無參考鏈,稍后就會被保安大叔趕走了(GC回收)!
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
         //如果前驅正常,那就把前驅的狀態設定成SIGNAL,告訴它拿完號后通知自己一下,有可能失敗,人家說不定剛剛釋放完呢!
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}      

parkAndCheckInterrupt

如果執行緒找好安全休息點后,那就可以安心去休息了,此方法就是讓執行緒去休息,真正進入等待狀態,

private final boolean parkAndCheckInterrupt() {
    //執行緒會停在這里
     LockSupport.park(this);//呼叫park()使執行緒進入waiting狀態
     return Thread.interrupted();//如果被喚醒,查看自己是不是被中斷的,
 }

park()會讓當前執行緒進入waiting狀態,在此狀態下,有兩種途徑可以喚醒該執行緒:1)被unpark();2)被interrupt(),需要注意的是,Thread.interrupted()會清除當前執行緒的中斷標記位,

總結

acquireQueued的具體流程
  1. 節點進入隊尾,檢查狀態,找到安全的休息點;
  2. 呼叫park()進入wait,等待unpark()或interrupt()喚醒
  3. 被喚醒后,看自己是不是有資格拿到資源,如果拿到,head指向當前節點,并回傳從入隊到拿到號整個程序是否被中斷過,如果沒有拿到,繼續流程1
acquire的具體流程
  1. 呼叫自定義同步器的tryAcquire()獲取資源,如果成功則直接回傳
  2. 沒成功,則addWaiter()將該執行緒加入等待佇列的尾部,并標記為獨占模式
  3. acquireQueued()使執行緒在等待佇列中休息,有機會時(輪到自己,會被unpark())會去嘗試獲取資源,獲取到資源后才回傳,如果在整個等待程序中被中斷過,則回傳true,否則回傳false,
  4. 如果執行緒在等待程序中被中斷過,他是不回應的,只是在獲取資源后再進行自我中斷selfInterrupt(),將中斷補上

img

release(int)

此方法是獨占模式下執行緒釋放共享資源的頂層入口,他會釋放指定量的資源,如果徹底釋放了(state = 0),他會喚醒等待佇列里的其他執行緒來獲取資源,這也正是unlock()的語意

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;//找到頭結點
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);//喚醒等待佇列里的下一個執行緒
        return true;
    }
    return false;
} 

邏輯并不復雜,它呼叫tryRelease()來釋放資源,有一點需要注意的是,它是根據tryRelease()的回傳值來判斷該執行緒是否已經完成釋放掉資源了!所以自定義同步器在設計tryRelease()的時候要明確這一點!!

tryRelease(int)

此方法嘗試去釋放指定量的資源,如果已經徹底釋放資源(state=0),要回傳true,否則回傳false,下面是tryRelease()的原始碼:

protected boolean tryRelease(int arg) {
     throw new UnsupportedOperationException();
}

unparkSuccessor(Node)

private void unparkSuccessor(Node node) {
    //這里,node一般為當前執行緒所在的結點,
    int ws = node.waitStatus;
    if (ws < 0)//置零當前執行緒所在的結點狀態,允許失敗,
        compareAndSetWaitStatus(node, ws, 0);
 
    Node s = node.next;//找到下一個需要喚醒的結點s
    if (s == null || s.waitStatus > 0) {//如果為慷訓已取消
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev) // 從后向前找,
            if (t.waitStatus <= 0)//從這里可以看出,<=0的結點,都是還有效的結點,
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);//喚醒
}

用unpark()喚醒等待佇列中最前面的那個未放棄的執行緒再和acquireQueued()聯系起來,s被喚醒后,進入if (p == head && tryAcquire(arg))的判斷(即使p!=head也沒關系,它會再進入shouldParkAfterFailedAcquire()尋找一個安全點,這里既然s已經是等待佇列中最前邊的那個未放棄執行緒了,那么通過shouldParkAfterFailedAcquire()的調整,s也必然會跑到head的next結點,下一次自旋p==head就成立啦),然后s把自己設定成head標桿結點,表示自己已經獲取到資源了,acquire()也回傳了

擴展問題

如果獲取鎖的執行緒在release時例外了,沒有unpark佇列中的其他結點,這時佇列中的其他結點會怎么辦?是不是沒法再被喚醒了?

答案是YES這時,佇列中等待鎖的執行緒將永遠處于park狀態,無法再被喚醒!!!但是我們再回頭想想,獲取鎖的執行緒在什么情形下會release拋出例外呢??

  1. 執行緒突然死掉了?可以通過thread.stop來停止執行緒的執行,但該函式的執行條件要嚴苛的多,而且函式注明是非執行緒安全的,已經標明Deprecated;
  2. 執行緒被interupt了?執行緒在運行態是不回應中斷的,所以也不會拋出例外;
  3. release代碼有bug,拋出例外了?目前來看,Doug Lea的release方法還是比較健壯的,沒有看出能引發例外的情形(如果有,恐怕早被用戶吐槽了),除非自己寫的tryRelease()有bug,那就沒啥說的,自己寫的bug只能自己含著淚去承受了,

共享鎖

ReentrantLock.ReadLock、Semaohore、CountDownLatch

與排他鎖相比,共享鎖的實作略微復雜一點,這也很好理解,因為排他鎖的場景很簡單,單進單出,而共享鎖就不一樣了,可能是N進M出,處理起來要麻煩一些,但是,他們的核心思想還是一致的,共享鎖的幾個典型應用有:信號量,讀寫鎖中的寫鎖,

共享鎖執行原理

img

acquireShared

public final void acquireShared(int arg) {
    //tryAcquireShared需要在子類中實作
    //他表示嘗試獲取arg個共享許可,如果tryAcquireShared回傳負數,表示失敗
    //回傳0表示成功,但是沒有多余的許可
    if (tryAcquireShared(arg) < 0)
        //申請失敗,進入通同步等待佇列
        doAcquireShared(arg);
}

doAcquireShared

private void doAcquireShared(int arg) {
    //加入同步等待佇列,指定為SHARED型別
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            //只有第二個節點有資格申請,因為是一個FIFO的佇列,而第一個節點已經獲得了許可
            //因此第二個節點就是第一個需要申請而的節點
            if (p == head) {
                //嘗試申請許可
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    //申請成功,把自己設定為頭部
                    //根據條件,判斷是否需要喚醒后續執行緒
                    //如果條件允許,就會嘗試傳播這個喚醒到后續節點
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            //沒有獲取成功,只能park等待
            //將來如果被喚醒,從這里開始執行,又會回到上面的tryAcquireShared
            //和setHeadAndPropagate,去嘗試共享許可,并且傳播
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

其實流程根acquireQueued并沒有太大區別,只不過這里將補中斷的selfInterrupt()放到doAcquireShared()里了,而獨占模式是放到acquireQueued()之外,其實都一樣,

跟獨占模式比,還有一點需要注意的是,這里只有執行緒是head.next時(“老二”),才會去嘗試獲取資源,有剩余的話還會喚醒之后的隊友,

那么問題就來了,假如老大用完后釋放了5個資源,而老二需要6個,老三需要1個,老四需要2個,老大先喚醒老二,老二一看資源不夠,他是把資源讓給老三呢,還是不讓?答案是否定的!老二會繼續park()等待其他執行緒釋放資源,也更不會去喚醒老三和老四了,(保證公平,但降低了并發),

setHeadAndPropagate

//設定標志位,喚醒后續節點
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    //這里取得了許可成功,將當前節點放在頭部
    setHead(node);
    //propagate>0表示后續節點的許可申請釋放可以成功
    //由propagate和waitStatus來判斷能否喚醒后續執行緒,如果只有propagate來判斷
    //在并發環境中,可能出現執行緒不能被喚醒的情況
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            //喚醒下一個執行緒,設定傳播狀態
            //被喚醒的執行緒 又會嘗試tryAcquireShared 和 setHeadAndPropagate
            doReleaseShared();
    }
}

此方法在setHead()的基礎上多了一步,就是自己蘇醒的同時,如果條件符合(比如還有剩余資源),還會去喚醒后繼結點,畢竟是共享模式!

總結

acquiredShared()流程

  1. tryAcquiredShared()嘗試成功獲取資源,成功則直接回傳
  2. 失敗通過doAcquireShared()進入等待佇列,直到被unpark()/interrupt()并成功獲取到資源才回傳,整個等待程序也是忽略中斷的,

releaseShared

此方法是共享模式下執行緒釋放共享資源的頂層入口,它會釋放指定量的資源,如果成功釋放且允許喚醒等待執行緒,它會喚醒等待佇列里的其他執行緒來獲取資源,

public final boolean releaseShared(int arg) {
    //嘗試釋放許可,需要在子類中實作
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

釋放掉資源后,喚醒后繼,跟獨占模式下的release()相似,但有一點稍微需要注意:獨占模式下的tryRelease()在完全釋放掉資源(state=0)后,才會回傳true去喚醒其他執行緒,這主要是基于獨占下可重入的考量;而共享模式下的releaseShared()則沒有這種要求,共享模式實質就是控制一定量的執行緒并發執行,那么擁有資源的執行緒在釋放掉部分資源時就可以喚醒后繼等待結點,

例如,資源總量是13,A(5)和B(7)分別獲取到資源并發運行,C(4)來時只剩1個資源就需要等待,

A在運行程序中釋放掉2個資源量,然后tryReleaseShared(2)回傳true喚醒C,C一看只有3個仍不夠繼續等待;隨后B又釋放2個,tryReleaseShared(2)回傳true喚醒C,C一看有5個夠自己用了,然后C就可以跟A和B一起運行,

而ReentrantReadWriteLock讀鎖的tryReleaseShared()只有在完全釋放掉資源(state=0)才回傳true,所以自定義同步器可以根據需要決定tryReleaseShared()的回傳值,

doReleaseShared

//喚醒下一個執行緒,或者設定傳播狀態
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                //如果需要喚醒后續執行緒,那么就喚醒,同時設定狀態為0
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            //設定PROPAGATE狀態,保證執行緒喚醒可以傳播下去
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        //如果被打擾,重新再來一次
        //否則直接退出
        if (h == head)                   // loop if head changed
            break;
    }
}

Mutex(互斥鎖)

Mutex是一個不可重入的互斥鎖實作,鎖資源(AQS里的state)只有兩種狀態:0表示未鎖定,1表示鎖定,下邊是Mutex的核心原始碼:

class Mutex implements Lock, java.io.Serializable {
    // 自定義同步器
    private static class Sync extends AbstractQueuedSynchronizer {
        // 判斷是否鎖定狀態
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
 
        // 嘗試獲取資源,立即回傳,成功則回傳true,否則false,
        public boolean tryAcquire(int acquires) {
            assert acquires == 1; // 這里限定只能為1個量
            if (compareAndSetState(0, 1)) {//state為0才設定為1,不可重入!
                setExclusiveOwnerThread(Thread.currentThread());//設定為當前執行緒獨占資源
                return true;
            }
            return false;
        }
 
        // 嘗試釋放資源,立即回傳,成功則為true,否則false,
        protected boolean tryRelease(int releases) {
            assert releases == 1; // 限定為1個量
            if (getState() == 0)//既然來釋放,那肯定就是已占有狀態了,只是為了保險,多層判斷!
                throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);//釋放資源,放棄占有狀態
            return true;
        }
    }
 
    // 真正同步類的實作都依賴繼承于AQS的自定義同步器!
    private final Sync sync = new Sync();
 
    //lock<-->acquire,兩者語意一樣:獲取資源,即便等待,直到成功才回傳,
    public void lock() {
        sync.acquire(1);
    }
 
    //tryLock<-->tryAcquire,兩者語意一樣:嘗試獲取資源,要求立即回傳,成功則為true,失敗則為false,
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }
 
    //unlock<-->release,兩者語文一樣:釋放資源,
    public void unlock() {
        sync.release(1);
    }
 
    //鎖是否占有狀態
    public boolean isLocked() {
        return sync.isHeldExclusively();
    }
}

同步類在實作時一般都將自定義同步器(sync)定義為內部類,供自己使用;而同步類自己(Mutex)則實作某個介面,對外服務,當然,介面的實作要直接依賴sync,它們在語意上也存在某種對應關系!!而sync只用實作資源state的獲取-釋放方式tryAcquire-tryRelelase,至于執行緒的排隊、等待、喚醒等,上層的AQS都已經實作好了,我們不用關心,

除了Mutex,ReentrantLock/CountDownLatch/Semphore這些同步類的實作方式都差不多,不同的地方就在獲取-釋放資源的方式tryAcquire-tryRelelase,掌握了這點,AQS的核心便被攻破了!

公平鎖和非公平鎖

abstract static class Sync extends AbstractQueuedSynchronizer 

Sync呢又分別有兩個子類:FairSync和NofairSync

img

公平鎖和非公平鎖在代碼層面怎么體現呢

image-20210421090136839

非公平鎖圖解

A執行緒準備進去獲取鎖,首先判斷了一下state狀態,發現是0,所以可以CAS成功,并且修改了當前持有鎖的執行緒為自己,

img

這個時候B執行緒也過來了,也是一上來先去判斷了一下state狀態,發現是1,那就CAS失敗了,真晦氣,只能乖乖去等待佇列,等著喚醒了,先去睡一覺吧,

img

A持有久了,也有點膩了,準備釋放掉鎖,給別的仔一個機會,所以改了state狀態,抹掉了持有鎖執行緒的痕跡,準備去叫醒B,

img

這個時候有個帶綠帽子的仔C過來了,發現state怎么是0啊,果斷CAS修改為1,還修改了當前持有鎖的執行緒為自己,

B執行緒被A叫醒準備去獲取鎖,發現state居然是1,CAS就失敗了,只能失落的繼續回去等待佇列,路線還不忘罵A渣男,怎么騙自己,欺騙我的感情,

img

非公平鎖原始碼

nonfairTryAcquire

final boolean nonfairTryAcquire(int acquires) {
    //獲取當前執行緒
    final Thread current = Thread.currentThread();
    //獲取當前的狀態
    int c = getState();
    //0代表空閑
    if (c == 0) {
        //直接CAS交換state和自己的acquires
        if (compareAndSetState(0, acquires)) {
            //設定當前資源的擁有者為當前執行緒(排它鎖)
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //不為0代表當前有執行緒正在執行
    //而且正在執行的執行緒是當前執行緒
    else if (current == getExclusiveOwnerThread()) {
        //重新設定state的值,不能為負數
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded")
        setState(nextc);
        return true;
    }
    return false;
}

state:當前執行緒的狀態

/**
* The synchronization state.
*/
private volatile int state;

tryRelease

protected final boolean tryRelease(int releases) {
    //修改state狀態
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        //設定當前資源擁有者為空
        setExclusiveOwnerThread(null);
    }
    //修改state
    setState(c);
    return free;
}

公平鎖圖解

線A現在想要獲得鎖,先去判斷下state,發現也是0,去看了看佇列,自己居然是第一位,果斷修改了持有執行緒為自己,

img

執行緒b過來了,去判斷一下state,嗯哼?居然是state=1,那cas就失敗了呀,所以只能乖乖去排隊了,

未命名檔案 (https://tva1.sinaimg.cn/large/00831rSTly1gcxaojuen2j30oa0jxgmh.jpg)

執行緒A暖男來了,持有沒多久就釋放了,改掉了所有的狀態就去喚醒執行緒B了,這個時候執行緒C進來了,但是他先判斷了下state發現是0,以為有戲,然后去看了看佇列,發現前面有人了,作為新時代的良好市民,果斷排隊去了,

img

執行緒B得到A的召喚,去判斷state了,發現值為0,自己也是佇列的第一位,那很香呀,可以得到了,

img

公平鎖原始碼

tryAcquire

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        //判斷當前等待佇列是否為空
        //如果為空并且交換state和acquires成功
        //占有當前資源
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }	
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

hasQueuedPredecessors

代碼的大概意思也是判斷當前的執行緒是不是位于同步佇列的首位,是就是回傳true,否就回傳false,

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

非公平鎖和公平鎖的區別

區別公平鎖非公平鎖
概念多個執行緒按照申請鎖額順序去獲取鎖,執行緒會直接進入佇列,永遠都是佇列的第一位才能獲得鎖多個執行緒去獲取鎖的時候,會直接去獲取鎖,獲取不到,在進入等待佇列,如果能獲取,就直接獲取了
優點所有執行緒都能得到資源,不會餓死在佇列中可以減少CPU去喚醒執行緒的開銷,整體的吞吐量會高點,CPU不必喚醒所有執行緒,會減少喚起執行緒數量
缺點吞吐量會下降很多,佇列除了第一個執行緒,其他的執行緒都會阻塞,等待CPU喚醒,CPU喚醒阻塞執行緒的開銷會很大這樣可能導致佇列中間的執行緒一直獲取不到鎖,或者長時間獲取不到鎖,導致餓死

樂觀鎖和悲觀鎖

樂觀鎖

CAS(Compare And Swap 比較并且替換)是樂觀鎖的一種體現,是一種輕量級鎖,JUC工具類的實作就是基于CAS

CAS怎么實作執行緒安全

執行緒在讀取資料時不進行加鎖,在準備寫會資料時,先查詢原值,操作的時候比較原值是否修改,若未被其他執行緒修改則寫回,若已被修改,則重新執行讀取流程

比較 + 更新 整體是一個原子操作,當然這個流程還是有問題的

img

樂觀鎖的問題

ABA問題

img

看到問題所在沒,我說一下順序:

執行緒1讀取了資料A
執行緒2讀取了資料A
執行緒2通過CAS比較,發現值是A沒錯,可以把資料A改成資料B
執行緒3讀取了資料B
執行緒3通過CAS比較,發現資料是B沒錯,可以把資料B改成了資料A
執行緒1通過CAS比較,發現資料還是A沒變,就寫成了自己要改的值
懂了么,我盡可能的幼兒園化了,在這個程序中任何執行緒都沒做錯什么,但是值被改變了,執行緒1卻沒有辦法發現,其實這樣的情況出現對結果本身是沒有什么影響的,但是我們還是要防范,

加標志、時間戳,值可能相同,但是版本號一定不一樣

回圈時間長開銷大的問題

是因為CAS操作長時間不成功的話,會導致一直自旋,相當于死回圈了,CPU的壓力會很大,

只能保證一個共享變數的原子操作

CAS操作單個共享變數的時候可以保證原子的操作,多個變數就不行了,JDK 5之后 AtomicReference可以用來保證物件之間的原子性,就可以把多個物件放入CAS中操作,

那我就拿AtomicInteger舉例,他的自增函式incrementAndGet()就是這樣實作的,其中就有大量回圈判斷的程序,直到符合條件才成功,

悲觀鎖

synchronized,代表這個方法加鎖,相當于不管哪一個執行緒(例如執行緒A),運行到這個方法時,都要檢查有沒有其它執行緒B(或者C、 D等)正在用這個方法(或者該類的其他同步方法),有的話要等正在使用synchronized方法的執行緒B(或者C 、D)運行完這個方法后再運行此執行緒A,沒有的話,鎖定呼叫者,然后直接運行,

synchronized對物件進行加鎖

在JVM中物件在記憶體中分為三塊區域:物件頭(Header)、實體資料(InstanceDate)和對齊填充(Padding),

物件頭

以Hotspot虛擬機為例,Hotspot的物件頭主要包括兩部分資料:Mark Word(標記欄位)、Kass Pointer(型別指標)

  • Mark Word:默認存盤物件的HashCode,分代年齡和鎖標志位資訊,它會根據物件的狀態復用自己的存盤空間,也就是說在運行期間Mark Word里存盤的資料會隨著鎖標志位的變化而變化,
  • Klass Point:物件指向它的類元資料的指標,虛擬機通過這個指標來確定這個物件是哪個類的實體,

你可以看到在物件頭中保存了鎖標志位和指向 monitor 物件的起始地址,如下圖所示,右側就是物件對應的 Monitor 物件,

img

當 Monitor 被某個執行緒持有后,就會處于鎖定狀態,如圖中的 Owner 部分,會指向持有 Monitor 物件的執行緒,

另外 Monitor 中還有兩個佇列分別是EntryList和WaitList,主要是用來存放進入及等待獲取鎖的執行緒,

如果執行緒進入,則得到當前物件鎖,那么別的執行緒在該類所有物件上的任何操作都不能進行,

synchronized對方法進行加鎖

在位元組碼中是通過方法的 ACC_SYNCHRONIZED 標志來實作的,

synchronized 應用在同步塊上

每個物件都會與一個monitor相關聯,當某個monitor被擁有之后就會被鎖住,當執行緒執行到monitorenter指令時,就會去嘗試獲得對應的monitor,

小結

同步方法和同步代碼塊底層都是通過monitor來實作同步的,

兩者的區別:同步方式是通過方法中的access_flags中設定ACC_SYNCHRONIZED標志來實作,同步代碼塊是通過monitorenter和monitorexit來實作,

我們知道了每個物件都與一個monitor相關聯,而monitor可以被執行緒擁有或釋放,

CAS和synchronized的使用場景

  • 對于資源競爭較少(執行緒沖突較輕)的情況下,使用synchronized同步鎖進行執行緒阻塞和喚醒切換以及用戶內核態間的切換操作額外浪費消耗CPU資源,而CAS基于硬體實作,不需要進入內核,不需要切換執行緒,操作自旋的幾率比較小,因此可以獲得更高的性能
  • 對于資源競爭嚴重(執行緒沖突嚴重)的情況,CAS自旋的概率比較大,從而浪費更多的CPU資源,效率低于synchronized

可重入鎖和不可重組鎖

可重入鎖

廣義上的可重入鎖指的是可重復可遞回呼叫的鎖,在外層使用鎖之后,在內層仍然可以使用,并且不發生死鎖(前提得是同一個物件或者class),這樣的鎖就叫做可重入鎖,ReentrantLock和synchronized都是可重入鎖

不可重組鎖

不可重入鎖,與可重入鎖相反,不可遞回呼叫,遞回呼叫就發生死鎖,看到一個經典的講解,使用自旋鎖來模擬一個不可重入鎖,代碼如下

import java.util.concurrent.atomic.AtomicReference;
 
public class UnreentrantLock {
 
   private AtomicReference<Thread> owner = new AtomicReference<Thread>();
 
   public void lock() {
       Thread current = Thread.currentThread();
       //這句是很經典的“自旋”語法,AtomicInteger中也有
       for (;;) {
           if (!owner.compareAndSet(null, current)) {
               return;
           }
       }
   }
 
   public void unlock() {
       Thread current = Thread.currentThread();
       owner.compareAndSet(current, null);
   }
}
/**
 * @author :zsy
 * @date :Created 2021/4/24 23:45
 * @description:測驗鎖
 */
public class Test {
    private static final UnReentrantLock lock = new UnReentrantLock();

    public static void main(String[] args) {
        new Thread(() -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "獲取到鎖");
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName() + "釋放鎖");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "A").start();

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "獲取到鎖");
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + "釋放鎖");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "B").start();
    }
}
A獲取到鎖
A釋放鎖
B獲取到鎖
B釋放鎖

使用原子參考來存放執行緒,同一執行緒兩次呼叫lock()方法,如果不執行unlock()釋放鎖的話,第二次呼叫自旋的時候就會產生死鎖,這個鎖就不是可重入的,而實際上同一個執行緒不必每次都去釋放鎖再來獲取鎖,這樣的調度切換是很耗資源的,

import java.util.concurrent.atomic.AtomicReference;
 
public class UnreentrantLock {
 
   private AtomicReference<Thread> owner = new AtomicReference<Thread>();
   private int state = 0;
 
   public void lock() {
       Thread current = Thread.currentThread();
       if (current == owner.get()) {
           state++;
           return;
       }
       //這句是很經典的“自旋”式語法,AtomicInteger中也有
       for (;;) {
           if (!owner.compareAndSet(null, current)) {
               return;
           }
       }
   }
 
   public void unlock() {
       Thread current = Thread.currentThread();
       if (current == owner.get()) {
           if (state != 0) {
               state--;
           } else {
               owner.compareAndSet(current, null);
           }
       }
   }
}

在執行每次操作之前,判斷當前鎖持有者是否是當前物件,采用state計數,不用每次去釋放鎖,

transient

  • ? transient底層實作原理
    • Java的serialization提供了一個非常棒的存盤物件狀態的機制,說白了serialization就是把物件的狀態存盤到硬碟上 去,等需要的時候就可以再把它讀出來使用,有些時候像銀行卡號這些欄位是不希望在網路上傳輸的,transient的作用就是把這個欄位的生命周期僅存于呼叫者的記憶體中而不會寫到磁盤里持久化,意思是transient修飾的age欄位,他的生命周期僅僅在記憶體中,不會被寫到磁盤中,

競態條件和資料競爭

  • 競態條件:某個計算的正確性取決于多個執行緒的交替執行時序時,那么就會發生競態條件,(正確的結果取決于運氣)最常見的競態條件的型別就是"先檢查后執行",大多數競態條件的本質就是基于一種可能失效的觀察結果來做出判斷或者執行某個計算
  • 資料競爭:當一個執行緒寫入一個變數而另一個執行緒接下來讀取這個變數,或者讀取一個之前由另一個執行緒寫入的變數的時,并且這兩個執行緒之間沒有使用同步,那么就可能出現資料競爭(資料訪問的錯誤)
  • 并非所有的競態條件都是資料競爭,同樣并非所有的資料競爭都是競態條件

執行緒協作

執行緒通信

  • 應用場景:生產者和消費者問題
    • 對于生產者,沒有生產產品之前,要通知消費者等待,而生產了產品之后,又需要馬上通知消費者消費
    • 對于消費者,在消費之前,要通知消費者已經結束消費,需要產生新的產品以供消費
    • 在生產者消費者問題中,僅有synchronized是不夠的的
      • synchronized可阻止并發更新同一個共享資源,實作了同步
      • synchronized不能用來實作不同執行緒之間的訊息傳遞(通信)
  • 這是一個執行緒同步問題,生產者和消費者共享同一個資源,并且生產者和消費者之間相互依賴,互為條件,
  • Java提供了幾個方法解決執行緒之間的通訊問題
方法名作用
wait()表示執行緒一直等待,直到其他執行緒通知,與sleep不同,會釋放鎖
wait(long timeout)指定等待的毫秒數
notify()喚醒一個處于等待狀態的執行緒
notifyAll()喚醒同一個物件上所有呼叫wait()方法的執行緒,優先級別高的執行緒優先調度
  • 注意:均為Object類的方法,都只能在同步方法或者同步代碼塊中使用,三個方法的呼叫者必須是同步代碼塊或同步方法中的同步監視器,否則會拋出例外’IIIegalMonitorStateException‘

wait()和sleep()方法的區別

  • 相同點:兩者都可以使當前執行緒進入阻塞狀態;
  • 不同點:
    • 兩者所屬的類不同,sleep()是Thread類中的方法,wait()是Object類中的方法;
    • 兩者的使用范圍不一樣,sleep()可以使用在任何有需要的場景下,而wait()方法只能使用在同步代碼塊和同步方法中;
    • 是否釋放鎖:sleep()方法不會釋放鎖,而wait()方法會釋放鎖,
    • 用法不同:使用sleep()方法,睡眠時間到了以后執行緒會自動蘇醒;而使用wait()方法必須通過呼叫物件的notify()或notifyAll()方法喚醒;
    • 作用不同:sleep()方法是用來暫停執行緒的,而wait()方法是用來進行執行緒間互動的,

解決方法

方式1:生產者消費者(管程法)

并發協作模型“生產者/消費者模式” ——>管程法

  • 生產者:負責生產資料的模塊(可能是方法、物件、執行緒、行程)
  • 消費者:負責處理資料的模塊(可能是方法、物件、執行緒、行程)
  • 緩沖區:消費者不能直接使用生產者資料,他們之間有緩沖區

【生產者將生產好的資料放入緩沖區,消費者從緩沖區拿出資料】

/**
 * @author :zsy
 * @date :Created 2021/4/13 22:02
 * @description:生產者消費者模式
 */
public class TestPc {

    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        Thread p = new Productor(container);
        Thread c = new Consumer(container);
        p.start();
        c.start();
    }
}

//生產者
class Productor extends Thread{
    SynContainer container;

    public Productor(SynContainer synContainer) {
        this.container = synContainer;
    }

    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 100; i++) {
            container.push(new Product(i));
            System.out.println("生產了------>" + i + "件產品");
        }
    }
}

//消費者
class Consumer extends Thread {
    SynContainer container;

    public Consumer(SynContainer synContainer) {
        this.container = synContainer;
    }

    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 100; i++) {
            System.out.println("消費了" + container.pop().id + "件產品");
        }
    }
}

//產品
class Product {
    public int id;

    public Product(int id) {
        this.id = id;
    }
}

//緩沖區
class SynContainer {
    Product[] products = new Product[10];
    int count = 0;

    public synchronized void push(Product product) {
        if (count == products.length) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        products[count] = product;
        count++;
        //通知消費者可以消費了
        this.notifyAll();
        
    }

    public synchronized Product pop() {
        if (count == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        count--;
        Product product = products[count];
        //通知生產者開始生產
        this.notifyAll();

        return product;
    }
}

方式2:(信號燈法)

并發協作模型“生產者/消費者模式” ——>信號燈法

/**
 * @author :zsy
 * @date :Created 2021/4/13 23:37
 * @description:信號燈法
 */
public class TestPc2 {

    public static void main(String[] args) {
        Tv tv = new Tv();
        Player player = new Player(tv);
        Watcher watcher = new Watcher(tv);
        player.start();
        watcher.start();
    }
}

//演員類
class Player extends Thread {
    Tv tv;

    public Player(Tv tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                tv.play("王牌對王牌");
            } else {
                tv.play("青春環游記");
            }
        }
    }
}

//觀眾類
class Watcher extends Thread{
    Tv tv;
    public Watcher(Tv tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}

//節目
class Tv {
    String program;
    /**
     * 演員表演節目 T
     * 觀眾觀看節目 F
     */
    boolean flag = true;

    public synchronized void play(String program) {
        if (!flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.program = program;
        System.out.println("演員表演了----->" + program + "節目");
        this.flag = ! this.flag;
        this.notifyAll();
    }

    public synchronized void watch() {
        if(flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("觀看了" + program + "節目");
        this.flag = !this.flag;
        this.notifyAll();
    }
}

執行緒池

  • 背景:經常創建和銷毀、使用量特別大的資源,比如并發情況下的執行緒,對性能影響很大
  • 思路:提前創建好多個執行緒,放入執行緒池中,使用時直接獲取,使用完放回池中,可以避免頻繁的創建和銷毀、實作重復利用
  • 好處
    • 提高回應速度(減少了創建新執行緒的時間)
    • 降低資源消耗(重復利用執行緒池中的執行緒,不需要每次都創建)
    • 便于執行緒管理
      • corePoolSize:核心池大小
      • maximumPoolSize:最大執行緒數
      • keepAliveTime:執行緒沒有任務時最多保持多長時間后會終止

創建執行緒池

  • JDK 5.0起提供了執行緒池相關API:ExecutorService 和 Executors
  • ExecutorService:真正的執行緒池介面,常見子類ThreadPoolExecutor
/**
 * @author :zsy
 * @date :Created 2021/4/14 15:35
 * @description:創建執行緒池
 */
public class TestPool {
    public static void main(String[] args) {
        //創建執行緒池
        ExecutorService service = Executors.newFixedThreadPool(10);
        //執行執行緒
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());

        //關閉執行緒池
        service.shutdown();
    }
}

class MyThread implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

JUC并發編程

  • Java真的可以開啟執行緒嗎?
    • 不能,呼叫本地方法start0(),底層是C++,Java無法直接操作硬體
  • 執行緒的狀態
public enum State {
        //新生
        NEW,
        //運行
        RUNNABLE,
        //阻塞
        BLOCKED,
        //等待,一直等待
        WAITING,
        //超時等待
        TIMED_WAITING,
    	//終止
        TERMINATED;
    }

傳統synchronized方法

/**
 * @author :zsy
 * @date :Created 2021/4/14 17:24
 * @description:買票
 */
public class SaleTicketDemo01 {
    
    /*
     * 真正的多執行緒開發,公司中的開發,降低耦合性
     * 執行緒就是一個單獨的資源類,沒有任何附屬的操作!
     */
    public static void main(String[] args) {
        //創建視窗物件
        Ticket ticket = new Ticket();

        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.saleTicket();
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.saleTicket();
            }
        }, "B").start();
    }
}

//買票視窗
class Ticket {
    //票數
    private int tickNum = 50;

    //買票
    public synchronized void saleTicket() {
        if (tickNum > 0) {
            try {
                TimeUnit.MILLISECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "買了第" + tickNum-- + "張票,剩余" + tickNum + "張票");
        }
    }
}

傳統Lock方法

//買票
    public void saleTicket() {
        if (tickNum > 0) {
            lock.lock();
            try {
                try {
                    TimeUnit.MILLISECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "買了第" + tickNum-- + "張票,剩余" + tickNum + "張票");
            }finally {
                lock.unlock();
            }
        }
    }

Lock鎖

公平鎖 :先來后到,十分公平

非公平鎖 :可以插隊,不公平

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

tryLock()和lock()的區別

tryLock()和lock()的區別

  • tryLock():如果獲取到鎖回傳true,否則回傳false,不阻塞
  • lock():沒有獲取到鎖,阻塞直到獲得鎖

執行緒之間通信問題

生產者和消費者問題

重要步驟:等待,業務,喚醒

synchronized版

/**
 * @author :zsy
 * @date :Created 2021/4/14 20:07
 * @description:生產者消費者模式,執行緒通信   
 */
public class A {

    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> { for (int i = 0; i < 20; i++) data.increment(); },  "A").start();
        new Thread(() -> { for (int i = 0; i < 20; i++) data.decrement(); },  "B").start();
    }
}

class Data {
    int num = 0;

    public synchronized void increment() {
        if (num != 0) {
            //等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //業務
        System.out.println(Thread.currentThread().getName() + "--->" + ++num);

        //喚醒
        this.notifyAll();
    }

    public synchronized void decrement() {
        if(num == 0) {
            //等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //業務
        System.out.println(Thread.currentThread().getName() + "--->" + --num);

        //喚醒
        this.notifyAll();
    }
}

在兩個執行緒的時候可以正常運行,當執行緒的數量加到四個的時候,出現了虛假喚醒的問題

new Thread(() -> { for (int i = 0; i < 20; i++) data.increment(); },  "C").start();
new Thread(() -> { for (int i = 0; i < 20; i++) data.decrement(); },  "D").start();
虛假喚醒
  • 產生原因:執行緒處于wait狀態的時候,執行緒也可以被喚醒,而不會被通知,中斷或超時,notifyAll()會喚醒所有等待這個同步監聽器的執行緒,所有等待的執行緒都會處于被喚醒的狀態,都會嘗試去嘗試獲取鎖然后運行,即使if()中的條件成立(因為if只判斷一次),也不會處于等待狀態
  • 解決方法:將if換成while即使執行緒被虛假喚醒,也會再次判斷條件是否成立,不會在進入等待狀態
 while (num != 0) {

JUC版

/**
 * @author :zsy
 * @date :Created 2021/4/14 20:45
 * @description:Lock鎖生產者消費者
 */
public class B {

    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> { for (int i = 0; i < 20; i++) data.increment(); },  "A").start();
        new Thread(() -> { for (int i = 0; i < 20; i++) data.decrement(); },  "B").start();
        new Thread(() -> { for (int i = 0; i < 20; i++) data.increment(); },  "C").start();
        new Thread(() -> { for (int i = 0; i < 20; i++) data.decrement(); },  "D").start();
    }

    static class Data {
        int num = 0;
        private Lock lock = new ReentrantLock();
        private Condition condition = lock.newCondition();

        public void increment() {
            lock.lock();
            try {
                while (num != 0) {
                    //等待
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //業務
                num++;
                System.out.println(Thread.currentThread().getName() + "--->" + num);

                //喚醒
                condition.signalAll();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }

        }

        public void decrement() {
            lock.lock();
            try {
                while(num == 0) {
                    //等待
                    condition.await();
                }

                //業務
                num--;
                System.out.println(Thread.currentThread().getName() + "--->" + num);

                //喚醒
                condition.signalAll();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }

        }
    }

}	
Condition實作精準喚醒
/**
 * @author :zsy
 * @date :Created 2021/4/14 21:12
 * @description:多個執行緒按順序列印
 */
public class C {

    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> { for (int i = 0; i < 10; i++) data.printA();}, "A").start();
        new Thread(() -> { for (int i = 0; i < 10; i++) data.printB();}, "B").start();
        new Thread(() -> { for (int i = 0; i < 10; i++) data.printC();}, "C").start();
    }

    static class Data {
        private int num = 1;
        private Lock lock = new ReentrantLock();
        private Condition condition1 = lock.newCondition();
        private Condition condition2 = lock.newCondition();
        private Condition condition3 = lock.newCondition();

        public void printA() {
            lock.lock();
            try {
                if (num != 1) {
                    condition1.await();
                }
                System.out.println(Thread.currentThread().getName() + "---->" + num);
                num = 2;
                condition2.signal();
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }

        public void printB() {
            lock.lock();
            try {
                if (num != 2) {
                    condition2.await();
                }
                System.out.println(Thread.currentThread().getName() + "---->" + num);
                num = 3;
                condition3.signal();
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }

        public void printC() {
            lock.lock();
            try {
                if (num != 3) {
                    condition3.await();
                }
                System.out.println(Thread.currentThread().getName() + "---->" + num);
                num = 1;
                condition1.signal();
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }

    }
}

synchronized

實作原理

在編譯的位元組碼中加入了兩條指令來進行代碼的同步

  • monitorenter

    每個物件有一個監視器鎖(monitor),當monitor被占用時,就會處于鎖定狀態,執行緒執行monitorenter指令時嘗試獲取monitor的所有權,程序如下:

    1. 如果monitor的進入數為0,則該執行緒進入monitor,然后將進入數設定為1,該執行緒即為monitor的所有制,
    2. 如果執行緒已經占有了該monitor,只是重新進入,則進入monitor的進入數加1
    3. 如果其他執行緒已經占有了monitor,則該執行緒進入阻塞狀態,指導monitor的進入數為0,再次重新嘗試獲取monitor所有權
  • monitorexit

    指令執行時,monitorexit的執行緒必須是objectref所對應的monitor的所有者,指令執行時,monitor的進入數減1,如果減1后進入數為0,那執行緒退出monitor,不再是這個monitor的所有者,其他被這個monitor阻塞的執行緒可以嘗試去獲取這個 monitor 的所有權,

通過這兩段描述,我們應該能很清楚的看出Synchronized的實作原理,Synchronized的語意底層是通過一個monitor的物件來完成,其實wait/notify等方法也依賴于monitor物件,這就是為什么只有在同步的塊或者方法中才能呼叫wait/notify等方法,否則會拋出java.lang.IllegalMonitorStateException的例外的原因,

鎖的物件問題

  • 同步代碼塊包括兩個部分
    • 一個作為鎖的物件參考
    • 一個作為由這個鎖保護的代碼塊
  • 以synchronized來修飾的方法就是一種橫跨整個方法體的同步代碼塊,其中該同步代碼塊的鎖就是方法所在的物件
  • 靜態的synchronized方法以Class為鎖

synchronized可以保證方法或者代碼塊在運行時,同一時刻只有一個方法可以進入到臨界區,同時它還可以保證共享變數的記憶體可見性,

Java中每一個物件都可以作為鎖,這是synchronized實作同步的基礎:

  • 普通同步方法,鎖是當前實體物件
  • 靜態同步方法,鎖是當前類的class物件
  • 同步方法塊,鎖是括號里面的物件

synchronized優化

鎖升級

在多執行緒并發編程中 synchronized 一直是元老級角色,很多人都會稱呼它為重量級鎖,

但是,隨著 Java SE 1.6 對 synchronized 進行了各種優化之后,有些情況下它就并不那么重,Java SE 1.6 中為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖,

針對 synchronized 獲取鎖的方式,JVM 使用了鎖升級的優化方式,就是先使用偏向鎖()優先同一執行緒然后再次獲取鎖,如果失敗,就升級為 CAS 輕量級鎖,如果失敗就會短暫自旋,防止執行緒被系統掛起,最后如果以上都失敗就升級為重量級鎖,
img

鎖只能升級,不能降級,

img

synchronized和Lock對比

類別synchronizedLock
存在層次Java的關鍵字,在JVM層面是Java類
鎖的釋放1、獲取鎖的執行緒執行完畢,釋放鎖2、執行緒執行出現例外,JVM會讓執行緒釋放鎖finally必須釋放鎖,不然容易造成死鎖
鎖獲取方式用synchronized關鍵字的兩個執行緒1和執行緒2,如果當前執行緒1獲得鎖,執行緒2執行緒等待,如果執行緒1阻塞,執行緒2則會一直等待下去Lock鎖就不一定會等待下去,如果嘗試獲取不到鎖,執行緒可以不用一直等待就結束了 lock.tryLock();
鎖型別可重入、不可中斷、非公平可重入、可中斷、可公平(兩者皆可)
使用區別synchronized鎖適合代碼少量的同步問題Lock鎖適合大量同步的代碼的同步問題
能否判斷獲取鎖的狀態Lock可以判斷是否獲取到鎖
鎖的物件代碼塊、方法、類代碼塊
  • 使用Lock鎖,JVM將花費較少的時間來調度執行緒,性能更好,并且具有更好的擴展性(提供更多的子類),
  • 優先使用順序
    • Lock -> 同步代碼塊(已經進入了方法體,分配了相應資源) -> 同步方法(在方法體外)

synchronized 和 ReentrantLock 區別是什么?

synchronized是和if、else、for、while一樣的關鍵字,ReentrantLock是類,這是二者的本質區別,既然ReentrantLock是類,那么它就提供了比synchronized更多更靈活的特性,可以被繼承、可以有方法、可以有各種各樣的類變數,ReentrantLock比synchronized的擴展性體現在幾點上:

  • ReentrantLock可以對獲取鎖的等待時間進行設定,這樣就避免了死鎖
  • ReentrantLock可以獲取各種鎖的資訊
  • ReentrantLock可以靈活地實作多路通知

另外,二者的鎖機制其實也是不一樣的:ReentrantLock底層呼叫的是Unsafe的park方法加鎖,synchronized操作的應該是物件頭中markword,

集合不安全類

ArrayList

測驗ArrayList的執行緒不安全性

/**
 * @author :zsy
 * @date :Created 2021/4/15 15:03
 * @description:CopyOnWriteList
 */
public class ListTest {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 4));
                System.out.println(Thread.currentThread().getName() + "---->" + list);
            }, String.valueOf(i)).start();
        }
    }
}

報例外:ConcurrentModificationException(并發修改例外)

解決方案

  • 使用Vector代理ArrayList
 List<String> list = new Vector<>();
  • 使用Collections工具類的synchronizedList()方法
List<String> list = Collections.synchronizedList(new ArrayList<>());
  • 使用JUC包下的執行緒安全類CopyOnWriteArrayList
List<String> list = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList原理

  • CopyOnWriteArrayList的實作原理就是讀寫分離,它對所有的寫操作都使用ReentrantLock來加鎖,對所有的讀操作都不進行加鎖,
  • CopyOnWriteArrayList在寫操作的時候,都會將list中的陣列copy一份作為快取,然后對該快取中的陣列進行操作,(此時若有其他執行緒過來讀的話,那么該執行緒讀的還是原先沒有被修改過的資料,若有其他執行緒過來寫的話,那么該執行緒會因為ReentrantLock的原因被鎖在外面),操作完畢后在將list中的陣列地址參考指向修改后的新陣列地址
  • 由CopyOnWriteArrayList的原理我們可以看出,我們每次往list里面寫資料的時候,陣列都需要重新copy一份,所以CopyOnWriteArrayList不需要實作像ArrayList一樣的擴容機制,初始創建時讓list中的陣列長度為0,我們每次add元素的時候,只需要對新陣列長度進行加1操作即可,所以CopyOnWriteArrayList實作起來相對還是比ArrayList簡單的,

? 所以我們可以看出,如果是讀操作十分頻繁的話,那么多執行緒下使用CopyOnWriteArrayList的性能基本上跟ArrayList差不多了,但如果是寫操作十分頻繁的話,建議還是不要使用CopyOnWriteArrayList了,因為它會造成陣列的不斷擴容及復制,十分耗性能,這其實就跟我們資料庫讀寫分離的原理是一樣的,如果寫操作很多的話,那么主從庫就會不斷的執行復制操作,消耗性能,但如果是讀操作多的話,由于該庫只用于讀,所以不會發生資料庫事務鎖,效率就會比一般的單庫查詢快很多,

Set

測驗Set的執行緒不安全性

/**
 * @author :zsy
 * @date :Created 2021/4/15 17:02
 * @description:Set集合
 */
public class SetTest {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                set.add(UUID.randomUUID().toString().substring(0, 4));
                System.out.println(set);
            }, String.valueOf(i)).start();
        }
    }
}
ConcurrentModificationException

解決方案

  • 使用Collections工具類的synchronizedList()方法
Set<String> set = Collections.synchronizedList(new HashSet<>());
  • 使用JUC包下的執行緒安全類CopyOnWriteArraySet
Set<String> set = new CopyOnWriteArraySet<>();

CopyOnWriteArraySet底層是一個CopyOnWriteArrayList

Map

測驗HashMap的執行緒不安全性

/**
 * @author :zsy
 * @date :Created 2021/4/15 17:50
 * @description:HashMap
 */
public class MapTest {
    public static void main(String[] args) {
        Map<String, String>map = new HashMap<>();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 4));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }
}
ConcurrentModificationException

解決方案

  • 使用Collections工具類的synchronizedMap()方法
Map<String, String>map = Collections.synchronizedMap(new HashMap<>());
  • 使用JUC包下的執行緒安全類ConcurrentHashMap
Map<String, String>map = new ConcurrentHashMap<>();

Hashtable

Hashtable和HashMap對比

  • 執行緒安全:HashMap是執行緒不安全的類,多執行緒下會造成并發沖突,但單執行緒下運行效率較高;Hashtable是執行緒安全的類,很多方法都使用了synchronized修飾,但同時因為加鎖導致并發小效率低下,單執行緒環境效率也十分低(當多個執行緒同時對Hashtable進行put操作是,盡管插入的不是同一個槽位,但是由于多個執行緒拿的是一把鎖,所以必須按順序執行put操作)
  • HashMap需要重新計算hash值,而Hashtable直接使用物件的hashCode
  • 插入null:HashMap允許有一個鍵為null,允許多個值為null;但Hashtable不允許鍵或值為null
public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }
    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();//這里如果key == null 會拋出例外
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }
    addEntry(hash, key, value, index);
    return null;
}
  • 容量:HashMap底層陣列長度必須為2的冪,這樣做是為了hash做準備,默認為16;而Hashtable底層陣列長度可以為任意值,這就造成了hash演算法散射不均勻,容易造成hash沖突,默認值為11,前者擴容時,擴大兩倍,后者擴大兩倍+1

rehash 擴容

protected void rehash() {
    int oldCapacity = table.length;
    Entry<?,?>[] oldMap = table;
    // overflow-conscious code
    int newCapacity = (oldCapacity << 1) + 1;
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        if (oldCapacity == MAX_ARRAY_SIZE)
            // Keep running with MAX_ARRAY_SIZE buckets
            return;
        newCapacity = MAX_ARRAY_SIZE;
    }
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
    modCount++;
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    table = newMap;
    for (int i = oldCapacity ; i-- > 0 ;) {
        for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
            Entry<K,V> e = old;
            old = old.next;
            int index = (e.hash & 0x7FFFFFFF) % newCapacity;
            e.next = (Entry<K,V>)newMap[index];
            newMap[index] = e;
        }
    }
}

Hashtable擴容,原容量 * 2 +1 ,插入采用頭插法

  • Hash映射:HashMap的hash演算法通過常規設計,將底層table長度設計為2的冪,使用位與運算代替取模運算,減少運算消耗;而Hashtable的hash演算法首先使得hash值小于整型數最大值,再通過取模進行散射運算

因為hashSeed ^ k.hashCode(); 的值可能是負數,如果直接index = hash % tab.length; 這樣就會導致index可能為負數

int index = (hash & 0x7FFFFFFF) % tab.length;

同步容器和并發容器

以下內容引自《Java并發編程實戰第五章》

同步容器

同步容器類包括Vector和Hashtable,二者是早期JDK的一部分,此外還包括在JDK1.2中添加的一些功能相似的類,這些同步的封裝器類由Collections.synchronizedXxx工廠方法創建,這些類實作執行緒安全的方式是:將他們封裝起來,并對每個共有方法都有同步,每次只有一個執行緒能訪問容器狀態,

同步器的問題

同步容器類都是執行緒安全的,但是在某些情況下可能需要額外的客戶端加鎖來保護復合操作,容器上常見的符合操作包括:迭代(反復訪問元素,直到遍歷完容器的所有元素)、跳轉(根據指定順序找到當前元素的下一個元素)以及條件運算,例如:若沒有則添加(在Map中是否存在鍵值K,如果沒有,則加入二元組(K,V)),在同步容器類中,這些復和操作在沒有客戶端加鎖的情況下仍是執行緒安全的,但當其他執行緒并發地修改容器時,他們可能會表現出意料之外的行為,

/**
 * @author :zsy
 * @date :Created 2021/4/24 16:48
 * @description:測驗Vector的執行緒安全性
 */
public class TestVector {
    public static void main(String[] args) {
        Vector vector = new Vector();
        vector.add(1);
        vector.add(2);
        new Thread(() -> System.out.println(getLast(vector)),
        new Thread(() -> deleteLast(vector), "B").start();
    }
    public static Object getLast(Vector list) {
        int lastIndex = list.size() - 1;
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return list.get(lastIndex);
    }
    public static void deleteLast(Vector list) {
        int lastIndex = list.size() - 1;
        list.remove(lastIndex);
    }
}

運行上面程式會拋出Exception in thread “A” java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 1例外

分析拋出例外的原因

如果執行緒A在包含2個元素的Vector呼叫getLast,同時執行緒B在同一個Vector上呼叫deleteLast,這些操作的交替執行如圖,getLast會拋出ArrayIndexOutOfBoundsException例外,

image-20210424170021378

由于并發容器類要遵守同步策略,即支持客戶端加鎖,因此可能創建一些新的操作,只要我們知道應該使用哪一個鎖,那么新操作就與容器的其他操作一樣都是原子操作,同步容器類會保護它的每個方法,

public static Object getLast(Vector list) {
    synchronized (list) {
        int lastIndex = list.size() - 1;
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return list.get(lastIndex);
    }
}
public static void deleteLast(Vector list) {
    synchronized (list) {
        int lastIndex = list.size() - 1;
        list.remove(lastIndex);
    }
}

同理所有對共享容器進行迭代的地方都需要加鎖,實際情況要更加復雜,因為在某些情況,迭代器會隱藏起來,操作不放會拋出ConcurrentModificationException

并發容器

同步容器將所有對容器的訪問都串行化,以實作他們的執行緒安全性,這種方法的代價是嚴重降低并發性,當多個執行緒競爭訪問容器的鎖時,吞吐量將會嚴重減低,

針對多個執行緒設計的,用并發容器來代替同步容器,可以極大地提高伸縮性并降低風險, 如ConcurrentHashMap,CopyOnWriteArrayList等,并發容器使用了與同步容器完全不同的加鎖策略來提供更高的并發性和伸縮性,例如在ConcurrentHashMap中采用了一種粒度更細的加鎖機制,可以稱為分段鎖,在這種鎖機制下,允許任意數量的讀執行緒并發地訪問map,并且執行讀操作的執行緒和寫操作的執行緒也可以并發的訪問map,同時允許一定數量的寫操作執行緒并發地修改map,所以它可以在并發環境下實作更高的吞吐量,

常用輔助類

CountDownLatch

允許一個或多個執行緒等待直到在其他執行緒中執行的一組操作完成的同步輔助

A CountDownLatch用給定的計數初始化, await方法阻塞,直到由于countDown()方法的呼叫而導致當前計數達到零,之后所有等待執行緒被釋放,并且任何后續的await 呼叫立即回傳, 這是一個一次性的現象 - 計數無法重置, 如果您需要重置計數的版本,請考慮使用CyclicBarrier

/**
 * @author :zsy
 * @date :Created 2021/4/20 19:50
 * @description:減法計數器
 */
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "---->" + "GO");
                countDownLatch.countDown(); //計數器 - 1
            }, String.valueOf(i)).start();
        }
        countDownLatch.await();//等待countDownLatch歸零,執行下面的操作
        System.out.println("Close door");
    }
}

原理:每次呼叫countDown()數量-1,假設計數器為零,countDownLatch.await()就會被喚醒,繼續執行,

A CountDownLatch是一種通用的同步工具,可用于多種用途, 一個CountDownLatch為一個計數的CountDownLatch用作一個簡單的開/關鎖存器,或者門:所有執行緒呼叫await在門口等待,直到被呼叫countDown()的執行緒打開, 一個CountDownLatch初始化N可以用來做一個執行緒等待,直到N個執行緒完成某項操作,或某些動作已經完成N次,

CountDownLatch一個有用的屬性是,它不要求呼叫countDown執行緒等待計數到達零之前繼續,它只是阻止任何執行緒通過await ,直到所有執行緒可以通過,

用法

  • 第一個是啟動信號,防止任何作業人員進入,直到駕駛員準備好繼續前進;
  • 第二個是完成信號,允許司機等到所有的作業人員完成,

CyclicBarrier

允許一組執行緒全部等待彼此達到共同屏障點的同步輔助, 回圈阻塞在涉及固定大小的執行緒方的程式中很有用,這些執行緒必須偶爾等待彼此, 屏障被稱為回圈 ,因為它可以在等待的執行緒被釋放之后重新使用,

/**
 * @author :zsy
 * @date :Created 2021/4/20 20:04
 * @description:回圈等待屏障
 */
public class CyclicBarrierDemo {

    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, new Thread(() -> {
            System.out.println("召喚神龍");
        }));

        for (int i = 0; i < 7; i++) {
            final int tmp = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "獲得第" + tmp + "顆龍珠");
                try {
                    cyclicBarrier.await(); //當等待的類數量達到時,才會執行,否則一直等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

作用:控制當前執行緒數量達到一定數量后,一起執行某項操作

CyclicBarrier對失敗的同步嘗試使用all-or-none斷裂模型:如果執行緒由于中斷,故障或超時而過早離開障礙點,那么在該障礙點等待的所有其他執行緒也將通過BrokenBarrierException (或InterruptedException)例外離開,

在 Java 中 CyclicBarrier和 CountDownLatch有什么區別

區別CountDownLatchCyclicBarrier
作用一般用于某個執行緒A等待若干個其他執行緒完成任務后,他才執行,強調多個執行緒完成某件事情一般用于一組執行緒相互等待至某個狀態,然后一組執行緒同時執行,強調多個執行緒互等,等大家都完成,再攜手共進
對當前執行緒的影響呼叫CountDownLatch的countDown方法會不導致當前執行緒阻塞,會繼續執行下去呼叫CyclicBarrier的await方法后,會阻塞當前執行緒(里面使用了ReentrantLock鎖),直到CyclicBarrier指定的執行緒全部都到達了指定點的時候,才能繼續往下執行
原理利用繼承AQS的共享鎖來進行執行緒的通知利用ReentrantLock的Condition來阻塞和通知執行緒
難易程度CountDownLatch方法比較少,操作比較簡單CyclicBarrier提供的方法更多,比如能夠通過getNumberWaiting(),isBroken()這些方法獲取當前多個執行緒的狀態,并且CyclicBarrier的構造方法可以傳入barrierAction,指定當所有執行緒都到達時執行的業務功能
是否可以復用

Semaphore

一個計數信號量, 在概念上,信號量維持一組許可證, 如果有必要,每個acquire()都會阻塞,直到許可證可用,然后才能使用它, 每個release()添加許可證,潛在地釋放阻塞獲取方, 但是,沒有使用實際的許可證物件; Semaphore只保留可用數量的計數,并相應地執行,

信號量通常用于限制執行緒數,而不是訪問某些(物理或邏輯)資源,

/**
 * @author :zsy
 * @date :Created 2021/4/20 20:32
 * @description:信號量
 */
public class SemaphoreDemo {

    public static void main(String[] args) {
        //指定只有3個信號量
        Semaphore semaphore = new Semaphore(3);
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();//獲取當前的信號量
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "獲得");
                //doSomething()
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "離開");
                semaphore.release();//釋放當前信號量
            }, String.valueOf(i)).start();
        }
    }
}

作用

Semaphore 就是一個信號量,它的作用是限制某段代碼塊的并發數,Semaphore有一個建構式,可以傳入一個 int 型整數 n,表示某段代碼最多只有 n 個執行緒可以訪問,如果超出了 n,那么請等待,等到某個執行緒執行完畢這段代碼塊,下一個執行緒再進入,由此可以看出如果 Semaphore 建構式中傳入的 int 型整數 n=1,相當于變成了一個 synchronized 了,

Semaphore(信號量)-允許多個執行緒同時訪問: synchronized 和 ReentrantLock 都是一次只允許一個執行緒訪問某個資源,Semaphore(信號量)可以指定多個執行緒同時訪問某個資源,

public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

此類的建構式可選擇接受公平引數, 當設定為false時,此類不會保證執行緒獲取許可的順序, 特別是, 闖入是允許的,也就是說,一個執行緒呼叫acquire()可以提前已經等待執行緒分配的許可證-在等待執行緒佇列的頭部邏輯新的執行緒將自己, 當公平設定為真時,信號量保證呼叫acquire方法的執行緒被選擇以按照它們呼叫這些方法的順序獲得許可(先進先出; FIFO), 請注意,FIFO排序必須適用于這些方法中的特定內部執行點, 因此,一個執行緒可以在另一個執行緒之前呼叫acquire ,但是在另一個執行緒之后到達排序點,并且類似地從方法回傳, 另請注意, 未定義的tryAcquire方法不符合公平性設定,但將采取任何可用的許可證,

讀寫鎖(ReadWriteLock)

A ReadWriteLock維護一對關聯的locks ,一個用于只讀操作,一個用于寫入, read lock可以由多個閱讀器執行緒同時進行,只要沒有作者, write lock是獨家的,

/**
 * @author :zsy
 * @date :Created 2021/4/20 21:33
 * @description:讀寫鎖
 */
public class ReadWriteLockDemo {
    public static void main(String[] args) {

        MyCache myCache = new MyCache();

        for (int i = 0; i < 5; i++) {
            final String tmp = String.valueOf(i);
            new Thread(() -> {
                myCache.put(tmp, "a");
            }, String.valueOf(i)).start();
        }

        for (int i = 0; i < 5; i++) {
            final String tmp = String.valueOf(i);
            new Thread(() -> {
                myCache.get(tmp);
            }, String.valueOf(i)).start();
        }

    }
}

class MyCache {
    private Map<String, String > map = new HashMap<>();
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void put(String key, String value) {
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "開始寫");
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "寫完成");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock();
        }

    }

    public String get(String key) {
        String val = null;
        lock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "開始讀");
            val = map.get(key);
            System.out.println(Thread.currentThread().getName() + "讀完成");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.readLock().unlock();
        }
        return val;
    }
}
1開始寫
1寫完成
0開始寫
0寫完成
2開始寫
2寫完成
3開始寫
3寫完成
4開始寫
4寫完成
0開始讀
0讀完成
2開始讀
3開始讀
3讀完成
1開始讀
4開始讀
4讀完成
1讀完成
2讀完成

ReadWriteLock 是什么

首先明確一下,不是說 ReentrantLock 不好,只是 ReentrantLock 某些時候有局限,如果使用 ReentrantLock,可能本身是為了防止執行緒 A 在寫資料、執行緒 B 在讀資料造成的資料不一致,但這樣,如果執行緒 C 在讀資料、執行緒 D 也在讀資料,讀資料是不會改變資料的,沒有必要加鎖,但是還是加鎖了,降低了程式的性能,因為這個,才誕生了讀寫鎖 ReadWriteLock,

ReadWriteLock 是一個讀寫鎖介面,讀寫鎖是用來提升并發程式性能的鎖分離技術,ReentrantReadWriteLock 是 ReadWriteLock 介面的一個具體實作,實作了讀寫的分離,讀鎖是共享的,寫鎖是獨占的,讀和讀之間不會互斥,讀和寫、寫和讀、寫和寫之間才會互斥,提升了讀寫的性能,

而讀寫鎖有以下三個重要的特性:

(1)公平選擇性:支持非公平(默認)和公平的鎖獲取方式,吞吐量還是非公平優于公平,

(2)重進入:讀鎖和寫鎖都支持執行緒重進入,

(3)鎖降級:遵循獲取寫鎖、獲取讀鎖再釋放寫鎖的次序,寫鎖能夠降級成為讀鎖,

阻塞佇列

image-20210421152800616

四組API

方式拋出例外不拋出例外,有回傳值阻塞一直等待超時等待
添加addofferputoffer(“c”,2, TimeUnit.SECONDS)
洗掉removepolltakepoll(2,TimeUnit.SECONDS)
判斷對列首元素elementpeek--
/**
 * @author :zsy
 * @date :Created 2021/4/21 15:41
 * @description:阻塞佇列
 */
public class bq {
    public static void main(String[] args) throws InterruptedException {
        test04();
    }

    /**
     * 拋出例外
     */
    public static void test01() {
        BlockingQueue blockingQueue = new ArrayBlockingQueue(2);
        blockingQueue.add("a");
        blockingQueue.add("b");
        //拋出例外IllegalStateException
        //blockingQueue.add("c");
        System.out.println(blockingQueue.element());
        blockingQueue.remove();
        blockingQueue.remove();
        //拋出例外NoSuchElementException
        //blockingQueue.remove();
    }

    /**
     * 不拋出例外,有回傳值
     */
    public static void test02() {
        BlockingQueue blockingQueue = new ArrayBlockingQueue(2);
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        blockingQueue.peek();
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        //blockingQueue.poll();
    }

    /**
     * 阻塞一直等待
     */
    public static void test03() throws InterruptedException {
        BlockingQueue blockingQueue = new ArrayBlockingQueue(2);
        blockingQueue.put("a");
        blockingQueue.put("b");
        blockingQueue.put("c");

        blockingQueue.take();
        blockingQueue.take();
        //blockingQueue.remove();
    }

    /**
     * 超時等待
     * @throws InterruptedException
     */
    public static void test04() throws InterruptedException {
        BlockingQueue blockingQueue = new ArrayBlockingQueue(2);
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        blockingQueue.offer("c",2, TimeUnit.SECONDS);

        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        blockingQueue.poll(2,TimeUnit.SECONDS);
    }
}

SynchronousQueue

同步佇列沒有任何內部容量,甚至沒有容量,每個插入操作必須等待另一個執行緒回應的洗掉操作

/**
 * @author :zsy
 * @date :Created 2021/4/21 16:37
 * @description:同步佇列
 */
public class SynchronousQueueDemo {

    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
        new Thread(() -> {
            try {
                for (int i = 0; i < 3; i++) {
                    System.out.println(Thread.currentThread().getName() + "添加元素" + i);
                    blockingQueue.put("a" + i);
                    System.out.println(Thread.currentThread().getName() + "添加成功" + i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "A").start();
        new Thread(() -> {

            for (int i = 0; i < 3; i++) {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "洗掉元素");
                System.out.println(blockingQueue.poll());
                System.out.println(Thread.currentThread().getName() + "洗掉成功");
            }
        }, "B").start();
    }
}
A添加元素0
B洗掉元素
a0
B洗掉成功
A添加成功0
A添加元素1
B洗掉元素
a1
B洗掉成功
A添加成功1
A添加元素2
B洗掉元素
a2
B洗掉成功
A添加成功2

執行緒池

池化技術

程式運行的本質:占用系統的資源,優化資源的使用 => 池化技術

執行緒池的好處

  1. 降低資源消耗
  2. 提高回應速度
  3. 方便管理
  4. 執行緒復用,可以控制最大并發數,管理執行緒

創建執行緒池的三大方法

/**
 * @author :zsy
 * @date :Created 2021/4/21 17:05
 * @description:執行緒池的三大方法
 */
public class Demo1 {
    public static void main(String[] args) {
        //ExecutorService threadPool = Executors.newSingleThreadExecutor();//創建一個執行緒
        //ExecutorService threadPool = Executors.newFixedThreadPool(5);//創建指定數量的執行緒
        ExecutorService threadPool = Executors.newCachedThreadPool();//創建足夠多的執行緒

        try {
            for (int i = 0; i < 10; i++) {
                final int tmp = i;
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "----->" + tmp);
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();

        }
    }
}

newScheduledThreadPool 創建一個支持定時及周期性的任務執行的執行緒池,多數情況下可用來替代Timer類,

三大方法原始碼分析

newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

newCachedThreadPool

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

引數分析

public ThreadPoolExecutor(int corePoolSize, 					
                          int maximumPoolSize,					
                          long keepAliveTime,					
                          TimeUnit unit,						
                          BlockingQueue<Runnable> workQueue) {	
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

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;
}

創建的執行緒池最大包含請求數量 = maximumPoolSize + workQueue的長度,超過最大請求數量,就會采取拒絕策略

image-20210421174200019

四個拒絕策略

  • AbortPolicy:拋出例外RejectedExecutionException
  • CallerRunsPolicy:執行呼叫者執行緒中的任務,除非執行執行緒已被關閉,否則任務被拋棄,回傳上一級執行緒
  • DiscardPolicy:丟棄拒絕的任務,不拋出例外
  • DiscardOldestPolicy:被拒絕的任務的處理程式,丟棄最舊的未處理請求,然后重試 execute ,除非執行程式被關閉,在這種情況下,任務被丟棄,(與最先來的執行緒競爭)

image-20210421171756151

自定義執行緒池

/**
 * @author :zsy
 * @date :Created 2021/4/21 17:50
 * @description:自定義執行緒池
 */
public class Demo2 {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(2,
                5,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                new ThreadPoolExecutor.AbortPolicy());
        //執行緒池最大容量為5 + 3 = 8

        try {
            for (int i = 0; i < 8; i++) {
                final int tmp = i;
                threadPool.execute(new Thread(() -> {
                    System.out.println(Thread.currentThread().getName() + "---->" + tmp);
                }));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

總結

最大執行緒池數量如何定義

  • IO密集型:判斷程式中十分消耗IO的執行緒數量,最大執行緒數量大于2倍的IO執行緒數量
  • CPU密集型:判斷CPU核數,最大執行緒池數量等于CPU核數,可以保持CPU的高效運行

執行緒池的作業流程

image-20210424211008284

1)當提交一個新任務到執行緒池時,執行緒池判斷corePoolSize執行緒池是否都在執行任務,如果有空閑執行緒,則從核心執行緒池中取一個執行緒來執行任務,直到當前執行緒數等于corePoolSize;

2)如果當前執行緒數為corePoolSize,繼續提交的任務被保存到阻塞佇列中,等待被執行;

3)如果阻塞佇列滿了,那就創建新的執行緒執行當前任務,直到執行緒池中的執行緒數達到maxPoolSize,這時再有任務來,由飽和策略來處理提交的任務

四大函式式介面

只有一個方法的介面

image-20210421192526470

函式式介面

輸入T型別回傳R型別

public interface Function<T, R> {
    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);

實體

/**
 * @author :zsy
 * @date :Created 2021/4/21 19:26
 * @description:函式式介面
 */
public class Demo1 {
    public static void main(String[] args) {
        Function<String, String> function = str -> {return str;};

        System.out.println(function.apply("zhangsna"));
    }
}

斷定性介面

輸入T型別回傳boolean型別

@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);

實體

/**
 * @author :zsy
 * @date :Created 2021/4/21 19:29
 * @description:斷定性介面
 */
public class Demo2 {
    public static void main(String[] args) {
        Predicate<String> predicate = str -> {return str.isEmpty();};

        predicate.test("zhangsan");

    }
}

消費型介面

只要輸入型別,沒有回傳型別

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);

供給性介面

只有回傳型別,沒有輸入型別

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

實體

/**
 * @author :zsy
 * @date :Created 2021/4/21 19:37
 * @description:消費型介面和供給型介面
 */
public class Demo3 {

    public static void main(String[] args) {
        Consumer<String> consumer = o -> System.out.println(o);

        Supplier<Integer> supplier = () -> 1024;
        
    }
}

流式編程

大資料 : 存盤 + 計算

存盤交給集合、資料庫

計算交給流

/**
 * @author :zsy
 * @date :Created 2021/4/21 20:05
 * @description:流操作
 */
public class Demo1 {
    public static void main(String[] args) {
        List<User> userList = new ArrayList<>();
        userList.add(new User("a", 10,30.0));
        userList.add(new User("b", 11,25.0));
        userList.add(new User("c", 12,40.0));
        userList.add(new User("d", 14,31.0));
        userList.add(new User("e", 16,35.0));
        //找出年齡大于11成績大于33的用戶倒序的第一位名字的大寫
        userList.stream()
                .filter(u -> {return u.getAge() > 11;})
                .filter(u -> {return u.getPoint() > 33;})
                .sorted((u1, u2) -> {return u2.getUsername().compareTo(u1.getUsername());})
                .limit(1)
                .map(user -> {return user.getUsername().toUpperCase();})
                .forEach(System.out :: println);
    }
}

Future

參考:JAVA Future類詳解

什么是Future模式

Future模式是多執行緒開發中常見的設計模式,它的核心思想就是異步呼叫,他無法立即回傳你需要的資料,但是他會回傳一個契約,將來你可以憑借這個契約去獲取你需要的資訊,例如:點外賣,外賣訂單相當于契約,你可以通過外面訂單獲取到你的外賣,而制作外賣是一個很長的程序,無法立刻獲得外賣,

同步方法

客戶端發出獲取資料的請求,盡管請求需要很長一段時間才能回傳,但是同步模式規定客戶端一直等到到資料回傳后才進行后續任務

Java中的Future

img

首先,JDK內部有一個Future介面,這就是類似前面提到的訂單,當然了,作為一個完整的商業化產品,這里的Future的功能更加豐富了,除了get()方法來獲得真實資料以外,還提供一組輔助方法,比如:

  • cancel():如果等太久,你可以直接取消這個任務
  • isCancelled():任務是不是已經取消了
  • isDone():任務是不是已經完成了
  • get():有2個get()方法,不帶引數的表示無窮等待,或者你可以只等待給定時間

什么是FutureTask

FutureTask表示一個異步運算任務,FutureTask里面可以傳入一個Callable的具體實作類,可以對這個異步運算任務的結果進行等待獲取、判斷是否已經完成、取消任務等操作,只有當運算完成的時候結果才能取回,如果尚未完成get方法將阻塞,一個FutureTask物件可以對呼叫了Callable和Runnable的物件進行包裝,由于由于 FutureTask 也是Runnable 介面的實作類,所以 FutureTask 也可以放入執行緒池中,

ForkJoin

什么是ForkJoin

并發執行任務,提高效率,大資料量

image-20210422164719113

/**
 * @author :zsy
 * @date :Created 2021/4/22 17:10
 * @description:ForkJoin
 */
public class ForkJoinDemo extends RecursiveTask<Long> {

    private Long start;
    private Long end;
    private final Long tmp = 1000L;

    public ForkJoinDemo(Long start, Long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        if (end - start > tmp) {
            Long sum = 0L;
            for (Long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        } else {
            Long mid = start + end >> 1;
            ForkJoinDemo f1 = new ForkJoinDemo(start, mid);
            ForkJoinDemo f2 = new ForkJoinDemo(mid + 1, end);
            return  f1.join() + f2.join();
        }
    }

    public static void main(String[] args) {
        test1();
    }


    public static void test1() {
        long start = System.currentTimeMillis();
        Long sum = (0 + 10_0000_0000) * 10_0000_0000L / 2;
        long end = System.currentTimeMillis();
        System.out.println(end - start + "ms : " + sum);
    }

    public static void test2() {
        long start = System.currentTimeMillis();
        Long sum = 0L;
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinDemo forkJoinDemo = new ForkJoinDemo(0L, 10_0000_0000L);
        ForkJoinTask submit = forkJoinPool.submit(forkJoinDemo);
        try {
            sum = (Long) submit.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start + "ms : " + sum);
    }

    public static void test3() {
        long start = System.currentTimeMillis();
        Long sum = 0L;
        sum = LongStream.rangeClosed(0,10_0000_0000L).parallel().reduce(0, Long :: sum);
        long end = System.currentTimeMillis();
        System.out.println(end - start + "ms : " + sum);
    }
}

CompletableFuture

Future模式雖然好用,但也有一個問題,那就是將任務提交給執行緒后,呼叫執行緒并不知道這個任務什么時候執行完,如果執行呼叫get()方法或者isDone()方法判斷,可能會進行不必要的等待,那么系統的吞吐量很難提高,

為了解決這個問題,JDK對Future模式又進行了加強,創建了一個CompletableFuture,它可以理解為Future模式的升級版本,它最大的作用是提供了一個回呼機制,可以在任務完成后,自動回呼一些后續的處理,這樣,整個程式可以把“結果等待”完全給移除了,

開辟一個新的執行緒去執行異步任務,如果需要獲取回傳值,那么主執行緒將進入阻塞狀態

異步呼叫執行緒

/**
 * @author :zsy
 * @date :Created 2021/4/24 11:56
 * @description:
 */
public class CompletableDemo {

    public static void main(String[] args) {
        //創建異步任務
        CompletableFuture.supplyAsync(CompletableDemo::getPrice)
                //當getPrice()執行完后會回呼當前函式
                .thenAccept(result -> {
                    System.out.println("price = " + result);
                })
                //出現例外后,會回呼exceptionally
                .exceptionally(e -> {
                    System.out.println(e.getMessage());
                    return null;
                });
        System.out.println(1111);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(2222);
    }

    public static Double getPrice() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (Math.random() < 0.3) {
            throw new RuntimeException("Error");
        }
        return Math.random() * 20;
    }
}

在這個例子中,首先以getPrice()為基礎創建一個異步呼叫,接著,使用thenAccept()方法,設定了一個后續的操作,也就是當getPrice()執行完成后的后續處理,

不難看到,CompletableFuture比一般的Future更具有實用性,因為它可以在Future執行成功后,自動回呼進行下一步的操作,因此整個程式不會有任何阻塞的地方(也就是說你不用去到處等待Future的執行,而是讓Future執行成功后,自動來告訴你),

以上面的代碼為例,CompletableFuture之所有會有那么神奇的功能,完全得益于AsyncSupply類(由上述代碼中的supplyAsync()方法創建),
AsyncSupply在執行時,如下所示:

    public void run() {
        CompletableFuture<T> d; Supplier<T> f;
        if ((d = dep) != null && (f = fn) != null) {
            dep = null; fn = null;
            if (d.result == null) {
                try {
                    //這里就是你要執行的異步方法
                    //結果會被保存下來,放到d.result欄位中
                    d.completeValue(f.get());
                } catch (Throwable ex) {
                    d.completeThrowable(ex);
                }
            }
            //執行成功了,進行后續處理,在這個后續處理中,就會呼叫thenAccept()中的消費者
            //這里就相當于Future完成后的通知
            d.postComplete();
        }
    }

繼續看d.postComplete(),這里會呼叫后續一系列操作

   final void postComplete() {
                //省略部分代碼,重點在tryFire()里
                //在tryFire()里,真正觸發了后續的呼叫,也就是thenAccept()中的部分
                f = (d = h.tryFire(NESTED)) == null ? this : d;
            }
        }
    }

JMM

作業記憶體的八種操作

image-20210423173533103

關于主記憶體與作業記憶體之間的互動協議,即一個變數如何從主記憶體拷貝到作業記憶體,如何從作業記憶體同步到主記憶體中的實作細節,java記憶體模型定義了8種操作來完成,這8種操作每一種都是原子操作,8種操作如下:

  • lock(鎖定):作用于主記憶體,它把一個變數標記為一條執行緒獨占狀態;
  • read(讀取):作用于主記憶體,它把變數值從主記憶體傳送到執行緒的作業記憶體中,以便隨后的load動作使用;
  • load(載入):作用于作業記憶體,它把read操作的值放入作業記憶體中的變數副本中;
  • use(使用):作用于作業記憶體,它把作業記憶體中的值傳遞給執行引擎,每當虛擬機遇到一個需要使用這個變數的指令時候,將會執行這個動作;
  • assign(賦值):作用于作業記憶體,它把從執行引擎獲取的值賦值給作業記憶體中的變數,每當虛擬機遇到一個給變數賦值的指令時候,執行該操作;
  • store(存盤):作用于作業記憶體,它把作業記憶體中的一個變數傳送給主記憶體中,以備隨后的write操作使用;
  • write(寫入):作用于主記憶體,它把store傳送值放到主記憶體中的變數中,
  • unlock(解鎖):作用于主記憶體,它將一個處于鎖定狀態的變數釋放出來,釋放后的變數才能夠被其他執行緒鎖定;

Java記憶體模型還規定了執行上述8種基本操作時必須滿足如下規則:

(1)不允許read和load、store和write操作之一單獨出現(即不允許一個變數從主存讀取了但是作業記憶體不接受,或者從作業記憶體發起會寫了但是主存不接受的情況),以上兩個操作必須按順序執行,但沒有保證必須連續執行,也就是說,read與load之間、store與write之間是可插入其他指令的,

(2)不允許一個執行緒丟棄它的最近的assign操作,即變數在作業記憶體中改變了之后必須把該變化同步回主記憶體,

(3)不允許一個執行緒無原因地(沒有發生過任何assign操作)把資料從執行緒的作業記憶體同步回主記憶體中,

(4)一個新的變數只能從主記憶體中“誕生”,不允許在作業記憶體中直接使用一個未被初始化(load或assign)的變數,換句話說就是對一個變數實施use和store操作之前,必須先執行過了assign和load操作,

(5)一個變數在同一個時刻只允許一條執行緒對其執行lock操作,但lock操作可以被同一個條執行緒重復執行多次,多次執行lock后,只有執行相同次數的unlock操作,變數才會被解鎖,

(6)如果對一個變數執行lock操作,將會清空作業記憶體中此變數的值,在執行引擎使用這個變數前,需要重新執行load或assign操作初始化變數的值,

(7)如果一個變數實作沒有被lock操作鎖定,則不允許對它執行unlock操作,也不允許去unlock一個被其他執行緒鎖定的變數,

(8)對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體(執行store和write操作),

JMM和底層實作原理

現代計算機記憶體模型

其中一個重要的復雜性來源是絕大多數的運算任務都不可能只靠處理器“計算”就能完成,處理器至少要與記憶體互動,如讀取運算資料、存盤運算結果等,這個I/O操作是很難消除的(無法僅靠暫存器來完成所有運算任務),早期計算機中cpu和記憶體的速度是差不多的,但在現代計算機中,cpu的指令速度遠超記憶體的存取速度,由于計算機的存盤設備與處理器的運算速度有幾個數量級的差距,所以現代計算機系統都不得不加入一層讀寫速度盡可能接近處理器運算速度的高速快取(Cache)來作為記憶體與處理器之間的緩沖:將運算需要使用到的資料復制到快取中,讓運算能快速進行,當運算結束后再從快取同步回記憶體之中,這樣處理器就無須等待緩慢的記憶體讀寫了,

img

快取一致性問題

  • 通過在總線加LOCK#鎖的方式
  • 通過快取一致性協議

在早期的CPU當中,是通過在總線上加LOCK#鎖的形式來解決快取不一致的問題,因為CPU和其他部件進行通信都是通過總線來進行的,如果對總線加LOCK#鎖的話,也就是說阻塞了其他CPU對其他部件訪問(如記憶體),從而使得只能有一個CPU能使用這個變數的記憶體,比如上面例子中 如果一個執行緒在執行 i = i +1,如果在執行這段代碼的程序中,在總線上發出了LCOK#鎖的信號,那么只有等待這段代碼完全執行完畢之后,其他CPU才能從變數i所在的記憶體讀取變數,然后進行相應的操作,這樣就解決了快取不一致的問題,

但是上面的方式會有一個問題,由于在鎖住總線期間,其他CPU無法訪問記憶體,導致效率低下,

所以就出現了快取一致性協議,最出名的就是Intel 的MESI協議,MESI協議保證了每個快取中使用的共享變數的副本是一致的,它核心的思想是:當CPU寫資料時,如果發現操作的變數是共享變數,即在其他CPU中也存在該變數的副本,會發出信號通知其他CPU將該變數的快取行置為無效狀態,因此當其他CPU需要讀取這個變數時,發現自己快取中快取該變數的快取行是無效的,那么它就會從記憶體重新讀取,

Java記憶體模型

JMM定義了Java 虛擬機(JVM)在計算機記憶體(RAM)中的作業方式,JMM定義了執行緒和主記憶體之間的抽象關系:執行緒之間的共享變數存盤在主記憶體(Main Memory)中,每個執行緒都一個私有的本地記憶體(Local Memory),本地記憶體中存盤了該執行緒以讀/寫共享變數的副本,本地記憶體是一個抽象概念,并不真實存在,它涵蓋了快取、寫緩沖區、暫存器以及其他硬體和編譯器優化

img

JMM的核心是找到一個平衡性,在保證記憶體可見性的前提下,盡量放松對編譯器和處理器的重排序限制

JVM對Java記憶體模型的實作

JVM中,Java記憶體模型將記憶體分為了兩部分:執行緒堆疊區和堆區,JVM中運行的每個執行緒都有自己的執行緒堆疊,執行緒堆疊包含了當前執行緒執行的方法呼叫和相關資訊,我們把它稱作呼叫堆疊,呼叫堆疊不斷變化,

img

  • 所有的原始資料型別(8大原始資料型別)的區域變數,都是直接保存在執行緒堆疊中的,他們的值各個執行緒之間是相互獨立的,無法共享,但是可以傳遞,
  • 堆區包含了Java應用創建的所有物件資訊不管物件是哪個執行緒創建的,其中的物件包括原始型別的封裝類(如Byte、Integer、Long等等),不管物件是屬于一個成員變數還是方法中的區域變數,它都會被存盤在堆區,
  • 對于一個物件的成員方法,這些方法中包含區域變數,仍需要存盤在堆疊區,即使它們所屬的物件在堆區, 對于一個物件的成員變數,不管它是原始型別還是包裝型別,都會被存盤到堆區,Static型別的變數以及類本身相關資訊都會隨著類本身存盤在堆區,

重排序

重排序型別

img

  • 編譯器優化的重排序,編譯器在不改變單執行緒程式語意的前提下,可以重新安排陳述句的執行順序,
  • 指令級并行的重排序,現代處理器采用了指令級并行技術(Instruction-LevelParallelism,ILP)來將多條指令重疊執行,如果不存在資料依賴性,處理器可以改變陳述句對應機器指令的執行順序,
  • 記憶體系統的重排序,由于處理器使用快取和讀/寫緩沖區,這使得加載和存盤操作看上去可能是在亂序執行,

as-if-serial

不管如何重排序,都必須保證代碼在單執行緒下運行正確,連單執行緒下都無法正確運行,更不用考慮多執行緒并發情況,所以提出了as-if-serial概念

as-if-serial語意的意思是:不管怎么重排序(編譯器和處理器為了提高并行度),單執行緒的執行結果不能被改變,所以編譯器不會對存在資料依賴關系的操作做重排序,因為重排序會改變執行結果(這里所說的資料依賴僅僅針對單個處理器中執行的執行序列和單個執行緒中執行的操作,不同處理器之間和不同執行緒之間的資料依賴性不能被編譯器和處理器考慮)但是,如果操作之間不存在資料依賴關系,這些操作依然可能被編譯器和處理器重排序,

volatile關鍵字

volatile關鍵字的原理和實作機制

觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的匯編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令”,lock前綴指令實際上相當于一個記憶體屏障(也成記憶體柵欄),記憶體屏障會提供3個功能:

  1. 它確保指令重排序時不會把其后面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的后面;即在執行到記憶體屏障這句指令時,在它前面的操作已經全部完成;
  2. 它會強制將對快取的修改操作立即寫入主存;
  3. 如果是寫操作,它會導致其他執行緒中(本地記憶體)對應的快取行無效,

volatile關鍵字使用場景

synchronized關鍵字是防止多個執行緒同時執行一段代碼,那么就會很影響程式執行效率,而volatile關鍵字在某些情況下性能要優于synchronized,但是要注意volatile關鍵字是無法替代synchronized關鍵字的,因為volatile關鍵字,通常來說,使用volatile必須具備以下2個條件:

  1. 對變數的寫操作不依賴于當前值
  2. 該變數不會與其他狀態變數一起納入不變形條件中
  3. 在訪問變數時不需要加鎖

實際上,這些條件表明,可以被寫入 volatile 變數的這些有效值獨立于任何程式的狀態,包括變數的當前狀態,

事實上,我的理解就是上面的2個條件需要保證操作是原子性操作,才能保證使用volatile關鍵字的程式在并發時能夠正確執行,

Happen-Before

img

用happens-before的概念來闡述操作之間的記憶體可見性,如果一個操作的結果需要對另一個操作可見,那么兩個操作之間必須要存在happens-before關系

兩個操作之間具有happens-before關系,并不意味著前一個操作必須要在后一個操作之前執行!happens-before僅僅要求前一個操作(執行的結果)對后一個操作可見,且前一個操作按順序排在第二個操作之前(the first is visible to and ordered before the second) ,

  • 程式次序規則:一個執行緒內,按照代碼順序,書寫在前面的操作先行發生于書寫在后面的操作
  • 鎖定規則:一個unLock操作先行發生于后面對同一個鎖額lock操作
  • volatile變數規則:對一個變數的寫操作先行發生于后面對這個變數的讀操作
  • 傳遞規則:如果操作A先行發生于操作B,而操作B又先行發生于操作C,則可以得出操作A先行發生于操作C
  • 執行緒啟動規則:Thread物件的start()方法先行發生于此執行緒的每個一個動作
  • 執行緒中斷規則:對執行緒interrupt()方法的呼叫先行發生于被中斷執行緒的代碼檢測到中斷事件的發生
  • 執行緒終結規則:執行緒中所有的操作都先行發生于執行緒的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的回傳值手段檢測到執行緒已經終止執行
  • 物件終結規則:一個物件的初始化完成先行發生于他的finalize()方法的開始

synchronized和volatile的區別

  • volatile本質是在告訴JVM當前變數在暫存器(作業記憶體)中的值是不確定的,需要從主存中讀取
  • volatile僅能在變數級別使用,synchronized則可以使用在變數、方法、和類級別
  • volatile僅能實作變數的修改可見性,不能保證原子性,而synchronized則可以保證變數的修改可見性和原子性
  • volatile不會造成執行緒阻塞,synchronized可能會造成執行緒阻塞
  • volatile標記的變數不會被編譯器優化,synchronized標記的變數可以被編譯器優化

單例模式

單例模式的特點

  1. 單例類只能有一個實體
  2. 單例類必須自己創建自己的唯一實體
  3. 單例類必須給所有其他物件提供這一實體

單例模式確保某個類只有一個實體,而且自行實體化并向整個系統提供這個實體,在計算機系統中,執行緒池、快取、日志物件、對話框、列印機、顯卡的驅動程式物件常被設計成單例,這些應用都或多或少具有資源管理器的功能,每臺計算機可以有若干個列印機,但只能有一個Printer Spooler,以避免兩個列印作業同時輸出到列印機中,每臺計算機可以有若干通信埠,系統應當集中管理這些通信埠,以避免一個通信埠同時被兩個請求同時呼叫,總之,選擇單例模式就是為了避免不一致狀態,避免政出多頭,

餓漢式

餓漢式在類創建的同時就已經創建好一個靜態的物件供系統使用,以后不再改變,天生是執行緒安全的,

/**
 * @author :zsy
 * @date :Created 2021/4/24 19:49
 * @description:餓漢式
 */
public class Hungry {
    //可能會浪費空間
    private Hungry() {}

    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance() {
        return HUNGRY;
    }
}

懶漢式

普通懶漢式

/**
 * @author :zsy
 * @date :Created 2021/4/24 19:51
 * @description:懶漢式
 */
public class LazyMan {

    private LazyMan() {}

    private static LazyMan LAZY_MAN = null;

    public static LazyMan getInstance() {
        if (LAZY_MAN == null) {
            LAZY_MAN = new LazyMan();
        }
        return LAZY_MAN;
    }
}

同步方法的懶漢式

/**
 * @author :zsy
 * @date :Created 2021/4/24 19:51
 * @description:懶漢式
 */
public class LazyMan {

    private LazyMan() {}

    private static LazyMan LAZY_MAN = null;

    public static synchronized LazyMan getInstance() {
        if (LAZY_MAN == null) {
            LAZY_MAN = new LazyMan();
        }
        return LAZY_MAN;
    }
}

這種寫法是對getInstance()加了鎖的處理,保證了同一時刻只能有一個執行緒訪問并獲得實體,但是缺點也很明顯,因為synchronized是修飾整個方法,每個執行緒訪問都要進行同步,而其實這個方法只執行一次實體化代碼就夠了,每次都同步方法顯然效率低下,為了改進這種寫法,就有了下面的雙重檢查懶漢式,

雙重檢查懶漢式

存在問題多執行緒下呼叫getInstance(),會產生錯誤

//雙重檢測鎖模式的懶漢式單例 DCL懶漢式
public static LazyMan getInstance() {
    if (LAZY_MAN == null) {
        synchronized (LazyMan.class) {
            if (LAZY_MAN == null) {
                LAZY_MAN = new LazyMan();
            }
        }
    }
    return LAZY_MAN;
}

這種寫法用了兩個if判斷,也就是Double-Check,并且同步的不是方法,而是代碼塊,效率較高,是對第三種寫法的改進,為什么要做兩次判斷呢?這是為了執行緒安全考慮,還是那個場景,物件還沒實體化,兩個執行緒A和B同時訪問靜態方法并同時運行到第一個if判斷陳述句,這時執行緒A先進入同步代碼塊中實體化物件,結束之后執行緒B也進入同步代碼塊,如果沒有第二個if判斷陳述句,那么執行緒B也同樣會執行實體化物件的操作了,

分析為什么需要volatile關鍵字

通過雙重檢測鎖模式的懶漢式單例模式多執行緒下仍然存在問題,因為 LAZY_MAN = new LazyMan();不是一個原子操作,而是分為三個步驟:

  1. 分配記憶體空間
  2. 執行構造方法創建物件
  3. 把這個物件指向這個空間

下圖由于指令重拍,而發生了執行緒B拿到了LAZY_MAN物件,但是LAZY_MAN為null所以需要將LAZY_MAN設定為volatile

image-20210424212047630

private volatile static LazyMan LAZY_MAN = new LazyMan();

存在問題:通過反射機制,打破封裝仍然可以創建物件

public static void main(String[] args) throws Exception {
    LazyMan instance1 = LazyMan.getInstance();
    Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
    declaredConstructor.setAccessible(true);
    LazyMan instance2 = declaredConstructor.newInstance();
    System.out.println(instance1);
    System.out.println(instance2);
}

改進措施

private LazyMan() {
    if (LAZY_MAN != null) {
        throw new RuntimeException("使用反射破壞例外");
    }
}

存在問題:可以通過兩次反射來創建物件

這次在構造方法加一個信號變數,只要創建了一次實體,就讓信號變數置為true,以后再來呼叫直接拋出例外

private static boolean isBuild = false;
private LazyMan() {
    if (!isBuild) {
        isBuild = true;
    } else {
        throw new RuntimeException("使用反射破壞例外");
    }	
}

當然這需要對信號變數進行加密,要不然仍然可以通過反射拿到信號變數,

public static void main(String[] args) throws Exception {
    //LazyMan instance1 = LazyMan.getInstance();
    Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
    declaredConstructor.setAccessible(true);
    //通過反射拿到信號變數
    Field isBuild = LazyMan.class.getDeclaredField("isBuild");
    isBuild.setAccessible(true);
    LazyMan instance1 = declaredConstructor.newInstance();
    //修改信號變數
    isBuild.set(instance1,false);
    LazyMan instance2 = declaredConstructor.newInstance();
    System.out.println(instance1);
    System.out.println(instance2);
}

完整代碼

public class LazyMan {

    private static boolean isBuild = false;

    private volatile static LazyMan INSTANCE;

    private LazyMan() {
        if (isBuild == false) {
            isBuild = true;
        } else {
            throw new RuntimeException("使用反射破壞例外");
        }

    }

    private volatile static LazyMan LAZY_MAN;

    //雙重檢測鎖模式的懶漢式單例 DCL懶漢式
    public static LazyMan getInstance() {
        if (LAZY_MAN == null) {
            synchronized (LazyMan.class) {
                if (LAZY_MAN == null) {
                    LAZY_MAN = new LazyMan();
                    isBuild = true;
                }
            }
        }
        return LAZY_MAN;
    }
}


靜態內部類(推薦)

/**
 * @author :zsy
 * @date :Created 2021/4/25 8:33
 * @description:靜態內部類
 */
public class Singleton {

    private static class LazyHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

這是很多開發者推薦的一種寫法,這種靜態內部類方式在Singleton類被裝載時并不會立即實體化,而是在需要實體化時,呼叫getInstance方法,才會裝載SingletonInstance類,從而完成物件的實體化,

同時,因為類的靜態屬性只會在第一次加載類的時候初始化,也就保證了SingletonInstance中的物件只會被實體化一次,并且這個程序也是執行緒安全的,

Enum

不能使用反射破壞列舉

/**
 * @author :zsy
 * @date :Created 2021/4/24 20:24
 * @description:列舉單例
 */
//enum本身也是一個Class
public enum EnumSingle {
    INSTANCE;

    public static EnumSingle getInstance() {
        return INSTANCE;
    }
}

class Test {
    public static void main(String[] args) throws Exception {
        EnumSingle instance1 = EnumSingle.getInstance();
        EnumSingle instance2 = EnumSingle.getInstance();

        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle enumSingle = declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(enumSingle);
    }
}

以上代碼是通過反射創建列舉的實體拋出例外Exception in thread “main” java.lang.IllegalArgumentException: Cannot reflectively create enum objects

  1. 執行緒安全問題,因為Java虛擬機在加載列舉類的時候會使用ClassLoader的方法,這個方法使用了同步代碼塊來保證執行緒安全,
  2. 避免反序列化破壞物件,因為列舉的反序列化并不通過反射實作,

總結

餓漢式和懶漢式區別

餓漢式:在類加載的時候就完成了初始化,所以類加載比較慢,但是獲取物件的速度快

懶漢式:在類加載的時候不初始化,等到第一次使用時才初始化

單例模式的優缺點

優點:

  • 單例類只有一個實體,節省了記憶體資源,對于一些需要頻繁創建銷毀的物件,使用到單例模式可以提高系統性能;
  • 單例模式可以在系統設定全域訪問點,優化和共享資料,例如Web應用的頁面計數器就可以用單例模式實作計數值的保存,

缺點:單例模式一般沒有介面,擴展的話只能修改代碼

CAS

什么是CAS

CAS全稱compareAndSwapObject,即比較替換,是實作并發應用到的一種技術,compareAndSwapObject 方法其實比較的就是兩個 Java Object 的地址,如果相等則將新的地址(Java Object)賦給該欄位,

CAS操作是一個原子操作,由一條CPU指令完成(CPU的并發原語),使用了三個基本運算元,記憶體地址V,舊的預期值A,要修改的新值B

Java Unsafe包中的compareAndSwapObject()方法

比較并且交換Java Object

 /***
   * Compares the value of the object field at the specified offset
   * in the supplied object with the given expected value, and updates
   * it if they match.  The operation of this method should be atomic,
   * thus providing an uninterruptible way of updating an object field.
   *
   * @param obj the object containing the field to modify.
   * @param offset the offset of the object field within <code>obj</code>.
   * @param expect the expected value of the field.
   * @param update the new value of the field if it equals <code>expect</code>.
   * @return true if the field was changed.
   */
  public native boolean compareAndSwapObject(Object obj, long offset, Object expect, Object update);

CAS該方法的作用是原子操作比較并交換兩個值,運用時底層硬體所提供的CAS支持,在Java API中該方法的四個引數

  1. obj:包含要修改的欄位物件
  2. offset:欄位在物件的偏移量
  3. expect:欄位的期望值
  4. update:如果該欄位的值等于欄位的期望值,用于更新欄位的新值

實體

/**
 * @author :zsy
 * @date :Created 2021/4/18 9:16
 * @description:CAS測驗
 */
public class UnsafeTest {

    private static Unsafe UNSAFE = null;
    private static long I_OFFSET;
    static {
        //Unsafe unsafe = Unsafe.getUnsafe();
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            UNSAFE = (Unsafe) field.get(null);
            I_OFFSET = UNSAFE.objectFieldOffset(Person.class.getDeclaredField("i"));
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Person person = new Person();
        new Thread(() -> {
            while (true) {
                //person.i++;
                boolean b = UNSAFE.compareAndSwapInt(person, I_OFFSET, person.i, person.i + 1);
                if (b) {
                    System.out.println(Thread.currentThread().getName() + UNSAFE.getIntVolatile(person, I_OFFSET));
                    //System.out.println(Thread.currentThread().getName() + person.i);
                }

                try {
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();
        new Thread(() -> {
            while (true) {
                //person.i++;
                boolean b = UNSAFE.compareAndSwapInt(person, I_OFFSET, person.i, person.i + 1);
                if (b) {
                    System.out.println(Thread.currentThread().getName() + UNSAFE.getIntVolatile(person, I_OFFSET));
                    //System.out.println(Thread.currentThread().getName() + person.i);
                }


                try {
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
    }
}
class Person {
    public int i;

}

CAS存在的三大問題

  • ABA問題
  • 回圈等待時間長開銷大
  • 只能保證一個共享變數的原子操作

ABA問題實作

img

樂觀鎖中詳細介紹了ABA問題

/**
 * @author :zsy
 * @date :Created 2021/4/24 21:45
 * @description:ABA問題
 */
public class ABADemo {

    private static AtomicInteger num = new AtomicInteger(1);

    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "---->" + num.get());
        }, "A").start();

        new Thread(() -> {
            num.compareAndSet(1, 21);
            System.out.println(Thread.currentThread().getName() + "修改了num:" + num.get());
        }, "B").start();

        new Thread(() -> {
            num.compareAndSet(21, 1);
            System.out.println(Thread.currentThread().getName() + "修改了num:" + num.get());
        }, "C").start();

        new Thread(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "---->" + num.get());
        }, "A").start();
    }
}
A---->1
B修改了num:21
C修改了num:1
A---->1

ABA問題解決

引入原子運用

/**
 * @author :zsy
 * @date :Created 2021/4/24 21:45
 * @description:ABA問題
 */
public class ABADemo {

    static AtomicStampedReference<Integer> num = new AtomicStampedReference<Integer>(1,1);

    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "版本號" + num.getStamp() + "---->" + num.getReference());
        }, "A").start();

        new Thread(() -> {
            try {
                Thread.sleep(11);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            num.compareAndSet(1, 21, num.getStamp(), num.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "版本號" + num.getStamp() + "修改了num:" + num.getReference());
        }, "B").start();

        new Thread(() -> {
            try {
                Thread.sleep(31);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            num.compareAndSet(21, 1, num.getStamp(), num.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "版本號" + num.getStamp() + "修改了num:" + num.getReference());
        }, "C").start();

        new Thread(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+ "版本號" + num.getStamp() + "---->" + num.getReference());
        }, "A").start();
    }
}

輸出
A版本號1---->1
B版本號2修改了num:21
C版本號3修改了num:1
A版本號3---->1

并發編程面試題

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/280744.html

標籤:java

上一篇:從零開始的java面試題(【2021】1.java基礎篇)

下一篇:大一學生的禿頭行為

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more