目錄
一.單例模式
1. 餓漢模式
2. 懶漢模式
?二.阻塞隊列
1. 阻塞隊列的概念
2. BlockingQueue接口
3.生產者-消費者模型
4.模擬生產者-消費者模型
一.單例模式
單例模式(Singleton Pattern)是一種常用的軟件設計模式,其核心思想是確保一個類只有一個實例,并提供一個全局訪問點來獲取這個實例。
為什么要引入單例模式?
單例模式的核心就是一個類中只有一個實例,因為只用管理一個實例,那么就可以更好的對代碼進行一個校驗和檢查,方便高效管理,同時也避免了多個實例可能帶來的問題。
如果創建一個實例需要耗費100G的資源,那么創建出多個實例,代價太大。而且在多數情況下,一個實例完全夠用,所有沒有必要創建出多個實例,這樣就避免資源的重復創建和浪費
在編譯器中,沒有提供類只能創建出多少個實例的方法,但是我們可以通過一些代碼邏輯去規定創建實例的要求,下面是常用的幾種單例模式。
1. 餓漢模式
核心特點是?在類加載時就立即創建單例實例,并通過靜態方法提供全局訪問。
class Singleton{private static Singleton singleton = new Singleton();public static Singleton getSingleton(){return singleton;}//核心操作private Singleton(){}}
- 使用static關鍵字保證唯一實例(在類被加載的時候就會創建出這個唯一實例)
- 構造方法被設為私有,導致構造方法無法被調用
- 如果想要獲取這個實例只能使用靜態方法getSingleton調用
?由于在類被加載的時候,就會創建出實例,創建實例的時機很早(感覺非常的迫切,像一個餓漢),所有叫做餓漢模式
?注意:在多線程中,并發調用getSingleton靜態方法,由于只有讀操作,所以是線程安全
缺點: 如果實例未被使用,實例依然會被創建,可能造成資源浪費(假設實例的大小是100G)。
2. 懶漢模式
其核心特點是?延遲實例的創建,只有在第一次使用時才初始化單例對象,以減少資源浪費。
public class LazySingleton {private static LazySingleton instance;private LazySingleton() {}// 非線程安全public static LazySingleton getInstance() {if (instance == null) {//這里會涉及指令重排序的問題instance = new LazySingleton();}return instance;}
}
在使用調用方法時,只有在第一次使用時才初始化實例,否則都是返回已經存在的實例
注意:?在多線程中,并發調用getInstance方法,由于同時存在兩個線程修改一個變量操作,線程不安全
?發現出現new兩次情況,所以解決這個問題,我們要進行加鎖
class LazySingleton {private static LazySingleton instance;private LazySingleton() {}static Object A = new Object();// 非線程安全public static LazySingleton getInstance() {synchronized (A){if (instance == null) {//這里會涉及指令重排序的問題instance = new LazySingleton();}}return instance;}
}
?這里我們會發現,每次調用getInstance方法,都要進行加鎖和解鎖的步驟,這樣的步驟開銷很大
所以我們需要進行改進
public static LazySingleton getInstance() {if(instance==null){synchronized (A){if (instance == null) {//這里會涉及指令重排序的問題instance = new LazySingleton();}}}return instance;}
- 第一次的? if? 語句判斷是否需要加鎖
- 第二次的? if? ?語句判斷是否為空
?寫到這里我們的代碼還存在一個很嚴重的問題,由于指令重排序引起的線程安全問題
instance = new LazySingleton();
?在創建一個實例的時候,主要有3步驟:正確的步驟順序是1—>2—>3
1. 在內存中開辟一份空間,2.使用構造方法去創建實例,3. 將空間的地址賦值給引用變量
但是JVM可能會將步驟的執行順序發送改變1—>3—>2,從而引發線程安全問題
具體原因:t2線程沒有進入因為鎖阻塞這步,t2線程會在t1線程執行完地址賦值后,剛好執行第一次的 if 判斷語句,發現引用變量不為空,會直接返回引用變量(但是引用變量的值是空的)?
其實解決也很簡單使用volatile關鍵字
class LazySingleton {private static volatile LazySingleton instance;private LazySingleton() {}static Object A = new Object();// 非線程安全public static LazySingleton getInstance() {if(instance==null){synchronized (A){if (instance == null) {//這里會涉及指令重排序的問題instance = new LazySingleton();}}}return instance;}
}
?二.阻塞隊列
1. 阻塞隊列的概念
阻塞隊列可以看成是普通隊列的一種擴展,遵循先進先出的原則,核心特性是當隊列操作無法立即執行時,線程會被自動阻塞直到條件滿足。
- 如果隊列是滿的,進行入隊列操作則會被阻塞,直到隊列不滿
- 如果隊列是空的,進行出隊列操作則會被阻塞,直到隊列不空
2. BlockingQueue接口
在 Java?標準庫中內置了阻塞隊列. 如果我們需要在一些程序中使用阻塞隊列, 直接使用標準庫中的
- BlockingQueue接口屬于java.util.concurrent包。它是線程安全的隊列,支持阻塞操作。
- 常見的實現類有:ArrayBlockingQueue,LinkedBlockingQueue,PriorityBlockingQueue
方法 | 說明 |
---|---|
| 隊列未滿時插入元素;若隊列已滿,則阻塞線程直到有空位。 |
| 隊列非空時取出元素;若隊列為空,則阻塞線程直到有元素可用。 |
| 隊列未滿時插入元素并返回? |
| 隊列非空時取出元素并返回;隊列為空時返回? |
| 隊列滿時等待指定超時時間,超時后返回? |
| 隊列空時等待指定超時時間,超時后返回? |
E peek() | 返回隊列頭部元素但不移除;隊列空時返回?null 。 |
其中只有put( )和take( )方法帶有阻塞的效果
3.生產者-消費者模型
生產者-消費者模型用于解決多線程環境下的線程同步問題,核心思想是通過?共享緩沖區(阻塞隊列)?解耦生產者和消費者,使兩者可以獨立并發工作
生產者和消費者彼此之間不直接進行聯系,而是通過緩沖區(阻塞隊列)進行聯系?
好處
(1)解耦合
- 解耦生產者和消費者的直接依賴,生產者和消費者可獨立開發,提高開發效率,方便維護
- ?生產者和消費者可并行執行,最大化利用 CPU、I/O 等資源
- 即使生產者掛了,也不會影響消費者的正常工作(反之同理)
解耦合在分布式系統中很常見,比如服務器的整個功能并不是由一個服務器全部完成,而是由多個服務器完成,每個服務器完成一部分功能,最后通過服務器之間的網絡通信,實現整個功能?
(2)緩沖機制
- 生產者突發大量請求時,隊列暫存數據,避免消費者過載崩潰。
- 消費者處理慢時,隊列累積任務,避免生產者因等待而阻塞。
- 可以將緩沖區作為“蓄水池”,協調速度差異
public class Demo_3 {public static void main(String[] args) {BlockingDeque<Integer> deque = new LinkedBlockingDeque<>(100);Thread t1 = new Thread(()->{while(true){try {Thread.sleep(300);Random random = new Random();int num = random.nextInt(101);System.out.println("生產數:"+ num);deque.put(num);} catch (InterruptedException e) {e.printStackTrace();}}},"生產者");t1.start();Thread t2 = new Thread(()->{while(true){try {Thread.sleep(500);int num = deque.take();System.out.println("消費數:"+num);} catch (InterruptedException e) {e.printStackTrace();}}},"消費者");t2.start();}
}
?
4.模擬生產者-消費者模型
核心:阻塞隊列的實現
將其看成一個循環隊列,其中兩個核心的方法:put()和take()
put():入隊列操作,如果隊列為滿則進入阻塞狀態,由take()進行喚醒
take():出隊列操作,如果隊列為空則進入阻塞狀態,由put()方法進行喚醒
在判斷是否需要進入阻塞狀態的時候,使用while語句,進行多次判斷,如果使用if語句相當于一錘定音,在阻塞的狀態下,可能會被notifyAll()喚醒,但是這時候隊列中的空間并不足夠(虛假喚醒),也有可能會出現連續喚醒的情況,最好的方式是再進行一次判斷
class MyBlockingQueue{Object A = new Object();int[] elems = null;int right ;int tail ;int usedSize;MyBlockingQueue(int capacity){elems = new int[capacity];}//注意鎖的位置//放入操作public void put(int elem) throws InterruptedException {synchronized (A){//如果滿,不能放出while (usedSize>=elems.length){A.wait();}//沒有滿,正常放入elems[tail++] = elem;if(tail>=elems.length){tail = 0;}usedSize++;A.notify();}}public int take() throws InterruptedException {synchronized (A){//如果為空,不能取出while (usedSize == 0){A.wait();}//不為空,正常取出int elem = elems[right++];//另一種寫法right = right%elems.length;usedSize--;A.notify();return elem;}}}
?點贊的寶子今晚自動觸發「躺贏錦鯉」buff!