1. 程式人生 > 實用技巧 >關於多執行緒、執行緒池、執行緒安全問題

關於多執行緒、執行緒池、執行緒安全問題

多執行緒

1、基礎概念

1.1 多執行緒技術

  • 從軟體或者硬體上實現同時執行多個任務
  • 具有多執行緒能攔的計算機因有硬體支援而能夠在同一時間執行多個執行緒
  • 多執行緒程式設計常常也將其稱之為併發程式設計

1.2 併發和並行

  • 並行
    • 在同一時刻,有多個指令在多個CPU上同時進行
  • 併發
    • 在同一時刻,有多個指令在單個CPU上交替進行

1.3 程序和執行緒

  • 程序:是正在執行的軟體,且一個程序最少包括一個執行緒
    • 獨立性:程序是一個可以獨立執行的基本單位,也是作業系統排程的最小單元,同時也是系統分配資源和排程的獨立單位
    • 動態性:程序的實質是程式的一次執行過程,程序是動態產生,動態消亡的
    • 併發性:任何程序都可以同其他程序一起併發執行
  • 執行緒:是程序中的單個順序控制流,是一條執行路徑
    • 單執行緒:一個程序如果只有一條執行路徑,則稱為單執行緒程式
    • 多執行緒:一個程式如果有多條執行路徑,則稱為多執行緒程式
  • 二者關係
    • 執行緒是程序中的單個順序控制流,是依賴於程序的,而一個程序最少包括一個執行緒,程序可以存在很多工,每一個任務就是一個執行緒,這些執行路徑之間沒有任何的關聯關係

2、執行緒的實現

2.1 繼承Thread類

  • Thread類
  • 方法介紹
方法名說明
void run() 線上程開啟後,此方法將被呼叫執行
void start() 使此執行緒開始執行,Java虛擬機器會呼叫run方法()
  • 實現步驟
    • 定義一個類繼承自Thread類,這裡我們定義這個類為MyThread
    • 重寫Thread類中的run方法(run方法中重寫的就是要被執行緒所執行的程式碼)
    • 建立MyThread類的物件
    • 啟動執行緒(呼叫start方法)
  • 注意:多執行緒的執行,具有隨機性
  • 程式碼實現
public class MyThread extends Thread {
    @Override
    public void run() {
        for(int i=0; i<100; i++) {
            System.out.println(i);
        }
    }
}
public class MyThreadDemo {
    public static void main(String[] args) {
        MyThread my1 = new MyThread();
        MyThread my2 = new MyThread();

//        my1.run();
//        my2.run();

        //void start() 導致此執行緒開始執行; Java虛擬機器呼叫此執行緒的run方法
        my1.start();
        my2.start();
    }
}
  • 兩個問題
    • 為什麼重寫run方法
      • 因為run方法封裝的就是要被執行緒執行的程式碼
    • run方法和start方法的區別
      • run():封裝執行緒執行的程式碼,直接呼叫,相當於普通方法的呼叫
      • start():啟動執行緒;然後由JVM呼叫此執行緒的run()方法
      • start方法只能呼叫一次,如果呼叫多次程式將報錯

2.2 實現Runnable介面

  • Thread構造方法
方法名說明
Thread(Runnable target) 分配一個新的Thread物件
Thread(Runnable target, String name) 分配一個新的Thread物件
  • 實現步驟
    • 定義一個類實現Runnable介面,這裡我們定義為MyRunnable
    • 在MyRunnable類中重寫run()方法
    • 建立MyRunnable類的物件
    • 建立Thread類的物件,把MyRunnable類物件作為構造方法的引數
    • 啟動執行緒
  • 程式碼實現
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for(int i=0; i<100; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
public class MyRunnableDemo {
    public static void main(String[] args) {
        //建立MyRunnable類的物件
        MyRunnable my = new MyRunnable();

        //建立Thread類的物件,把MyRunnable物件作為構造方法的引數
        //Thread(Runnable target)
//        Thread t1 = new Thread(my);
//        Thread t2 = new Thread(my);
        //Thread(Runnable target, String name)
        Thread t1 = new Thread(my,"坦克");
        Thread t2 = new Thread(my,"飛機");

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

2.3 實現Callable類

  • 方法介紹
方法名說明
V call() 計算結果,如果無法計算結果,則丟擲一個異常
FutureTask(Callable callable) 建立一個 FutureTask,一旦執行就執行給定的 Callable
V get() 如有必要,等待計算完成,然後獲取其結果
  • 實現步驟
    • 定義一個類實現Callable介面,這裡我們將這個類定義為MyCallable
    • 在MyCallable類中重寫call()方法
    • 建立MyCallable類的物件
    • 建立Future的實現類FutureTask物件,把MyCallable物件作為構造方法的引數
    • 建立Thread類的物件,把FutureTask物件作為構造方法的引數
    • 啟動執行緒
    • 呼叫get方法,就可以獲取執行緒結束之後的結果
  • 程式碼實現
public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 100; i++) {
            System.out.println("跟女孩表白" + i);
        }
        //返回值就表示執行緒執行完畢之後的結果
        return "答應";
    }
}
public class Demo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //執行緒開啟之後需要執行裡面的call方法
        MyCallable mc = new MyCallable();
        //Thread t1 = new Thread(mc);
        //可以獲取執行緒執行完畢之後的結果.也可以作為引數傳遞給Thread物件
        FutureTask<String> ft = new FutureTask<>(mc);

        //建立執行緒物件
        Thread t1 = new Thread(ft);
        String s = ft.get();
        //開啟執行緒
        t1.start();
        //String s = ft.get();
        System.out.println(s);
    }
}

2.4 三種實現方式的區別

  • 站在返回值的角度
    • 繼承Thread類和實現Runnable介面的方式沒有返回值,獲取不到執行緒執行的結果
    • 實現Callable介面的方式有返回值,所以可以獲取執行緒執行的結果
  • 站在繼承方式的角度
    • 繼承Thread類的方式,程式設計比較簡單,可以直接使用Thread類的方法,但同時它的擴充套件性相對較弱
    • 實現Runnable或Callable介面,因為在Java中一個類可以實現多個介面,因此擴充套件性較高,但同時程式設計相對複雜,不能直接使用Thread類中的方法

3、Thread類的API

3.1 和執行緒名稱相關

  • 方法介紹
方法名說明
void setName(String name) 設定執行緒名稱
public final String getName( ) 獲取執行緒名稱
Thread currentThread() 返回對當前正在執行的執行緒物件的引用
  • 程式碼實現
public class MyThread extends Thread {
    public MyThread() {}
    public MyThread(String name) {
        super(name);
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName()+":"+i);
        }
    }
}
public class MyThreadDemo {
    public static void main(String[] args) {
        MyThread my1 = new MyThread();
        MyThread my2 = new MyThread();

        //void setName(String name):將此執行緒的名稱更改為等於引數 name
        my1.setName("高鐵");
        my2.setName("飛機");

        //Thread(String name)
        MyThread my1 = new MyThread("高鐵");
        MyThread my2 = new MyThread("飛機");

        my1.start();
        my2.start();

        //static Thread currentThread() 返回對當前正在執行的執行緒物件的引用
        System.out.println(Thread.currentThread().getName());
    }
}

3.2 執行緒休眠

  • 相關方法
方法名說明
static native void sleep (long millis) 使當前正在執行的執行緒停留(暫停執行)指定的毫秒數
  • 程式碼實現
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
    }
}
public class Demo {
    public static void main(String[] args) throws InterruptedException {
        /*System.out.println("睡覺前");
        Thread.sleep(3000);
        System.out.println("睡醒了");*/

        MyRunnable mr = new MyRunnable();

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

        t1.start();
        t2.start();
    }
}

3.3 執行緒優先順序

3.3.1 執行緒排程

  • 兩種排程方式
    • 分時排程模型
      • 所有執行緒輪流使用CPU的使用權,平均分配每個執行緒佔用CPU的時間片
    • 搶佔式排程模型
      • 優先順序高的執行緒優先使用CPU,如果優先順序相同,那麼隨機選擇一個,優先順序高的執行緒獲取的CPU時間片相對多一些
  • JAVA使用的是搶佔式排程模型
  • 隨機性
    • 假如計算機只有一個CPU ,那麼某一時刻只能執行一條指令,執行緒只有得到CPU時間片,也就是使用權,才可以執行指令。所以說多執行緒程式的執行是隨機性的,因為誰搶到CPU的使用權是不一定的

3.3.2 相關方法

方法名說明
final int getPriority() 返回此執行緒的優先順序
final void setPriority(int newPriority) 更改此執行緒的優先順序執行緒預設優先順序是5;執行緒優先順序的範圍是:1-10
  • 程式碼實現
public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
        return "執行緒執行完畢了";
    }
}
public class Demo {
    public static void main(String[] args) {
        //優先順序: 1 - 10 預設值:5
        MyCallable mc = new MyCallable();

        FutureTask<String> ft = new FutureTask<>(mc);

        Thread t1 = new Thread(ft);
        t1.setName("飛機");
        t1.setPriority(10);
        //System.out.println(t1.getPriority());//5
        t1.start();

        MyCallable mc2 = new MyCallable();

        FutureTask<String> ft2 = new FutureTask<>(mc2);

        Thread t2 = new Thread(ft2);
        t2.setName("坦克");
        t2.setPriority(1);
        //System.out.println(t2.getPriority());//5
        t2.start();
    }
}

3.4 守護執行緒

  • Java語言中的執行緒可以分為普通執行緒和守護執行緒
  • 守護執行緒的作用
    • 為普通執行緒服務,如果普通執行緒結束,守護執行緒也會結束
    • JVM會檢查執行緒的型別,如果當前的JVM程序中所有的執行緒都是守護執行緒,Jvm停止執行。
  • 相關方法
方法名說明
void setDaemon(boolean on) 將此執行緒標記為守護執行緒
  • 程式碼實現
public class MyThread1 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName() + "---" + i);
        }
    }
}
public class MyThread2 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + "---" + i);
        }
    }
}
public class Demo {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        MyThread2 t2 = new MyThread2();

        t1.setName("女神");
        t2.setName("備胎");

        //把第二個執行緒設定為守護執行緒
        //當普通執行緒執行完之後,那麼守護執行緒也沒有繼續執行下去的必要了.
        t2.setDaemon(true);

        t1.start();
        t2.start();
    }
}

4、執行緒安全問題

4.1 資料安全問題

  • 安全問題出現的條件
    • 是多執行緒環境
    • 有共享資料
    • 有多條語句操作共享資料
  • 如何解決多執行緒安全問題
    • 基本思想
      • 讓程式沒有安全問題的環境
    • 如何實現
      • 把多條語句操作共享資料的程式碼鎖起來,讓任意時刻只能有一個執行緒執行即可
  • 鎖在Java中存在兩種
    • 內部鎖
      • 就是通過synchronized進行實現
    • 顯式鎖
      • 是Jdk1.5之後提供的Lock物件進行實現
  • 內部鎖的實現
    • 同步程式碼塊
    • 同步方法
    • 靜態同步方法
  • 顯式鎖
    • 建立Lock類的物件

4.2 同步程式碼塊

  • 格式
    • synchronized(任意物件) { 多條語句操作共享資料的程式碼 }
  • synchronized(任意物件)
    • 就相當於給程式碼加鎖,任意物件就能看成是一把鎖
  • 同步的好處和弊端
    • 好處
      • 解決了多執行緒的資料安全問題
    • 弊端
      • 當執行緒很多時,因為每個執行緒都會判斷同步上的鎖,這會很耗費資源,降低程式執行效率
  • 程式碼實現
public class SellTicket implements Runnable {
    private int tickets = 100;
    private Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) { // 對可能有安全問題的程式碼加鎖,多個執行緒必須使用同一把鎖
                //t1進來後,就會把這段程式碼給鎖起來
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                        //t1休息100毫秒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //視窗1正在出售第100張票
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "張票");
                    tickets--; //tickets = 99;
                }
            }
            //t1出來了,這段程式碼的鎖就被釋放了
        }
    }
}

public class SellTicketDemo {
    public static void main(String[] args) {
        SellTicket st = new SellTicket();

        Thread t1 = new Thread(st, "視窗1");
        Thread t2 = new Thread(st, "視窗2");
        Thread t3 = new Thread(st, "視窗3");

        t1.start();
        t2.start();
        t3.start();
    }
}
  • 注意
    • 多個執行緒一定要使用同一把鎖

4.3 同步方法

  • 同步方法
    • 就是把synchronized關鍵字加到方法上
  • 格式
    • 修飾符 synchronized 返回值型別 方法名(方法引數) { 方法體;}
  • 同步方法的鎖物件
    • 就是this
  • 同步靜態方法
    • 修飾符 static synchronized 返回值型別 方法名(方法引數) { 方法體;}
  • 同步靜態方法的鎖物件
    • 就是類名.class
  • 程式碼實現
public class Demo {
      public static void main(String[] args) {
          MyRunnable mr = new MyRunnable();
          Thread t1 = new Thread(mr);
          Thread t2 = new Thread(mr);

          t1.setName("視窗一");
          t2.setName("視窗二");

          t1.start();
          t2.start(); 
      }  
}

class MyRunnable implements Runnable {
    private static int ticketCount = 100;

    @Override
    public void run() {
        while(true){
            if("視窗一".equals(Thread.currentThread().getName())){
                //同步方法
                boolean result = synchronizedMthod();
                if(result){
                    break;
                }
            }

            if("視窗二".equals(Thread.currentThread().getName())){
                //同步程式碼塊
                synchronized (MyRunnable.class){
                    if(ticketCount == 0){
                       break;
                    }else{
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        ticketCount--;
                        System.out.println(Thread.currentThread().getName() + "在賣票,還剩下" + ticketCount + "張票");
                    }
                }
            }

        }
    }

    private static synchronized boolean synchronizedMthod() {
        if(ticketCount == 0){
            return true;
        }else{
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticketCount--;
            System.out.println(Thread.currentThread().getName() + "在賣票,還剩下" + ticketCount + "張票");
            return false;
        }
    }
}

4.4 Lock鎖

  • 雖然我們可以理解同步程式碼塊和同步方法的鎖物件問題,但是我們並沒有直接看到在哪裡加上了鎖,在哪裡釋放了鎖,為了更清晰的表達如何加鎖和釋放鎖,JDK5以後提供了一個新的鎖物件Lock
  • Lock是介面不能直接例項化,這裡採用它的實現類ReentrantLock來例項化
  • 多個執行緒物件需要使用同一個ReentrantLock物件
  • ReentrantLock構造方法
方法名說明
ReentrantLock() 建立一個ReentrantLock的例項
  • 加鎖解鎖方法
方法名說明
void lock() 獲得鎖
void unlock() 釋放鎖
  • 程式碼實現
public class Ticket implements Runnable {
      //票的數量
      private int ticket = 100;
      private Object obj = new Object();
      private ReentrantLock lock = new ReentrantLock();

      @Override
      public void run() {
          while (true) {
              //synchronized (obj){//多個執行緒必須使用同一把鎖.
              try {
                  lock.lock();
                  if (ticket <= 0) {
                      //賣完了
                      break;
                  } else {
                      Thread.sleep(100);
                      ticket--;
                      System.out.println(Thread.currentThread().getName() + "在賣票,還剩下" + ticket + "張票");
                  }
              } catch (InterruptedException e) {
                  e.printStackTrace();
              } finally {
                  lock.unlock();
              }
              // }
          }
      }
  }

  public class Demo {
      public static void main(String[] args) {
          Ticket ticket = new Ticket();

          Thread t1 = new Thread(ticket);
          Thread t2 = new Thread(ticket);
          Thread t3 = new Thread(ticket);

          t1.setName("視窗一");
          t2.setName("視窗二");
          t3.setName("視窗三");

          t1.start();
          t2.start();
          t3.start();
      }
  }

5、死鎖

  • 概述
    • 執行緒死鎖是指由於兩個或多個執行緒互相持有對方所需要的資源,導致這些執行緒處於等待狀態,無法前往執行
  • 什麼情況下會產生死鎖
    • 交叉式鎖可能導致程式死鎖
      • 執行緒A持有R1的鎖等待獲取R2鎖,執行緒B持有R2的鎖等待獲取R1的鎖。
    • 記憶體不足
      • 當併發請求系統可用記憶體時,如果此時系統記憶體不足,則可能會出現死鎖的情況。舉例:兩個執行緒T1和T2,執行某個任務,其中T1已經獲取到了10MB記憶體,T2獲取到了20MB記憶體,如果每一個執行緒執行單元都需要30MB,但是剩餘的可用記憶體剛好5MB,那麼兩個執行緒有可能都在等待彼此能夠釋放記憶體資源。
    • 死迴圈引起的死鎖
      • 程式由於程式碼原因或者對某些異常處理不得當,進入了死迴圈。CPU的佔有率居高不下,但是程式就是不工作。這種死鎖一般稱之為假死,是一種最為致命也是最難排查的死鎖現象。
  • 交叉式鎖程式碼實現
public class Demo {
    public static void main(String[] args) {
        Object objA = new Object();
        Object objB = new Object();

        new Thread(()->{
            while(true){
                synchronized (objA){
                    //執行緒一
                    synchronized (objB){
                        System.out.println("小康同學正在走路");
                    }
                }
            }
        }).start();

        new Thread(()->{
            while(true){
                synchronized (objB){
                    //執行緒二
                    synchronized (objA){
                        System.out.println("小薇同學正在走路");
                    }
                }
            }
        }).start();
    }
}

6、生產者消費者模式

6.1 概述

  • 生產者消費者模式是一個十分經典的多執行緒協作的模式
  • 所謂生產者消費者問題,實際上主要是包含了兩類執行緒:
    • 一類是生產者執行緒用於生產資料
    • 一類是消費者執行緒用於消費資料
  • 為了解生產者和消費者的關係,通常會採用共享的資料區域,就像是一個倉庫
  • 生產者生產資料之後直接放置在共享資料區中,並不需要關心消費者的行為
  • 消費者只需要從共享資料區中去獲取資料,並不需要關心生產者的行為

6.2 Object類的等待和喚醒方法

方法名說明
void wait() 導致當前執行緒等待,直到另一個執行緒呼叫該物件的 notify()方法或 notifyAll()方法
void notify() 喚醒正在等待物件監視器的單個執行緒
void notifyAll() 喚醒正在等待物件監視器的所有執行緒

6.3 生產者和消費者案例

  • 案例需求
    • 桌子類(Desk):定義表示包子數量的變數,定義鎖物件變數,定義標記桌子上有無包子的變數
    • 生產者類(Cooker):實現Runnable介面,重寫run()方法,設定執行緒任務
      1. 判斷是否有包子,決定當前執行緒是否執行
      2. 如果有包子,就進入等待狀態,如果沒有包子,繼續執行,生產包子
      3. 生產包子之後,更新桌子上包子狀態,喚醒消費者消費包子
    • 消費者類(Foodie):實現Runnable介面,重寫run()方法,設定執行緒任務
      1. 判斷是否有包子,決定當前執行緒是否執行
      2. 如果沒有包子,就進入等待狀態,如果有包子,就消費包子
      3. 消費包子後,更新桌子上包子狀態,喚醒生產者生產包子
    • 測試類(Demo):裡面有main方法,main方法中的程式碼步驟如下
      • 建立生產者執行緒和消費者執行緒物件
      • 分別開啟兩個執行緒
  • 程式碼實現
public class Desk {

    //定義一個標記
    //true 就表示桌子上有漢堡包的,此時允許吃貨執行
    //false 就表示桌子上沒有漢堡包的,此時允許廚師執行
    public static boolean flag = false;

    //漢堡包的總數量
    public static int count = 10;

    //鎖物件
    public static final Object lock = new Object();
}

public class Cooker extends Thread {
//    生產者步驟:
//            1,判斷桌子上是否有漢堡包
//    如果有就等待,如果沒有才生產。
//            2,把漢堡包放在桌子上。
//            3,叫醒等待的消費者開吃。
    @Override
    public void run() {
        while(true){
            synchronized (Desk.lock){
                if(Desk.count == 0){
                    break;
                }else{
                    if(!Desk.flag){
                        //生產
                        System.out.println("廚師正在生產漢堡包");
                        Desk.flag = true;
                        Desk.lock.notifyAll();
                    }else{
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

public class Foodie extends Thread {
    @Override
    public void run() {
//        1,判斷桌子上是否有漢堡包。
//        2,如果沒有就等待。
//        3,如果有就開吃
//        4,吃完之後,桌子上的漢堡包就沒有了
//                叫醒等待的生產者繼續生產
//        漢堡包的總數量減一

        //套路:
            //1. while(true)死迴圈
            //2. synchronized 鎖,鎖物件要唯一
            //3. 判斷,共享資料是否結束. 結束
            //4. 判斷,共享資料是否結束. 沒有結束
        while(true){
            synchronized (Desk.lock){
                if(Desk.count == 0){
                    break;
                }else{
                    if(Desk.flag){
                        //有
                        System.out.println("吃貨在吃漢堡包");
                        Desk.flag = false;
                        Desk.lock.notifyAll();
                        Desk.count--;
                    }else{
                        //沒有就等待
                        //使用什麼物件當做鎖,那麼就必須用這個物件去呼叫等待和喚醒的方法.
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        /*消費者步驟:
        1,判斷桌子上是否有漢堡包。
        2,如果沒有就等待。
        3,如果有就開吃
        4,吃完之後,桌子上的漢堡包就沒有了
                叫醒等待的生產者繼續生產
        漢堡包的總數量減一*/

        /*生產者步驟:
        1,判斷桌子上是否有漢堡包
        如果有就等待,如果沒有才生產。
        2,把漢堡包放在桌子上。
        3,叫醒等待的消費者開吃。*/
        Foodie f = new Foodie();
        Cooker c = new Cooker();
        f.start();
        c.start();

    }
}

6.4 案例優化

  • 需求
    • 將Desk類中的變數,採用面向物件的方式封裝起來
    • 生產者和消費者類中構造方法接收Desk類物件,之後在run方法中進行使用
    • 建立生產者和消費者執行緒物件,構造方法中傳入Desk類物件
    • 開啟兩個執行緒
  • 程式碼實現
public class Desk {
    //定義一個標記
    //true 就表示桌子上有漢堡包的,此時允許吃貨執行
    //false 就表示桌子上沒有漢堡包的,此時允許廚師執行
    //public static boolean flag = false;
    private boolean flag;

    //漢堡包的總數量
    //public static int count = 10;
    //以後我們在使用這種必須有預設值的變數
   // private int count = 10;
    private int count;

    //鎖物件
    //public static final Object lock = new Object();
    private final Object lock = new Object();

    public Desk() {
        this(false,10); // 在空參內部呼叫帶參,對成員變數進行賦值,之後就可以直接使用成員變量了
    }

    public Desk(boolean flag, int count) {
        this.flag = flag;
        this.count = count;
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

    public Object getLock() {
        return lock;
    }

    @Override
    public String toString() {
        return "Desk{" +
                "flag=" + flag +
                ", count=" + count +
                ", lock=" + lock +
                '}';
    }
}

public class Cooker extends Thread {

    private Desk desk;

    public Cooker(Desk desk) {
        this.desk = desk;
    }
//    生產者步驟:
//            1,判斷桌子上是否有漢堡包
//    如果有就等待,如果沒有才生產。
//            2,把漢堡包放在桌子上。
//            3,叫醒等待的消費者開吃。

    @Override
    public void run() {
        while(true){
            synchronized (desk.getLock()){
                if(desk.getCount() == 0){
                    break;
                }else{
                    //System.out.println("驗證一下是否執行了");
                    if(!desk.isFlag()){
                        //生產
                        System.out.println("廚師正在生產漢堡包");
                        desk.setFlag(true);
                        desk.getLock().notifyAll();
                    }else{
                        try {
                            desk.getLock().wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

public class Foodie extends Thread {
    private Desk desk;

    public Foodie(Desk desk) {
        this.desk = desk;
    }
    @Override
    public void run() {
//        1,判斷桌子上是否有漢堡包。
//        2,如果沒有就等待。
//        3,如果有就開吃
//        4,吃完之後,桌子上的漢堡包就沒有了
//                叫醒等待的生產者繼續生產
//        漢堡包的總數量減一

        //套路:
            //1. while(true)死迴圈
            //2. synchronized 鎖,鎖物件要唯一
            //3. 判斷,共享資料是否結束. 結束
            //4. 判斷,共享資料是否結束. 沒有結束
        while(true){
            synchronized (desk.getLock()){
                if(desk.getCount() == 0){
                    break;
                }else{
                    //System.out.println("驗證一下是否執行了");
                    if(desk.isFlag()){
                        //有
                        System.out.println("吃貨在吃漢堡包");
                        desk.setFlag(false);
                        desk.getLock().notifyAll();
                        desk.setCount(desk.getCount() - 1);
                    }else{
                        //沒有就等待
                        //使用什麼物件當做鎖,那麼就必須用這個物件去呼叫等待和喚醒的方法.
                        try {
                            desk.getLock().wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        /*消費者步驟:
        1,判斷桌子上是否有漢堡包。
        2,如果沒有就等待。
        3,如果有就開吃
        4,吃完之後,桌子上的漢堡包就沒有了
                叫醒等待的生產者繼續生產
        漢堡包的總數量減一*/

        /*生產者步驟:
        1,判斷桌子上是否有漢堡包
        如果有就等待,如果沒有才生產。
        2,把漢堡包放在桌子上。
        3,叫醒等待的消費者開吃。*/
        Desk desk = new Desk();
        Foodie f = new Foodie(desk);
        Cooker c = new Cooker(desk);
        f.start();
        c.start();
    }
}

7、阻塞佇列

  • 常見BlockingQueue
    • ArrayBlockingQueue: 底層是陣列,有界
    • LinkedBlockingQueue: 底層是連結串列,無界.但不是真正的無界,最大為int的最大值
  • BlockingQueue的核心方法
方法名說明
put(anObject) 將引數放入佇列,如果放不進去會阻塞
take() 取出第一個資料,取不到會阻塞
  • 程式碼實現
public class Demo02 {
    public static void main(String[] args) throws Exception {
        // 建立阻塞佇列的物件,容量為 1
        ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1);
        // 儲存元素
        arrayBlockingQueue.put("漢堡包");
        // 取元素
        System.out.println(arrayBlockingQueue.take());
        System.out.println(arrayBlockingQueue.take()); // 取不到會阻塞

        System.out.println("程式結束了");
    }
}
  • 使用阻塞佇列實現等待喚醒機制
public class Cooker extends Thread {

    private ArrayBlockingQueue<String> bd;

    public Cooker(ArrayBlockingQueue<String> bd) {
        this.bd = bd;
    }
//    生產者步驟:
//            1,判斷桌子上是否有漢堡包
//    如果有就等待,如果沒有才生產。
//            2,把漢堡包放在桌子上。
//            3,叫醒等待的消費者開吃。

    @Override
    public void run() {
        while (true) {
            try {
                bd.put("漢堡包");
                System.out.println("廚師放入一個漢堡包");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Foodie extends Thread {
    private ArrayBlockingQueue<String> bd;

    public Foodie(ArrayBlockingQueue<String> bd) {
        this.bd = bd;
    }

    @Override
    public void run() {
//        1,判斷桌子上是否有漢堡包。
//        2,如果沒有就等待。
//        3,如果有就開吃
//        4,吃完之後,桌子上的漢堡包就沒有了
//                叫醒等待的生產者繼續生產
//        漢堡包的總數量減一

        //套路:
        //1. while(true)死迴圈
        //2. synchronized 鎖,鎖物件要唯一
        //3. 判斷,共享資料是否結束. 結束
        //4. 判斷,共享資料是否結束. 沒有結束
        while (true) {
            try {
                String take = bd.take();
                System.out.println("吃貨將" + take + "拿出來吃了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

public class Demo {
    public static void main(String[] args) {
        ArrayBlockingQueue<String> bd = new ArrayBlockingQueue<>(1);

        Foodie f = new Foodie(bd);
        Cooker c = new Cooker(bd);

        f.start();
        c.start();
    }
}

8、執行緒池

8.1 執行緒狀態介紹

  • 當執行緒被建立並啟動以後,它既不是一啟動就進入了執行狀態,也不是一直處於執行狀態。執行緒物件在不同的時期有不同的狀態。那麼Java中的執行緒存在哪幾種狀態呢?Java中的執行緒狀態被定義在了java.lang.Thread.State列舉類中,State列舉類的原始碼如下:
public class Thread {
    
    public enum State {
    
        /* 新建 */
        NEW , 

        /* 可執行狀態 */
        RUNNABLE , 

        /* 阻塞狀態 */
        BLOCKED , 

        /* 無限等待狀態 */
        WAITING , 

        /* 計時等待 */
        TIMED_WAITING , 

        /* 終止 */
        TERMINATED;
	}
    // 獲取當前執行緒的狀態
    public State getState() {
        return jdk.internal.misc.VM.toThreadState(threadStatus);
    }
    
}
  • 通過原始碼我們可以看到Java中的執行緒存在6種狀態,每種執行緒狀態的含義如下
執行緒狀態具體含義
NEW 一個尚未啟動的執行緒的狀態。也稱之為初始狀態、開始狀態。執行緒剛被建立,但是並未啟動。還沒呼叫start方法。MyThread t = new MyThread()只有執行緒象,沒有執行緒特徵。
RUNNABLE 當我們呼叫執行緒物件的start方法,那麼此時執行緒物件進入了RUNNABLE狀態。那麼此時才是真正的在JVM程序中建立了一個執行緒,執行緒一經啟動並不是立即得到執行,執行緒的執行與否要聽令與CPU的排程,那麼我們把這個中間狀態稱之為可執行狀態(RUNNABLE)也就是說它具備執行的資格,但是並沒有真正的執行起來而是在等待CPU的度。
BLOCKED 當一個執行緒試圖獲取一個物件鎖,而該物件鎖被其他的執行緒持有,則該執行緒進入Blocked狀態;當該執行緒持有鎖時,該執行緒將變成Runnable狀態。
WAITING 一個正在等待的執行緒的狀態。也稱之為等待狀態。造成執行緒等待的原因有兩種,分別是呼叫Object.wait()、join()方法。處於等待狀態的執行緒,正在等待其他執行緒去執行一個特定的操作。例如:因為wait()而等待的執行緒正在等待另一個執行緒去呼叫notify()或notifyAll();一個因為join()而等待的執行緒正在等待另一個執行緒結束。
TIMED_WAITING 一個在限定時間內等待的執行緒的狀態。也稱之為限時等待狀態。造成執行緒限時等待狀態的原因有三種,分別是:Thread.sleep(long),Object.wait(long)、join(long)。
TERMINATED 一個完全執行完成的執行緒的狀態。也稱之為終止狀態、結束狀態
  • 各個狀態的轉換,如下圖所示

8.2 執行緒池-基本原理

  • 概述
    • 提到池,大家應該能想到的就是水池。水池就是一個容器,在該容器中儲存了很多的水。那麼什麼是執行緒池呢?執行緒池也是可以看做成一個池子,在該池子中儲存很多個執行緒。
  • 執行緒池存在的意義
    • 系統建立一個執行緒的成本是比較高的,因為它涉及到與作業系統互動,當程式中需要建立大量生存期很短暫的執行緒時,頻繁的建立和銷燬執行緒對系統的資源消耗有可能大於業務處理時對系統資源的消耗,這樣就有點"捨本逐末"了。針對這一種情況,為了提高效能,我們就可以採用執行緒池。執行緒池在啟動的時候,會建立大量空閒執行緒,當我們向執行緒池提交任務的時,執行緒池就會啟動一個執行緒來執行該任務。等待任務執行完畢以後,執行緒並不會死亡,而是再次返回到執行緒池中稱為空閒狀態。等待下一次任務的執行。

8.3 執行緒池的設計思路

  1. 準備一個任務容器
  2. 一次性啟動多個(2個及以上)消費者執行緒
  3. 剛開始任務容器是空的,所以執行緒都在wait
  4. 直到一個外部執行緒向這個任務容器中扔了一個"任務",就會有一個消費者執行緒被喚醒
  5. 這個消費者執行緒取出"任務",並且執行這個任務,執行完畢後,繼續等待下一次任務的到來

8.4 Executors預設執行緒池

8.4.1 概述

  • JDK對執行緒池也進行了相關的實現,在真正企業開發中很少去自定義執行緒池,而是使用JDK自帶的執行緒池

8.4.2 使用Executors提供的靜態方法建立執行緒池

方法說明
static ExecutorService newCachedThreadPool() 建立一個預設的執行緒池
static newFixedThreadPool(int nThreads) 建立一個指定最多執行緒數量的執行緒池
  • 程式碼實現
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyThreadPoolDemo {
    public static void main(String[] args) throws InterruptedException {

        //1,建立一個預設的執行緒池物件.池子中預設是空的.預設最多可以容納int型別的最大值.
        ExecutorService executorService = Executors.newCachedThreadPool();
        //Executors --- 可以幫助我們建立執行緒池物件
        //ExecutorService --- 可以幫助我們控制執行緒池

        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName() + "在執行了");
        });

        //Thread.sleep(2000);

        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName() + "在執行了");
        });

        executorService.shutdown();
    }
}

8.4.3 使用Executors提供的靜態方法建立指定數量的執行緒池

方法說明
static ExecutorService newFixedThreadPool(int nThreads) 建立一個指定最多執行緒數量的執行緒池
  • 程式碼實現
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class MyThreadPoolDemo2 {
    public static void main(String[] args) {
        //引數不是初始值而是最大值
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        ThreadPoolExecutor pool = (ThreadPoolExecutor) executorService;
        System.out.println(pool.getPoolSize());//0

        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName() + "在執行了");
        });

        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName() + "在執行了");
        });

        System.out.println(pool.getPoolSize());//2
//        executorService.shutdown();
    }
}

8.5 ThreadPoolExecutor建立執行緒池物件

8.5.1 構造方法引數

  • 構造方法
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) 
  • 建立執行緒池
    • ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(核心執行緒數量,最大執行緒數量,空閒執行緒最大存活時間,時間單位,任務佇列,建立執行緒工廠,任務的拒絕策略)
    • 這裡將引數用中文表達,方便理解
  • 引數詳解
引數含義要求
引數1 核心執行緒數量 不能小於0
引數2 最大執行緒數量 不能小於等於0,最大數量>=核心執行緒數量
引數3 空閒執行緒最大存活時間 不能小於0
引數4 時間單位 時間單位
引數5 任務佇列 不能為null
引數6 建立執行緒工廠 不能為null
引數7 任務的拒絕策略 不能為null
  • 程式碼實現
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MyThreadPoolDemo3 {
//    引數一:核心執行緒數量
//    引數二:最大執行緒數
//    引數三:空閒執行緒最大存活時間
//    引數四:時間單位
//    引數五:任務佇列
//    引數六:建立執行緒工廠
//    引數七:任務的拒絕策略
    public static void main(String[] args) {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(2,5,2,TimeUnit.SECONDS,new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());

        pool.shutdown();
    }
}

8.5.2 執行緒池的原理

  • 當我們通過submit方法向執行緒池中提交任務的時候,具體的工作流程如下
    1. 客戶端每次提交一個任務,執行緒池就會在核心執行緒池中建立一個工作執行緒來執行這個任務,當核心執行緒池中的執行緒已滿時,進入下一步操作
    2. 把任務試圖儲存到工作佇列中,如果工作佇列沒有滿,則將新提交的任務儲存在這個任務佇列裡,等待核心執行緒中的空閒執行緒來執行,如果工作佇列滿了,則進入下一個流程
    3. 執行緒池會再次在非核心執行緒池區域來建立新的工作執行緒來執行任務,直到當前執行緒池匯流排程數量超過最大執行緒數量時,則會按照指定的任務處理策略處理多餘的任務

8.5.3 任務拒絕策略

  • RejectedExecutionHandler是jdk提供的一個任務拒絕策略介面,它下面存在4個子類。
類名作用
ThreadPoolExecutor.AbortPolicy 丟棄任務並丟擲RejectedExecutionException異常。是預設的策略
ThreadPoolExecutor.DiscardPolicy 丟棄任務,但是不丟擲異常 這是不推薦的做法
ThreadPoolExecutor.DiscardOldestPolicy 拋棄佇列中等待最久的任務 然後把當前任務加入佇列中
ThreadPoolExecutor.CallerRunsPolicy 呼叫任務的run()方法繞過執行緒池直接執行
  • 常用的任務拒絕策略就是第一種
  • 注:明確執行緒池最多可執行的任務數=佇列容量+最大執行緒數
  1. 演示ThreadPoolExecutor.AbortPolicy任務處理策略
public class ThreadPoolExecutorDemo01 {

    public static void main(String[] args) {

        /**
         * 核心執行緒數量為1 , 最大執行緒池數量為3, 任務容器的容量為1 ,空閒執行緒的最大存在時間為20s
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
                new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.AbortPolicy()) ;

        // 提交5個任務,而該執行緒池最多可以處理4個任務,當我們使用AbortPolicy這個任務處理策略的時候,就會丟擲異常
        for(int x = 0 ; x < 5 ; x++) {
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 執行了任務");
            });
        }
    }
}
  • 控制檯輸出結果
pool-1-thread-1---->> 執行了任務
pool-1-thread-3---->> 執行了任務
pool-1-thread-2---->> 執行了任務
pool-1-thread-3---->> 執行了任務
  • 控制檯報錯,僅僅執行了4個任務,有一個任務被丟棄了
  1. 演示ThreadPoolExecutor.DiscardPolicy任務處理策略
public class ThreadPoolExecutorDemo02 {
    public static void main(String[] args) {
        /**
         * 核心執行緒數量為1 , 最大執行緒池數量為3, 任務容器的容量為1 ,空閒執行緒的最大存在時間為20s
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
                new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.DiscardPolicy()) ;

        // 提交5個任務,而該執行緒池最多可以處理4個任務,當我們使用DiscardPolicy這個任務處理策略的時候,控制檯不會報錯
        for(int x = 0 ; x < 5 ; x++) {
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 執行了任務");
            });
        }
    }
}
  • 控制檯輸出結果
pool-1-thread-1---->> 執行了任務
pool-1-thread-1---->> 執行了任務
pool-1-thread-3---->> 執行了任務
pool-1-thread-2---->> 執行了任務
  • 控制檯沒有報錯,僅僅執行了4個任務,有一個任務被丟棄了
  1. 演示ThreadPoolExecutor.DiscardOldestPolicy任務處理策略
public class ThreadPoolExecutorDemo02 {
    public static void main(String[] args) {
        /**
         * 核心執行緒數量為1 , 最大執行緒池數量為3, 任務容器的容量為1 ,空閒執行緒的最大存在時間為20s
         */
        ThreadPoolExecutor threadPoolExecutor;
        threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
                new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.DiscardOldestPolicy());
        // 提交5個任務
        for(int x = 0 ; x < 5 ; x++) {
            // 定義一個變數,來指定指定當前執行的任務;這個變數需要被final修飾
            final int y = x ;
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 執行了任務" + y);
            });     
        }
    }
}
  • 控制檯輸出結果
pool-1-thread-2---->> 執行了任務2
pool-1-thread-1---->> 執行了任務0
pool-1-thread-3---->> 執行了任務3
pool-1-thread-1---->> 執行了任務4
  • 由於任務1線上程池中等待時間最長,因此任務1被丟棄
  1. 演示ThreadPoolExecutor.CallerRunsPolicy任務處理策略
public class ThreadPoolExecutorDemo04 {
    public static void main(String[] args) {

        /**
         * 核心執行緒數量為1 , 最大執行緒池數量為3, 任務容器的容量為1 ,空閒執行緒的最大存在時間為20s
         */
        ThreadPoolExecutor threadPoolExecutor;
        threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
                new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.CallerRunsPolicy());

        // 提交5個任務
        for(int x = 0 ; x < 5 ; x++) {
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 執行了任務");
            });
        }
    }
}
  • 控制檯輸出結果
pool-1-thread-1---->> 執行了任務
pool-1-thread-3---->> 執行了任務
pool-1-thread-2---->> 執行了任務
pool-1-thread-1---->> 執行了任務
main---->> 執行了任務
  • 通過控制檯的輸出,我們可以看到次策略沒有通過執行緒池中的執行緒執行任務,而是直接呼叫任務的run()方法繞過執行緒池直接執行。

9、原子性

9.1 volatile關鍵字

9.1.1 多執行緒的小問題

  • 某一個執行緒對共享資料做了修改,但是其他的執行緒感知不到,也就是說一個執行緒對共享資料做了修改對其他執行緒是不可見的

9.1.2 JMM記憶體模型

  • JMM(Java Memory Model)Java記憶體模型,是java虛擬機器規範中所定義的一種記憶體模型。
  • JMM記憶體模型描述了Java程式中各種變數(執行緒共享變數)的訪問規則,以及在JVM中將變數儲存到記憶體以及從記憶體中讀取變數這樣的底層細節
  • 特點
    1. 所有的共享變數都儲存於主記憶體(計算機的RAM),這裡所說的變數指的是例項變數和類變數,不包含區域性變數,因為區域性變數是執行緒私有的,因此不存在競爭問題
    2. 每一個執行緒還存在自己的工作記憶體,執行緒的工作記憶體,保留了被執行緒使用的變數的工作副本
    3. 執行緒對變數的所有操作(讀和寫)都必須在工作記憶體中完成,而不能直接操作主記憶體中的變數,不同執行緒之間也不能直接訪問對方工作記憶體中的變數,執行緒之間變數的值的傳遞需要通過主記憶體來完成

9.1.3 解決方案

9.1.3.1 使用volatile關鍵字解決
  • 把共享變數通過volatile關鍵字進行修飾
public class Money {
    public static volatile int money = 100000;
}
  • 被volatile修飾的變數,執行緒在對其進行操作的時候,首先會將當前工作記憶體中的變數副本失效,然後從主記憶體中重新獲取最新的資料,進行操作
  • volatile關鍵字可以解決多個執行緒直接對變數進行改變的時候的可見性
9.1.3.2 通過synchronized進行解決
  • 工作原理
    1. 執行緒獲得鎖
    2. 清空工作記憶體
    3. 從主記憶體拷貝共享資料最新的值到工作記憶體成為副本
    4. 執行程式碼
    5. 將修改後的副本的值重新整理回主記憶體中
    6. 執行緒釋放鎖
9.1.3.3 兩種方案的區別
  • volatile只能修飾例項變數和類變數,而synchronized可以修飾方法以及程式碼塊
  • volatile保證資料的可見性,但是不保證原子性(多執行緒進行寫操作,不保證執行緒安全);而synchronized是一種排他(互斥)的機制(因此有時我們也將synchronized這種鎖稱之為排它/互斥鎖),synchronized修飾的程式碼塊,被修飾的程式碼塊稱之為同步程式碼塊,無法被中斷可以保證原子性,也可以間接的保證可見性

9.2 原子性(AtomicInteger)

9.2.1 原子性測試

  • 原子性是指在一次操作或者多次操作中,要麼所有的操作全部都得到了執行並且不會受到任何因素的干擾而中斷,要麼所有的操作都不執行,多個操作是一個不可分割的整體
  • 例:從張三的賬戶給李四的賬戶轉1000元,這個動作將包含兩個基本的操作:從張三的賬戶扣除1000元,給李四的賬戶增加1000元。這兩個操作必須符合原子性的要求,要麼都成功要麼都失敗。
  • ++運算子分析
    • count++操作包含3個步驟
    • 從主記憶體中讀取資料到工作記憶體
    • 對工作記憶體中的資料進行++操作
    • 將工作記憶體中的資料寫回到主記憶體
    • 這3步中的任何一步都有可能會被其他執行緒中斷,當中斷以後,就會出現資料錯誤問題

9.2.2 測試volatile不保證原子性

  • 程式碼演示
public class MyAtomThread implements Runnable {

    private volatile int count = 0; //送冰淇淋的數量

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //1,從共享資料中讀取資料到本執行緒棧中.
            //2,修改本執行緒棧中變數副本的值
            //3,會把本執行緒棧中變數副本的值賦值給共享資料.
            count++;
            System.out.println("已經送了" + count + "個冰淇淋");
        }
    }
}
  • 控制檯輸出結果出現錯誤,volatile關鍵字修飾的變數沒有原子性

9.2.3 原子性的解決方案

9.2.3.1 加鎖
  • 程式碼演示
public class MyAtomThread implements Runnable {
    private volatile int count = 0; //送冰淇淋的數量
    private Object lock = new Object();
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //1,從共享資料中讀取資料到本執行緒棧中.
            //2,修改本執行緒棧中變數副本的值
            //3,會把本執行緒棧中變數副本的值賦值給共享資料.
            synchronized (lock) {
                count++;
                System.out.println("已經送了" + count + "個冰淇淋");
            }
        }
    }
}
9.2.3.2 使用JDK提供的原子類
  • java從JDK1.5開始提供了java.util.concurrent.atomic包(簡稱Atomic包),這個包中的原子操作類提供了一種用法簡單,效能高效,執行緒安全地更新一個變數的方式。因為變數的型別有很多種,所以在Atomic包裡一共提供了13個類,屬於4種類型的原子更新方式,分別是原子更新基本型別、原子更新陣列、原子更新引用和原子更新屬性(欄位)。這裡我們只講解使用原子的方式更新基本型別,使用原子的方式更新基本型別Atomic包提供了以下3個類:
類名介紹
AtomicBoolean 原子更新布林型別
AtomicInterger 原子更新整型
AtomicLong 原子更新長整型
  • 以上3個類提供的方法幾乎一模一樣,這裡僅以AtomicInteger為例進行講解,AtomicInteger的常用方法如下
  • 構造方法
方法說明
public AtomicInterger() 初始化一個預設值為0的原子型Interger
public AtomicInterger(int initialValue) 初始化一個指定值的原子型Interger
  • 成員方法
方法說明
int get() 獲取值
int getAndIncrement() 以原子方式將當前值加一,這裡返回的是自增前的值
int incrementAndGet() 以原子方式將當前值加一,這裡返回的是自增後的值
int addAndGet(int data) 以原子方式將輸入的數值與例項中的值相加並返回結果
int getAndSet(int value) 以原子方式設定為newValue的值,並返回舊值
  • 程式碼演示
public class MyAtomThread implements Runnable {

    AtomicInteger ac = new AtomicInteger(0);
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            int count = ac.incrementAndGet();
            System.out.println("已經送了" + count + "個冰淇淋");
        }
    }
}

9.3 CAS演算法+自旋鎖

9.3.1 概述

  • CAS的全稱是: Compare And Swap(比較再交換)
  • 是現代CPU廣泛支援的一種對記憶體中的共享資料進行操作的一種特殊指令
  • CAS可以將read-modify-write轉換為原子操作,這個原子操作直接由CPU保證
  • CAS有3個運算元:記憶體值V,舊的預期值A,要修改的新值B。當且僅當舊預期值A和記憶體值V相同時,將記憶體值V修改為B並返回true,否則什麼都不做,並返回false。
  • 舉例說明
    1. 在記憶體值V當中,儲存著為10的變數
    2. 此時執行緒1想把變數的值增加1,對執行緒1來說,舊的預期值A = 10 ,要修改的新值B = 11
    3. 線上程1要提交更新之前,另一個執行緒2搶先一步把記憶體值V中的變數率先更新成了11
    4. 執行緒1提交更新,首先進行A和記憶體值V的實際值比較(Compare),發現A不等於V的值,提交失敗
    5. 執行緒1會重新獲取記憶體值V作為當前A的值,並重新計算想修改的值,對此時的執行緒1來說,A= 11,B= 12,這個重新嘗試的過程被稱為內旋
    6. 如果沒有其他執行緒改變V的值,執行緒1進行Compare,發現A和V的值是相等的
    7. 執行緒1進行SWAP,把記憶體V的值替換為B,也就是12

9.3.2 原始碼解析

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}
  • 自旋的實現是通過do …while迴圈進行實現的
  • CAS演算法的實現是通過Unsafe這個類中的方法進行實現的
public final native boolean compareAndSetInt(Object o, long offset, int expected, int x);

9.3.3 悲觀鎖和樂觀鎖

  • synchronized是從悲觀的角度出發
    • 總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞直到它拿到鎖(共享資源每次只給一個執行緒使用,其它執行緒阻塞,用完後再把資源轉讓給其它執行緒)。因此synchronized我們也將其稱之為悲觀鎖。jdk中的ReentrantLock也是一種悲觀鎖。
  • CAS是從樂觀的角度出發
    • 總是假設最好的情況,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料。CAS這種機制我們也可以將其稱之為樂觀鎖。

10、併發工具類

10.1 ConcurrentHashMap

10.1.1 使用

  • ConcurrentHashMap是一個執行緒安全的Map集合
  • 之前所學習的HashMap是執行緒不安全的,如果存在多個執行緒操作同一個HashMap的時候就會出現執行緒安全問題。在Jdk1.5之前如果我們要通過多個執行緒操作Map集合,保證資料的安全性,我們常常使用的就是Hashtable。Hashtable是執行緒安全的。但是Hashtable保證執行緒安全性的效率比較低,因此在Jdk1.5之後java就提供了ConcurrentHashMap供我們進行使用
  • Hashtable效率低下的原因:Hashtable使用的就是同步方法的方式來保證資料的安全性,並且每一個方法都會加鎖。也就是說只要有一個執行緒對Hashtable進行操作,其他執行緒必須等待
  • ConcurrentHashMap的繼承體系結構
  • ConcurrentHashMap繼承自Map,所以是一個雙列集合
  • 構造方法
public ConcurrentHashMap() {}
  • 程式碼演示
public class MyConcurrentHashMapDemo {
    
    public static void main(String[] args) throws InterruptedException {
        
        ConcurrentHashMap<String, String> hm = new ConcurrentHashMap<>();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 25; i++) {
                hm.put(i + "", i + "");
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 25; i < 51; i++) {
                hm.put(i + "", i + "");
            }
        });

        t1.start();
        t2.start();

        System.out.println("----------------------------");
        //為了t1和t2能把資料全部新增完畢
        Thread.sleep(1000);
        //0-0 1-1 ..... 50- 50
        for (int i = 0; i < 51; i++) {
            System.out.println(hm.get(i + ""));
        }//0 1 2 3 .... 50


    }
}

10.1.2 JDK1.7版本原理

  • ConcurrentHashMap 保證執行緒安全性並且效率還比較高的原理就在於分段鎖
  • 新增元素的過程

10.1.3 JDK1.8版本原理

  • 保證資料安全性的原理
    • CAS演算法+區域性鎖定
  • 新增元素過程

10.2 CountDownLatch

  • 作用
    • 當某一個執行緒等待其他執行緒執行完畢之後再進行執行
  • 相關方法
方法說明
public CountDownLatch(int count) 初始化一個指定計數器的CountDownLatch物件
public void await() throws InterruptedException 讓當前執行緒等待
public void countDown() 計數器進行減一
  • 原理
    • CountDownLatch的構造方法引數表示的就是等待的執行緒數量。內部維護了一個計數器,這個計數器的初始化值就是引數值

10.3 Semaphore

  • Semaphore(訊號量):控制某一段程式碼同時執行的執行緒數量
  • 構造方法
方法說明
public Semaphore(int permits) permits表示許可執行緒的數量
  • 成員方法
方法說明
public void acquire() throws InterruptedException 表示獲取許可
public void release() 表示釋放許可
  • 使用場景
    • Semaphore可以用來限流
  • 程式碼演示
public class MyRunnable implements Runnable {
    //1.獲得管理員物件,
    private Semaphore semaphore = new Semaphore(2);
    @Override
    public void run() {
        try {
            //2.獲得通行證
            semaphore.acquire();

            //3.開始行駛
            System.out.println("獲得了通行證開始行駛");
            Thread.sleep(2000);
            System.out.println("歸還通行證");

            //4.歸還通行證
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class MySemaphoreDemo {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();

        for (int i = 0; i < 100; i++) {
            new Thread(mr).start();
        }
    }
}