目錄
#故事
寫在前面
單例模式定義
餓漢式單例模式
懶漢式單例模式
内部静态类单例模式
首頁 電腦教學 電腦知識 一個單例模式,沒必要這麼卷吧

一個單例模式,沒必要這麼卷吧

Feb 22, 2024 pm 06:25 PM
設計模式 程式碼 spring框架 招式

老貓的設計模式專欄已經偷偷發車了。不甘願做crud boy?看了幾遍的設計模式還記不住?那就不要刻意記了,跟上老貓的步伐,在一個個有趣的職場故事中領悟設計模式的精髓。還等什麼?趕快上車吧

將系統軟體比喻為江湖,設計原則就是OO程式設計師的武功心法,設計模式則是招式。光有心法還不夠,需要結合招式才行。心法和招式相輔相成,才能應付強敵(如「坑爹系統」),靈活應變,戰無不勝。

一個單例模式,沒必要這麼卷吧

#故事

之前讓小貓梳理的業務流程以及程式碼流程基本上已經梳理完畢【系統梳理大法&程式碼梳理大法】。從代碼側而言也搞清楚了系統臃腫的原因【違背設計原則】。小貓逐漸步入正軌,他決定從一些簡單的業務場景入手,開始著手優化系統程式碼。那麼什麼樣的業務程式碼,動了之後影響最小呢?小貓看了看,打算就從氾濫創建的線程池著手吧,他打算用單例模式做一次重構。

在小貓接手的系統中,執行緒池的創建通常是根據需要在具體的類別中直接實作多執行緒功能。因此,在許多service服務類別中都會發現創建線程池的踪跡。

寫在前面

為了解決上述小貓的問題,我們計劃使用單例模式來建立一個共享執行緒池執行器,並結合工廠模式根據業務類型進行分類管理。

接下來,我們就單例模式開始吧。

一個單例模式,沒必要這麼卷吧

#概要

單例模式定義

單例模式(Singleton)是一種設計模式,旨在確保系統中只有一個特定類別的實例,並提供一個全域存取點以便外部存取。這種模式的出現是為了控制系統中某個類別的實例數量,節省系統資源並方便存取。透過單例模式,可以有效管理系統中的物件實例,避免多次建立相同類型的對象,提高系統效能和資源利用率。單例模式的實作方式有多種,包括懶漢式、餓漢式、雙重檢查鎖定等。在軟體開發中,單例模式經常被用於需要唯一實例的場景,如配置資訊、日誌記錄、執行緒池等。透過單例模式,可以簡化系統架構,降低耦合度,提高程式碼的可維護性和可擴充性。

一個單例模式,沒必要這麼卷吧

#單例模式簡單示意圖

餓漢式單例模式

什麼叫做餓漢式單例?為了方便記憶,老貓是這麼理解的,餓漢給人的形象就是有食物就迫不及待去吃的形象。那麼餓漢式單例模式的圖像也就是當類別創建的時候就迫不及待地去創建單例對象,這種單例模式是絕對線程安全的,因為這種模式在尚未產生線程之前就已經創建了單例。

看一下範例,如下:

/**
 * 公众号:程序员老猫
 * 饿汉单例模式 
 */
public class HungrySingleton {

private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();

//构造函数私有化,保证不被new方式多次创建新对象
private HungrySingleton() {
}

public static HungrySingleton getInstance(){
return HUNGRY_SINGLETON;
}
}
登入後複製

我們來看看上述案例的優缺點:

  • 優點:線程安全,類別載入時完成初始化,取得物件的速度較快。
  • 缺點:由於類別載入的時候就完成了物件的創建,有的時候我們無需呼叫的情況下,物件已經存在,這樣的話就會造成記憶體浪費。

當前硬體和伺服器的發展,快於軟體的發展,另外的,微服務和集群化部署,大大降低了橫向擴展的門檻和成本,所以老貓覺得當前的內存其實是不值錢的,所以上述這種單例模式硬說其缺點有多嚴重其實也不然,個人覺得這種模式用在實際開發過程中其實是沒有問題的。

其實在我們日常使用的spring框架中,IOC容器本身就是一個餓漢式單例模式,spring啟動的時候就將對象加載到了內存中,這裡咱們不做展開,等到後續咱們梳理到spring源程式碼的時候再展開來說。

懶漢式單例模式

上述餓漢單例模式我們說它的缺點是浪費內存,因為其在類別加載的時候就創建了對象,那麼針對這種內存浪費的解決方案,我們就有了“懶漢模式”。對於這種類型的單例模式,老貓是這麼理解的,懶漢的定義給人的直覺感覺是懶惰、拖延。那麼對應的模式上來說,這種方案創建對象的方法也是在程式使用對象前,先判斷該對像是否已經實例化(判空),若已實例化直接返回該類對象,否則則先執行實例化操作。

看一下範例,如下:

/**
 * 公众号:程序员老猫
 * 懒汉式单例模式
 */
public class LazySingleton {
private LazySingleton() {
}

private static LazySingleton lazySingleton = null;
public static LazySingleton getInstance() {
if (lazySingleton == null) {
lazySingleton =new LazySingleton();
}
return lazySingleton;
}
}
登入後複製

上面這種單例模式創建對象,記憶體問題看起來是已經解決了,但是這種創建方式真的就線程安全了麼?咱們接下來寫個簡單的測試demo:

public class Main {
public static void main(String[] args) {
Thread thread1 = new Thread(()->{
LazySingleton lazySingleton = LazySingleton.getInstance();
System.out.println(lazySingleton.toString());
});
Thread thread2 = new Thread(()->{
LazySingleton lazySingleton = LazySingleton.getInstance();
System.out.println(lazySingleton.toString());
});
thread1.start();
thread2.start();
System.out.println("end");
}
}
登入後複製

執行輸出結果如下:

end
LazySingleton@3fde6a42
LazySingleton@2648fc3a
登入後複製

從上述的輸出我們很容易地發現,兩個執行緒所得到的物件是不同的,當然這個是有一定機率性質的。所以在這種多執行緒請求的場景下,就出現了執行緒安全性問題。

聊到共享变量访问线程安全性的问题,我们往往就想到了锁,所以,咱们在原有的代码块上加上锁对其优化试试,我们首先想到的是给方法代码块加上锁。

加锁后代码如下:

public class LazySingleton {

private LazySingleton() {
}

private static LazySingleton lazySingleton = null;
public synchronized static LazySingleton getInstance() {
if (lazySingleton == null) {
lazySingleton =new LazySingleton();
}
return lazySingleton;
}
}
登入後複製

经过上述同样的测试类运行之后,我们发现问题似乎解决了,每次运行之后得到的结果,两个线程对象的输出都是一致的。

我们用线程debug的方式看一下具体的运行情况,如下图:

一個單例模式,沒必要這麼卷吧

线程输出

我们可以发现,当一个线程进行初始化实例时,另一个线程就会从Running状态自动变成了Monitor状态。试想一下,如果有大量的线程同时访问的时候,在这样一个锁的争夺过程中就会有很多的线程被挂起为Monitor状态。CPU压力随着线程数的增加而持续增加,显然这种实现对性能还是很有影响的。

那还有优化的空间么?当然有,那就是大家经常听到的“DCL”即“Double Check Lock” 实现如下:

/**
 * 公众号:程序员老猫
 * 懒汉式单例模式(DCL)
 * Double Check Lock
 */
public class LazySingleton {

private LazySingleton() {
}
//使用volatile防止指令重排
private volatile static LazySingleton lazySingleton = null;
public static LazySingleton getInstance() {
if (lazySingleton == null) {
synchronized (LazySingleton.class) {
if(lazySingleton == null){
lazySingleton =new LazySingleton();
}
}
}
return lazySingleton;
}
}
登入後複製

通过DEBUG,我们来看一下下图:

一個單例模式,沒必要這麼卷吧

双重校验锁

这里引申一个常见的问题,大家在面试的时候估计也会碰到。问题:为什么要double check?去掉第二次check行不行?

回答:当2个线程同时执行getInstance方法时,都会执行第一个if判断,由于锁机制的存在,会有一个线程先进入同步语句,而另一个线程等待,当第一个线程执行了new Singleton()之后,就会退出synchronized的保护区域,这时如果没有第二重if判断,那么第二个线程也会创建一个实例,这就破坏了单例。

问题:这里为什么要加上volatile修饰关键字?回答:这里加上该关键字主要是为了防止”指令重排”。关于“指令重排”具体产生的原因我们这里不做细究,有兴趣的小伙伴可以自己去研究一下,我们这里只是去分析一下,“指令重排”所带来的影响。

lazySingleton =new LazySingleton();
登入後複製

这样一个看似简单的动作,其实从JVM层来看并不是一个原子性的行为,这里其实发生了三件事:

  • 给LazySingleton分配内存空间。
  • 调用LazySingleton的构造函数,初始化成员字段。
  • 将LazySingleton指向分配的内存空间(注意此时的LazySingleton就不是null了)

在此期间存在着指令重排序的优化,第2、3步的顺序是不能保证的,最后的执行顺序可能是1-2-3,也可能是1-3-2,假如执行顺序是1-3-2,我们看看会出现什么问题。看一下下图:

一個單例模式,沒必要這麼卷吧

指令重排执行

从上图中我们看到虽然LazySingleton不是null,但是指向的空间并没有初始化,最终被业务使用的时候还是会报错,这就是DCL失效的问题,这种问题难以跟踪难以重现可能会隐藏很久。

JDK1.5之前JMM(Java Memory Model,即Java内存模型)中的Cache、寄存器到主存的回写规定,上面第二第三的顺序无法保证。JDK1.5之后,SUN官方调整了JVM,具体化了volatile关键字,private volatile static LazySingleton lazySingleton;只要加上volatile,就可以保证每次从主存中读取(这涉及到CPU缓存一致性问题,感兴趣的小伙伴可以研究研究),也可以防止指令重排序的发生,避免拿到未完成初始化的对象。

上面这种方式可以有效降低锁的竞争,锁不会将整个方法全部锁定,而是锁定了某个代码块。其实完全做完调试之后我们还是会发现锁争夺的问题并没有完全解决,用到了锁肯定会对整个代码的执行效率带来一定的影响。所以是否存在保证线程的安全,并且能够不浪费内存完美的解决方案呢?一起看下下面的解决方案。

内部静态类单例模式

这种方式其实是利用了静态对象创建的特性来解决上述内存浪费以及线程不安全的问题。在这里我们要弄清楚,被static修饰的属性,类加载的时候,基本属性就已经加载完毕,但是静态方法却不会加载的时候自动执行,而是等到被调用之后才会执行。并且被STATIC修饰的变量JVM只为静态分配一次内存。(这里老猫不展开去聊static相关知识点,有兴趣的小伙伴也可以自行去了解一下更多JAVA中static关键字修饰之后的类、属性、方法的加载机制以及存储机制)

所以综合这一特性,我们就有了下面这样的写法:

public class LazyInnerClassSingleton {
private LazyInnerClassSingleton () {
}

public static final LazyInnerClassSingleton getInstance() {
return LazyHolder.LAZY;
}

private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
登入後複製

上面这种写法,其实也属于“懒汉式单例模式”,并且这种模式相对于“无脑加锁”以及“DCL”以及“饿汉式单例模式”来说无疑是最优的一种实现方式。

但是深度去追究的话,其实这种方式也会有问题,这种写法并不能防止反序列化和反射生成多个实例。我们简单看一下反射的破坏的测试类:

public class DestructionSingletonTest {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class enumSingletonClass = LazyInnerClassSingleton.class;
//枚举默认有个String 和 int 类型的构造器
Constructor constructor = enumSingletonClass.getDeclaredConstructor();
constructor.setAccessible(true);
//利用反射调用构造方法两次直接创建两个对象,直接破坏单例模式
LazyInnerClassSingleton singleton1 = (LazyInnerClassSingleton) constructor.newInstance();
LazyInnerClassSingleton singleton2 = (LazyInnerClassSingleton) constructor.newInstance();
}
}
登入後複製

这里序列化反序列化单例模式破坏老猫偷个懒,因为下面会有写到,有兴趣的小伙伴继续看下文,老猫觉得这种破坏场景在真实的业务使用场景比较极端,如果不涉及底层框架变动,光从业务角度来看,上面这些单例模式的实现已经管够了。当然如果硬是要防止上面的反射创建单例两次问题也能解决,如下:

public class LazyInnerClassSingleton {
private LazyInnerClassSingleton () {
if(LazyHolder.LAZY != null) {
throw new RuntimeException("不允许创建多个实例");
}
}

public static final LazyInnerClassSingleton getInstance() {
return LazyHolder.LAZY;
}

private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
登入後複製

写到这里,可能大家都很疑惑了,咋还没提及用单例模式优化线程池创建。下面这不来了么,老猫个人觉得上面的这种方式进行创建单例还是比较好的,所以就用这种方式重构一下线程池的创建,具体代码如下:

public class InnerClassLazyThreadPoolHelper {
public static void execute(Runnable runnable) {
ThreadPoolExecutor threadPoolExecutor = ThreadPoolHelperHolder.THREAD_POOL_EXECUTOR;
threadPoolExecutor.execute(runnable);
}
/**
 * 静态内部类创建实例(单例).
 * 优点:被调用时才会创建一次实例
 */
public static class ThreadPoolHelperHolder {
private static final int CPU = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU + 1;
private static final int MAXIMUM_POOL_SIZE = CPU * 2 + 1;
private static final long KEEP_ALIVE_TIME = 1L;
private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS;
private static final int MAX_QUEUE_NUM = 1024;

private ThreadPoolHelperHolder() {
}

private static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME, TIME_UNIT,
new LinkedBlockingQueue(MAX_QUEUE_NUM),
new ThreadPoolExecutor.AbortPolicy());
}
}
登入後複製

以上是一個單例模式,沒必要這麼卷吧的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

清華大學與智譜AI重磅開源 GLM-4:掀起自然語言處理新革命 清華大學與智譜AI重磅開源 GLM-4:掀起自然語言處理新革命 Jun 12, 2024 pm 08:38 PM

自2023年3月14日開始,ChatGLM-6B以來,GLM系列模型受到了廣泛的關注與認可。特別是在ChatGLM3-6B開源之後,開發者對智譜AI推出的第四代模型充滿了期待。而這項期待,隨著GLM-4-9B的發布,終於得到了充分的滿足。 GLM-4-9B的誕生為了賦予小模型(10B及以下)更加強大的能力,GLM技術團隊經過近半年的探索,推出了這款全新的第四代GLM系列開源模型:GLM-4-9B。這一模型在確保精度的同時,大幅度壓縮了模型大小,具有更快的推理速度和更高的效率。 GLM技術團隊的探索沒

Java框架中設計模式與架構模式的區別 Java框架中設計模式與架構模式的區別 Jun 02, 2024 pm 12:59 PM

在Java框架中,設計模式和架構模式的區別在於:設計模式定義了在軟體設計中解決常見問題的抽象解決方案,專注於類別和物件之間的交互,例如工廠模式。架構模式定義了系統結構和模組之間的關係,關注系統元件的組織和交互,如分層架構。

Mistral 開源程式碼模型奪得王座! Codestral瘋狂訓練超80種語言,國內通義開發者請求出戰! Mistral 開源程式碼模型奪得王座! Codestral瘋狂訓練超80種語言,國內通義開發者請求出戰! Jun 08, 2024 pm 09:55 PM

出品|51CTO技術棧(微訊號:blog51cto)Mistral發布了首個程式碼模型Codestral-22B!該模型的瘋狂之處不僅在於訓練了80多種程式語言,包括許多程式碼模型忽略的Swift等。他們的速度沒有完全一致。要求使用Go語言編寫一個「發布/訂閱」系統。這裡的GPT-4o正在輸出,Codestral已經快到看不清楚的速度交捲了!由於該模型剛剛推出,尚未公開測試。但根據Mistral的負責人說法,Codestral是目前表現最佳的開源程式碼模型。圖片有興趣的朋友可以移步:-抱抱臉:https

Java設計模式之裝飾器模式剖析 Java設計模式之裝飾器模式剖析 May 09, 2024 pm 03:12 PM

裝飾器模式是一種結構型設計模式,允許動態添加物件功能,無需修改原始類別。它透過抽象組件、具體組件、抽象裝飾器和具體裝飾器的協作實現,可以靈活擴展類別功能,滿足變化的需求。範例中,將牛奶和摩卡裝飾器添加到Espresso,總價為2.29美元,展示了裝飾器模式在動態修改物件行為方面的強大功能。

設計模式如何應對程式碼維護難題 設計模式如何應對程式碼維護難題 May 09, 2024 pm 12:45 PM

設計模式透過提供可重複使用和可擴展的解決方案來解決程式碼維護難題:觀察者模式:允許物件訂閱事件,並在事件發生時收到通知。工廠模式:提供了一種創建物件的集中式方式,而無需依賴特定類別。單例模式:確保一個類別只有一個實例,用於建立全域可存取的物件。

從前端轉型後端開發,學習Java還是Golang更有前景? 從前端轉型後端開發,學習Java還是Golang更有前景? Apr 02, 2025 am 09:12 AM

後端學習路徑:從前端轉型到後端的探索之旅作為一名從前端開發轉型的後端初學者,你已經有了nodejs的基礎,...

Java設計模式之適配器模式的妙用 Java設計模式之適配器模式的妙用 May 09, 2024 pm 12:54 PM

適配器模式是一種結構型設計模式,允許不相容物件協同工作,它將一個介面轉換為另一個,使物件能夠順利互動。物件適配器透過建立包含被適配器對象的適配器對象,並實現目標接口,實現適配器模式。在一個實戰案例中,透過適配器模式,客戶端(如MediaPlayer)可以播放高級格式的媒體(如VLC),儘管本身僅支援普通媒體格式(如MP3)。

PHP設計模式:測試驅動開發實踐 PHP設計模式:測試驅動開發實踐 Jun 03, 2024 pm 02:14 PM

TDD用於編寫高品質PHP程式碼,步驟包括:編寫測試案例,描述預期功能並使其失敗。編寫程式碼,僅使測試案例通過,無需過度優化或詳細設計。測試案例通過後,優化和重構程式碼以提高可讀性、可維護性和可擴展性。

See all articles