目錄
單例模式
餓漢模式
懶漢模式
阻塞隊列
?生產者消費者模型意義:
阻塞隊列使用方法
實現阻塞隊列
阻塞隊列實現生產者消費者模型
定時器
實現簡單的定時器
工廠模式
線程池
為啥呢? 從池子里面取 比 創建線程 效率更高
線程池的創建
怎么填坑
ThreadPoolExecutor
線程數目設置
?實現線程池
?小結
兩個設計模式: 單例模式, 工廠模式
單例模式
有些場景中希望有些類僅僅創建一個對象, 代碼中很多管理數據的對象都是單例的, MySQL JDBC等.
人可能會出錯, 需要編譯器幫我們做出監督. 就比如 @Override 必須是方法重寫.,在語法層面上沒有對單例做出支持, 只能通過編程技巧實現
餓漢模式
剛開始就創建了實例舉個例子:
//期望這個類能有唯一實例
class Singleton {//設置為靜態變量在 Singleton 類被加載時會創建實例private static Singleton instance = new Singleton();//獲取實例public static Singleton getInstance() {return instance;}//把構造方法設為 私有 , 類外面的代碼無法 new 出類對象了.private Singleton() {};
}
注意:
1> 在類的內部提供線程的實例
2> 把構造方法設為 private ,避免其他代碼創建實例.
懶漢模式
先判斷是否需要創建實例舉個例子:
//期望這個類能有唯一實例
class SingletonLazy {private static volatile SingletonLazy instance = null;//獲取實例public static SingletonLazy getInstance() {if(instance == null) {synchronized (SingletonLazy.class) {if(instance == null) {instance = new SingletonLazy();}}}return instance;}//把構造方法設為 私有 , 類外面的代碼無法 new 出類對象了.private SingletonLazy() {};
}
注意:
1> 第一次判斷是否為空原因:
因為加鎖開銷很大, 而且可能涉及到鎖沖突, 所以我們增加一次判斷, 不為空直接返回 instance
2> 加鎖的原因:
在本操作中會出現讀取和修改的操作, 會出現兩個都判斷為空后創建多個實例的情況.
3> 使用 volatile 原因:
指令重排序問題
編譯器為了提高效率, 可能調整代碼的執行順序, 但是必須保持代碼邏輯不變, 單線程沒問題, 但是多線程可能有問題.
new 操作, 可能觸發指令重排序
new 操作分為三步:
1.?申請內存空間
2. 在內存空間上構造對象
3. 把內存地址給 instance
可能按照 123, 132順序執行, 1一定先執行
在多線程下, 假設? t1線程? ?按照1 3 2 的順序? 執行1? 3后, instance非空指向一個沒初始化的非法對象, 這時? ? ??t2線程? ?在判斷instance 不為空后, 直接返回一個非法對象, 導致出現bug
使用 volatile 保證不會出現指令重排序問題
阻塞隊列
多線程代碼中比較常用到的一種數據結構
特殊的隊列
1> 線程安全
2> 帶有阻塞特性
a) 如果隊列為空, 繼續出隊列, 就會發生阻塞, 阻塞到其他線程往隊列里添加元素位置為止
b) 如果隊列為滿, 繼續入隊列, 也會發生阻塞, 阻塞到其他線程從隊列中取走元素位置為止.
意義: 實現 " 生產者消費者模型 " 一種常見的多線程代碼編寫方式
舉個例子: 包餃子
1> 每個人分別負責搟餃子皮和包餃子
2> 當搟餃子皮快了 就會在 放餃子皮的蓋簾滿的時候停下來等包餃子的
3> 當包餃子快了 就會停下來等 搟餃子皮的
蓋簾就相當于阻塞隊列
生產者 把生產出來的內容放到阻塞隊列中
消費者 從阻塞隊列中獲取元素
?生產者消費者模型意義:
1> 解耦合
兩個模塊聯系越緊密, 耦合就越高, 這個模型讓耦合降低
2> 削峰填谷
服務器 A 給服務器 B發起請求, 不同服務器消耗的硬件資源不一樣, A收到的請求發給B可能就掛了.使用削峰填谷讓 B 接受的請求按照 B 的原有節奏處理情況.(這種情況一般不會持續存在, 就好比學校搶課的情況), 峰值過后 B把積壓的數據處理掉
阻塞隊列使用方法
在 Java 標準庫里, 已經提供了現成的 阻塞隊列直接使用
在標準庫里, 針對 BlockingQueue 提供了兩種最重要的實現方式
1> 基于數組
2> 基于鏈表
BlockingQueue 一般不適用 Queue 中的一些方法, 因為他們不具備阻塞的特性.?
一般使用 (put 阻塞式的入隊列), (take 阻塞式的出隊列)
示例:?
public class Test {public static void main(String[] args) throws InterruptedException {BlockingDeque<String> queue = new LinkedBlockingDeque<>();queue.put("111");queue.put("222");queue.put("333");queue.put("444");String elem = queue.take();System.out.println(elem);elem = queue.take();System.out.println(elem);elem = queue.take();System.out.println(elem);elem = queue.take();System.out.println(elem);elem = queue.take();System.out.println(elem);}
}
最后一次輸出時發生了阻塞.
實現阻塞隊列
基于普通隊列加上阻塞和線程安全
普通隊列基于數組 或者 基于鏈表
基于數組實現隊列理解成一個環
class MyBlockingQueue {private String[] data = new String[1000];// 隊列的起始位置private int head = 0;// 隊列的結束位置的下一個位置private int tail = 0;//隊列中有效元素的個數private int size = 0;//提供的方法 入隊列 出隊列public void put(String elem) throws InterruptedException {synchronized (this) {while(size == data.length) {this.wait();}data[size] = elem;tail++;if(tail == data.length) {tail = 0;}size++;//這個 notify 用來喚醒 take 中的 waitthis.notify();}}public String take() throws InterruptedException {synchronized (this) {while(size == 0) {this.wait();}String ret = data[head];head++;if(head == data.length) {head = 0;}size--;//這個 notify 用來喚醒 put 中的 waitthis.notify();return ret;}}
}
wait 除了可以用 notify 喚醒, 還可以用 interrupt 喚醒, 直接整個方法結束了, 因為使用了 throws 拋出異常, 這是沒有什么事
如果使用 try catch 方式就會出現bug, 讓 tail 把指向的元素覆蓋掉了, 然后弄丟了一個元素, 而且 size 也會比數組最長長度還大.(此處不理解看http://t.csdnimg.cn/OBwXN?-->中斷一個線程目錄)
所以在wait 返回的時候進一步確認是否當前隊列是滿的不是, 如果是滿的繼續進行wait
所以直接使用 while 判定是否是滿的.
為了避免內存可見性問題, 把 volatile 加好
阻塞隊列實現生產者消費者模型
package Demo2;import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;class MyBlockingQueue {private String[] data = new String[1000];// 隊列的起始位置private volatile int head = 0;// 隊列的結束位置的下一個位置private volatile int tail = 0;//隊列中有效元素的個數private volatile int size = 0;//提供的方法 入隊列 出隊列public void put(String elem) throws InterruptedException {synchronized (this) {while(size == data.length) {this.wait();}data[tail] = elem;tail++;if(tail == data.length) {tail = 0;}size++;//這個 notify 用來喚醒 take 中的 waitthis.notify();}}public String take() throws InterruptedException {synchronized (this) {while(size == 0) {this.wait();}String ret = data[head];head++;if(head == data.length) {head = 0;}size--;//這個 notify 用來喚醒 put 中的 waitthis.notify();return ret;}}
}public class Test {public static void main(String[] args) {MyBlockingQueue queue = new MyBlockingQueue();// 消費者Thread t1 = new Thread(() -> {while(true) {try {String result = queue.take();System.out.println("消費元素: " + result);Thread.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}}});// 生產者Thread t2 = new Thread(() -> {int num = 1;while(true) {try {queue.put(num+ " ");System.out.println("生產元素: " + num);num++;} catch (InterruptedException e) {throw new RuntimeException(e);}}});t1.start();t2.start();}
}
定時器
約定一個時間, 時間到達之后執行某個代碼邏輯, 在網絡通信中很常見
?在 標準庫 中有現成定時器的實現
public static void main(String[] args) {Timer timer = new Timer();// 給定時器安排了一個任務, 預定在 xxx 時間去執行timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("執行定時器任務");}}, 2000);System.out.println("程序啟動!");}
使用匿名內部類的寫法繼承 TimerTask 創建出實例, 目的時重寫 run, 描述任務的詳細情況
當前代碼也是多線程, timer 里面包含一個線程, 下圖是運行結果
可以發現整個進程沒有結束, 因為 Timer 內部的線程阻止了進程結束.
?Timer 里面可以安排多個任務.
public static void main(String[] args) {Timer timer = new Timer();// 給定時器安排了一個任務, 預定在 xxx 時間去執行timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("3000");}}, 3000);System.out.println("程序啟動!");timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("2000");}}, 2000);System.out.println("程序啟動!");timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("1000");}}, 2000);System.out.println("程序啟動!");}
實現簡單的定時器
1> Timer 中需要有一個線程, 掃描任務是否到時間了, 可以執行了
2> 需要一個數據結構把所有任務保存起來(使用優先級隊列)?
3> 創建一個類, 通過類的對象描述一個任務(至少要包含任務內容和時間)
?其中需要記錄, 絕對的時間.
import java.awt.*;
import java.util.PriorityQueue;
import java.util.Timer;
import java.util.TimerTask;// 通過這個類, 描述一個任務
class MyTimerTask implements Comparable<MyTimerTask> {// 執行的任務private Runnable runnable;// 執行任務的時間private long time;// 此處的 delay 就是 schedule 方法傳入的 "相對時間"public MyTimerTask(Runnable runnable, long delay) {this.runnable = runnable;this.time = System.currentTimeMillis() + delay;}@Overridepublic int compareTo(MyTimerTask o) {// 讓隊首元素是最小時間的值return (int) (this.time - o.time);// 讓隊首元素是最大時間的值//return (int) (o.time - this.time);}public long getTime() {return time;}public Runnable getRunnable() {return runnable;}
}// 自己的定時器
// 添加元素和掃描線程是不同線程操作同一個隊列, 需要加鎖 <--原因之一
class MyTimer {// 使用一個數據結構, 保存所有的任務private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();// 使用這個對象作為鎖對象private Object locker = new Object();public void schedule(Runnable runnable, long delay) {synchronized(locker) {queue.offer(new MyTimerTask(runnable, delay));locker.notify();}}// 掃描線程public MyTimer() {// 創建一個掃描線程Thread t = new Thread(() -> {// 掃描線程需要不停掃描看是否到達時間while (true) {try {synchronized (locker) {// 不要使用 if 作為 wait 的判定條件, 應使用while// 使用 while 是為了在喚醒之后 在再次確認一下條件while (queue.isEmpty()) {locker.wait();}MyTimerTask task = queue.peek();// 比較一下當前的隊首元素是否可以執行了long curTime = System.currentTimeMillis();if (curTime >= task.getTime()) {// 執行任務task.getRunnable().run();//執行完了, 就從隊列中刪除queue.poll();} else {// 不可執行, 先等著, 等待下一輪的循環判定locker.wait(task.getTime() - curTime);}}}catch (InterruptedException e) {e.printStackTrace();}}});t.start();}
}public class Demo2 {public static void main(String[] args) {MyTimer timer = new MyTimer();timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("3000");}}, 3000);timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("2000");}}, 2000);timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("1000");}}, 1000);}
}
工廠模式
線程池
線程創建/銷毀 比 進程快, 但是進一步提高創建/銷毀的頻率, 線程的開銷也不能忽視了
兩種提高效率的方法:
1> 協程 (輕量級線程)?
相對于線程, 把系統調度的過程給忽略了,(程序猿手動調度), 當下比較流行(Java 標準庫沒有協程)
2> 線程池
兜底, 使線程不至于很慢
例子: 我是個妹子, 在談男朋友, 一段時間后, 我不想和他好了, 就冷暴力然后分手, 分手之后再去找另一個小哥哥, 然后和另一個小哥哥好上了.?
線程池就是我在談第一個男朋友的時候就同時和其他小哥哥搞曖昧(培養感情), 哪天想分手了直接分, 然后無縫銜接
線程池: 在使用第一個線程的時候, 提前把 2, 3, 4, 5線程創建好(培養感情), 后續想使用新的線程不必創建, 直接使用(創建線程的開銷降低了)
為啥呢? 從池子里面取 比 創建線程 效率更高
從池子里取, 就是純粹用戶態操作
創建新的線程需要 用戶態 + 內核態 相互配合 完成
操作系統是由 內核 + 配套的應用程序 構成
內核 是系統最核心的部分, 創建線程操作需要調用系統 api, 進入到內核中, 按照內核態的方式來完成一系列動作
當你想要創建線程的時候, 內核需要給所有進程提供服務, 不可控, 難以避免會做一些其他的事導致效率減低
線程池的創建
Java標準庫提供了寫好的線程池.
創建線程池對象并沒有 new , 而是通過專門的方法返回了一個線程池對象(工廠模式), 通常創建對象使用 new , new 就會觸發類的構造方法, 但構造方法存在一定的局限性. 工廠模式是給構造方法填坑的.
怎么填坑
我們構造一個對象希望有多種構造方式, 這就需要多個構造方法, 但是構造方法的名字必須是類名, 不同的構造方法只能通過 重載區分, 但是如果實現方法不一樣, 但是參數類型/個數一樣咋辦呢?
使用工廠設計模式, 使用普通的方法代替構造方法完成初始化工作, 普通方法使用名字區分.
?Executors 是一個 工廠類, newCachedThreadPool 是工廠方法, 使用靜態方法通過類名調用
工廠方法有很多, 上述方法創建出來的線程池對象的線程數目可以動態適應, 隨著王線程池里面添加任務, 線程池中的線程自動創建, 創建出來在池子里保留一定時間以備后續使用.
這個方法是固定的線程池, 調用方法時手動指定創建幾個線程
?還用很多其他線程池上面介紹的兩種用的更多一點
ThreadPoolExecutor
上述工廠方法生成的線程池本質上是對 類(ThreadPoolExecutor) 的封裝
核心方法:
1> 添加任務
2> 構造
舉例:? 1> 添加任務 (簡單)?
使用 submit 把任務交給線程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Demo3 {public static void main(String[] args) {ExecutorService service = Executors.newFixedThreadPool(4);service.submit(new Runnable() {@Overridepublic void run() {System.out.println("hello");}});}
}
?2> 構造方法 (重點)
構造方法中參數很多[經典面試題]
在 juc 包里面, 并發編程相關內容?
全部參數 如下圖:
對這 4 種情況 舉個例子:
我有 任務 A 要做, 朋友來讓我幫忙做任務 B, 這時我有 4 種回應方法.
1> 我心態崩了, 大哭. 拋出異常
2> 我對朋友說你自己做, 朋友自己做任務 B
3> 我的任務 A 不做了, 就去幫朋友
4> 我直接拒絕幫忙, 我仍然做任務 A , 朋友也不做任務 B 了
線程數目設置
使用線程池需要設置線程的數目, 設置多少合適?
具體數目是不對的, 需要實際情況分析
原因:
一個線程執行代碼主要有兩類:
1> cpu 密集型: 代碼主要是進行 算術運算/邏輯判斷
2> IO密集型: 代碼里主要進行的是 IO 操作
如果是 1>? 這個時候線程池的數量不要超過 N (設 N 就是極限), 比 N 更大, 就無法提高效率了, cpu吃滿了, 線程越多反而增加調度的開銷
如果是 2>? 不吃 CPU, 此時設置的線程數可以超過 N, 一個核心可以通過調度的方式來并發執行.
?實現線程池
class MyThreaPool {// 任務隊列private BlockingDeque<Runnable> queue = new ArrayBlockingQueue<>();// 通過這個方法, 把任務添加到隊列中public void submit(Runnable runnable) throws InterruptedException {//此處策略是第 5 種, 拒絕策略, 阻塞等待queue.offer(runnable);}public MyThreaPool(int n) {// 創建出 n 個線程, 負責執行上述隊列中的任務for (int i = 0; i < n; i++) {Thread t = new Thread(() -> {// 讓這個線程從隊列中消費任務,并進行執行try {Runnable runnable = queue.take();runnable.run();} catch (InterruptedException e) {e.printStackTrace();}});t.start();}}
}
?小結
認真學習各種多線程代碼實例, 理解其中的含義, 將各個代碼的的易錯點分析透徹