關於多線程、線程池、線程安全問題


多線程

1、基礎概念

1.1 多線程技術

  • 從軟件或者硬件上實現同時執行多個任務
  • 具有多線程能攔的計算機因有硬件支持而能夠在同一時間執行多個線程
  • 多線程編程常常也將其稱之為並發編程

1.2 並發和並行

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

1.3 進程和線程

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

2、線程的實現

2.1 繼承Thread類

  • Thread類
    • Thread類就是JDK給我們定義的一個線程類
  • 方法介紹
方法名 說明
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(); } } }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM