Java單例模式的8種寫法(推薦)
單例:Singleton,是指僅僅被實(shí)例化一次的類。
餓漢單例設(shè)計(jì)模式
一、餓漢設(shè)計(jì)模式
public class SingletonHungry {
private final static SingletonHungry INSTANCE = new SingletonHungry();
private SingletonHungry() {
}
public static SingletonHungry getInstance() {
return INSTANCE;
}
}
因?yàn)閱卫龑?duì)象一開始就初始化了,不會(huì)出現(xiàn)線程安全的問(wèn)題。
PS:因?yàn)槲覀冎恍枰跏蓟?次,所以給INSTANCE加了final關(guān)鍵字,表明初始化1次后不再允許初始化。
懶漢單例設(shè)計(jì)模式
二、簡(jiǎn)單懶漢設(shè)計(jì)模式
由于餓漢模式一開始就初始化好了,但如果一直沒有被使用到的話,是會(huì)浪費(fèi)珍貴的內(nèi)存資源的,所以引出了懶漢模式。
懶漢:首次使用時(shí)才會(huì)去實(shí)例化對(duì)象。
public class SingletonLazy1 {
private static SingletonLazy1 instance;
private SingletonLazy1() {
}
public static SingletonLazy1 getInstance() {
if (instance == null) {
instance = new SingletonLazy1();
}
return instance;
}
}
測(cè)試:
public class Main {
public static void main(String[] args) {
SingletonLazy1 instance1 = SingletonLazy1.getInstance();
SingletonLazy1 instance2 = SingletonLazy1.getInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
測(cè)試結(jié)果:從結(jié)果可以看出,打印出來(lái)的兩個(gè)實(shí)例對(duì)象地址是一樣的,所以認(rèn)為是只創(chuàng)建了一個(gè)對(duì)象。

三、進(jìn)階
1:解決多線程并發(fā)問(wèn)題
上述代碼存在的問(wèn)題:在多線程環(huán)境下,不能保證只創(chuàng)建一個(gè)實(shí)例,我們進(jìn)行問(wèn)題的重現(xiàn):
public class Main {
public static void main(String[] args) {
new Thread(()-> System.out.println(SingletonLazy1.getInstance())).start();
new Thread(()-> System.out.println(SingletonLazy1.getInstance())).start();
}
}
結(jié)果:獲取到的對(duì)象不一樣,這并不是我們的預(yù)期結(jié)果。

解決方案:
public class SingletonLazy2 {
private static SingletonLazy2 instance;
private SingletonLazy2() {
}
//在方法加synchronized修飾符
public static synchronized SingletonLazy2 getInstance() {
if (instance == null) {
instance = new SingletonLazy2();
}
return instance;
}
}
測(cè)試:
public class Main2 {
public static void main(String[] args) {
new Thread(()-> System.out.println(SingletonLazy2.getInstance())).start();
new Thread(()-> System.out.println(SingletonLazy2.getInstance())).start();
new Thread(()-> System.out.println(SingletonLazy2.getInstance())).start();
new Thread(()-> System.out.println(SingletonLazy2.getInstance())).start();
}
}

結(jié)果:多線程環(huán)境下獲取到的是同個(gè)對(duì)象。
四、進(jìn)階2:縮小方法鎖粒度
上一方案雖然解決了多線程問(wèn)題,但由于synchronized關(guān)鍵字是加在方法上的,鎖粒度很大,當(dāng)有上萬(wàn)甚至更多的線程同時(shí)訪問(wèn)時(shí),都被攔在了方法外,大大降低了程序性能,所以我們要適當(dāng)縮小鎖粒度,控制鎖的范圍在代碼塊上。
public class SingletonLazy3 {
private static SingletonLazy3 instance;
private SingletonLazy3() {
}
public static SingletonLazy3 getInstance() {
//代碼塊1:不要在if外加鎖,不然和鎖方法沒什么區(qū)別
if (instance == null) {
//代碼塊2:加鎖,將方法鎖改為鎖代碼塊
synchronized (SingletonLazy3.class) {
//代碼塊3
instance = new SingletonLazy3();
}
}
return instance;
}
}
測(cè)試:
public class Main3 {
public static void main(String[] args) {
new Thread(()-> System.out.println(SingletonLazy3.getInstance())).start();
new Thread(()-> System.out.println(SingletonLazy3.getInstance())).start();
new Thread(()-> System.out.println(SingletonLazy3.getInstance())).start();
new Thread(()-> System.out.println(SingletonLazy3.getInstance())).start();
}
}
我們看一下運(yùn)行結(jié)果:還是出現(xiàn)了線程安全的問(wèn)題(每次執(zhí)行都可能打印不同的地址情況,只要證明是非線程安全的即可)。

原因分析:當(dāng)線程A拿到鎖進(jìn)入到
代碼塊3并且還沒有創(chuàng)建完實(shí)例時(shí),線程B是有機(jī)會(huì)到達(dá)代碼塊2的,此時(shí)線程C和D可能在代碼塊1,當(dāng)線程A執(zhí)行完之后釋放鎖并返回對(duì)象1,線程B進(jìn)入進(jìn)入代碼塊3,又創(chuàng)建了新的對(duì)象2覆蓋對(duì)象1并返回,最后當(dāng)線程C和D在進(jìn)行判null時(shí)發(fā)現(xiàn)instance非空,直接返回最后創(chuàng)建的對(duì)象2。
五、進(jìn)階3:雙重檢查鎖DCL(Double-Checked-Locking)
所謂雙重檢查鎖,就是在線程獲取到鎖之后再對(duì)實(shí)例進(jìn)行第2次判空檢查,判斷是不是有上一個(gè)線程已經(jīng)進(jìn)行了實(shí)例化,有的話直接返回即可,否則進(jìn)行實(shí)例初始化。
public class SingletonLazy4DCL {
private static SingletonLazy4DCL instance;
private SingletonLazy4DCL() {
}
public static SingletonLazy4DCL getInstance() {
//代碼塊1:第一次判空檢查
if (instance == null) {
//代碼塊2:加鎖,將方法鎖改為鎖代碼塊
synchronized (SingletonLazy3.class) {
//代碼塊3:進(jìn)行第二次(雙重)判空檢查
if (instance == null) {
instance = new SingletonLazy4DCL();
}
}
}
return instance;
}
}
測(cè)試:
public class Main4DCL {
public static void main(String[] args) {
new Thread(()-> System.out.println(SingletonLazy4DCL.getInstance())).start();
new Thread(()-> System.out.println(SingletonLazy4DCL.getInstance())).start();
new Thread(()-> System.out.println(SingletonLazy4DCL.getInstance())).start();
new Thread(()-> System.out.println(SingletonLazy4DCL.getInstance())).start();
}
}

六、進(jìn)階4:禁止指令重排
在對(duì)象的實(shí)例過(guò)程中,大概可分為以下3個(gè)步驟:
- 分配對(duì)象內(nèi)存空間
- 在空間中創(chuàng)建對(duì)象
- 實(shí)例指向分配到的內(nèi)存空間地址
由于實(shí)例化對(duì)象的過(guò)程不是原子性的,且JVM本身對(duì)Java代碼指令有重排的操作,可能1-2-3的操作被重新排序成了1-3-2,這樣就會(huì)導(dǎo)致在3執(zhí)行完之后還沒來(lái)得及創(chuàng)建對(duì)象時(shí),其他線程先讀取到了未初始化的對(duì)象instance并提前返回,在使用的時(shí)候會(huì)出現(xiàn)NPE空指針異常。
解決:給instance加volatile關(guān)鍵字表明禁止指令重排,出現(xiàn)的概率不大, 但這是更安全的一種做法。
public class SingletonLazy5Volatile {
//加volatile關(guān)鍵字
private volatile static SingletonLazy5Volatile instance;
private SingletonLazy5Volatile() {
}
public static SingletonLazy5Volatile getInstance() {
//代碼塊1
if (instance == null) {
//代碼塊2:加鎖,將方法鎖改為鎖代碼塊
synchronized (SingletonLazy3.class) {
//代碼塊3
if (instance == null) {
instance = new SingletonLazy5Volatile();
}
}
}
return instance;
}
}
七、進(jìn)階5:靜態(tài)內(nèi)部類
我們還可以使用靜態(tài)類的靜態(tài)變量被第一次訪問(wèn)時(shí)才會(huì)進(jìn)行初始化的特性來(lái)進(jìn)行懶加載初始化。把外部類的單例對(duì)象放到靜態(tài)內(nèi)部類的靜態(tài)成員變量里進(jìn)行初始化。
public class SingletonLazy6InnerStaticClass {
private SingletonLazy6InnerStaticClass() {
}
public static SingletonLazy6InnerStaticClass getInstance() {
return SingletonLazy6InnerStaticClass.InnerStaticClass.instance;
//或者寫成return InnerStaticClass.instance;
}
private static class InnerStaticClass {
private static final SingletonLazy6InnerStaticClass instance = new SingletonLazy6InnerStaticClass();
}
}
雖然靜態(tài)內(nèi)部類里的寫法和餓漢模式很像,但它卻不是在外部類加載時(shí)就初始化了,而是在第一次被訪問(wèn)到時(shí)才會(huì)進(jìn)行初始化的操作(即getInstance方法被調(diào)用時(shí)),也就起到了懶加載的效果,并且它可以保證線程安全。
測(cè)試:
public class Main6InnerStatic {
public static void main(String[] args) {
new Thread(()-> System.out.println(SingletonLazy6InnerStaticClass.getInstance())).start();
new Thread(()-> System.out.println(SingletonLazy6InnerStaticClass.getInstance())).start();
new Thread(()-> System.out.println(SingletonLazy6InnerStaticClass.getInstance())).start();
new Thread(()-> System.out.println(SingletonLazy6InnerStaticClass.getInstance())).start();
}
}

反射攻擊
雖然我們一開始都對(duì)構(gòu)造器進(jìn)行了私有化處理,但Java本身的反射機(jī)制卻還是可以將private訪問(wèn)權(quán)限改為可訪問(wèn),依舊可以創(chuàng)建出新的實(shí)例對(duì)象,這里以餓漢模式舉例說(shuō)明:
public class MainReflectAttack {
public static void main(String[] args) {
try {
SingletonHungry normal1 = SingletonHungry.getInstance();
SingletonHungry normal2 = SingletonHungry.getInstance();
//開始反射創(chuàng)建實(shí)例
Constructor<SingletonHungry> reflect = SingletonHungry.class.getDeclaredConstructor(null);
reflect.setAccessible(true);
SingletonHungry attack = reflect.newInstance();
System.out.println("正常靜態(tài)方法調(diào)用獲取到的對(duì)象:");
System.out.println(normal1);
System.out.println(normal2);
System.out.println("反射獲取到的對(duì)象:");
System.out.println(attack);
} catch (Exception e) {
e.printStackTrace();
}
}
}

八、枚舉單例(推薦使用)
public enum SingletonEnum {
INSTANCE;
}
枚舉是最簡(jiǎn)潔、線程安全、不會(huì)被反射創(chuàng)建實(shí)例的單例實(shí)現(xiàn),《Effective Java》中也表明了這種寫法是最佳的單例實(shí)現(xiàn)模式。
單元素的枚舉類型經(jīng)常成為實(shí)現(xiàn)Singleton的最佳方法。 --《Effective Java》
為什么說(shuō)不會(huì)被反射創(chuàng)建對(duì)象呢?查閱構(gòu)造器反射實(shí)例化對(duì)象方法newInstance的源碼可知:反射禁止了枚舉對(duì)象的實(shí)例化,也就防止了反射攻擊,不用自己在構(gòu)造器實(shí)現(xiàn)復(fù)雜的重復(fù)實(shí)例化邏輯了。

測(cè)試:
public class MainEnum {
public static void main(String[] args) {
SingletonEnum instance1 = SingletonEnum.INSTANCE;
SingletonEnum instance2 = SingletonEnum.INSTANCE;
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}

總結(jié):幾種實(shí)現(xiàn)方式的優(yōu)缺點(diǎn) 懶漢模式
優(yōu)點(diǎn):節(jié)省內(nèi)存。
缺點(diǎn):存在線程安全問(wèn)題,若要保證線程安全,則寫法復(fù)雜。
餓漢模式
優(yōu)點(diǎn):線程安全。
缺點(diǎn):如果單例對(duì)象一直沒被使用,則會(huì)浪費(fèi)內(nèi)存空間。
靜態(tài)內(nèi)部類
優(yōu)點(diǎn):懶加載并避免了多線程問(wèn)題,寫法相比于懶漢模式更簡(jiǎn)單。
缺點(diǎn):需要多創(chuàng)建一個(gè)內(nèi)部類。
枚舉
優(yōu)點(diǎn):簡(jiǎn)潔、天生線程安全、不可反射創(chuàng)建實(shí)例。
缺點(diǎn):暫無(wú)
到此這篇關(guān)于Java單例模式的8種寫法的文章就介紹到這了,更多相關(guān)Java單例模式內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
maven tomcat plugin實(shí)現(xiàn)熱部署
這篇文章主要介紹了maven tomcat plugin實(shí)現(xiàn)熱部署,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-07-07
mybatis plus 開啟sql日志打印的方法小結(jié)
Mybatis-Plus(簡(jiǎn)稱MP)是一個(gè) Mybatis 的增強(qiáng)工具,在 Mybatis 的基礎(chǔ)上只做增強(qiáng)不做改變,為簡(jiǎn)化開發(fā)、提高效率而生。本文重點(diǎn)給大家介紹mybatis plus 開啟sql日志打印的方法小結(jié),感興趣的朋友一起看看吧2021-09-09
詳解SpringBoot中Session超時(shí)原理說(shuō)明
本篇文章主要介紹了詳解SpringBoot中Session超時(shí)原理說(shuō)明,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-08-08
詳解java如何實(shí)現(xiàn)將數(shù)據(jù)導(dǎo)出為yaml
這篇文章主要為大家詳細(xì)介紹了java如何利用snakeyaml和freemarker實(shí)現(xiàn)將數(shù)據(jù)導(dǎo)出為yaml文件,文中的示例代碼講解詳細(xì),有需要的小伙伴可以參考一下2023-11-11
java8 stream sort自定義復(fù)雜排序案例
這篇文章主要介紹了java8 stream sort自定義復(fù)雜排序案例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-10-10
使用?Spring?AI?+?Ollama?構(gòu)建生成式?AI?應(yīng)用的方法
通過(guò)集成SpringBoot和Ollama,本文詳細(xì)介紹了如何構(gòu)建生成式AI應(yīng)用,首先,介紹了AI大模型服務(wù)的兩種實(shí)現(xiàn)方式,選擇使用ollama進(jìn)行部署,隨后,通過(guò)SpringBoot+SpringAI來(lái)實(shí)現(xiàn)應(yīng)用構(gòu)建,本文為開發(fā)者提供了一個(gè)實(shí)用的指南,幫助他們快速入門生成式AI應(yīng)用的開發(fā)2024-11-11
運(yùn)用springboot搭建并部署web項(xiàng)目的示例
這篇文章主要介紹了運(yùn)用springboot搭建并部署web項(xiàng)目的示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-06-06

