單例模式(Singleton Pattern)是 Java 中最簡單的設(shè)計(jì)模式之一。這種類型的設(shè)計(jì)模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對(duì)象的最佳方式。
這種模式涉及到一個(gè)單一的類,該類負(fù)責(zé)創(chuàng)建自己的對(duì)象,同時(shí)確保只有單個(gè)對(duì)象被創(chuàng)建。這個(gè)類提供了一種訪問其唯一的對(duì)象的方式,可以直接訪問,不需要實(shí)例化該類的對(duì)象。
注意:
1、單例類只能有一個(gè)實(shí)例。
2、單例類必須自己創(chuàng)建自己的唯一實(shí)例。
3、單例類必須給所有其他對(duì)象提供這一實(shí)例。
單例模式常用的兩種:餓漢式單例模式;懶漢式單例模式。
1.餓漢式單例模式:
顧名思義餓漢式單例模式是說在類加載的時(shí)候就已經(jīng)將類給初始化在內(nèi)存中,從而后續(xù)的調(diào)用的都是從內(nèi)存中獲取這一個(gè)類的初始化實(shí)例對(duì)象,所有引用的都指向同一個(gè)地址。
餓漢式單例模式:
/**
* @author:
* @date: 2022/2/21 15:45
* @description: 餓漢式單例模式:類加載的時(shí)候就會(huì)自動(dòng)構(gòu)造產(chǎn)生一個(gè)單例對(duì)象
*/
public class HungrySingleton {
/**
* 定義類屬性
*/
private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();
/**
* 私有化構(gòu)造方法
*/
private HungrySingleton() {
}
/**
* 對(duì)外暴露拿到實(shí)例的方法
* @return
*/
public static HungrySingleton getInstance(){
return HUNGRY_SINGLETON;
}
}
單線程測試類:
public class Test {
public static void main(String[] args) throws Exception {
HungrySingleton singleton1 = HungrySingleton.getInstance();
HungrySingleton singleton2 = HungrySingleton.getInstance();
System.out.println(singleton1);
System.out.println(singleton2);
}
}
打印結(jié)果:
design.singleton.HungrySingleton@2b193f2d
design.singleton.HungrySingleton@2b193f2d
Process finished with exit code 0
多線程測試:
*/
public class Test {
public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i < 1000; i++) {
new Thread(()->{
Test.println();
}).start();
countDownLatch.await();
}
countDownLatch.countDown();
}
private static void println(){
System.out.println(HungrySingleton.getInstance());
}
}
多線程測試結(jié)果:

兩種測試結(jié)果(多線程的可以多測試幾次),返回的結(jié)果都是一樣的,說明餓漢式單例模式是線程安全的。
2.懶漢式單例模式:
在類加載時(shí),就給該類創(chuàng)建一個(gè)可用的內(nèi)存空間,但是具體的實(shí)例對(duì)象需要在使用時(shí)自己用new的方式初始化一次,之后所有其他引用的實(shí)例地址均指向第一次new的地址。
/**
* @author:
* @date: 2022/2/21 18:09
* @description: 懶漢式單例模式
*/
public class LazyUnSafeSingleton {
/**
* 類成員屬性
*/
private static LazyUnSafeSingleton instance;
/**
* 私有化構(gòu)造方法
*/
private LazyUnSafeSingleton() {
}
/**
* 對(duì)外暴露拿到實(shí)例的方法
* @return
*/
public static LazyUnSafeSingleton getInstance() {
if (instance == null) {
instance = new LazyUnSafeSingleton();
}
return instance;
}
}
一般測試:
public class Test {
public static void main(String[] args) throws Exception {
LazyUnSafeSingleton singleton1 = LazyUnSafeSingleton.getInstance();
LazyUnSafeSingleton singleton2 = LazyUnSafeSingleton.getInstance();
System.out.println(singleton1);
System.out.println(singleton2);
}
}
一般測試結(jié)果:
design.singleton.LazyUnSafeSingleton@2b193f2d
design.singleton.LazyUnSafeSingleton@2b193f2d
Process finished with exit code 0
可以看出singleton1與singleton2的地址是一樣的,兩次實(shí)例的對(duì)象引用地址一致。但是第一次引用時(shí),才開始new一個(gè)實(shí)例,之前在類加載過程中并未直接實(shí)例化。
多線程測試:
public class Test {
public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i < 1000; i++) {
new Thread(()->{
Test.println();
}).start();
countDownLatch.await();
}
countDownLatch.countDown();
}
private static void println(){
System.out.println(LazyUnSafeSingleton.getInstance());
}
}
多線程測試結(jié)果:

一般測試沒問題,多線程測試出現(xiàn)了對(duì)象不一樣的情況!,說明多線程下的一般懶漢單例模式屬于線程不安全的。
一般的懶漢式單例模式優(yōu)化后
演化成雙重檢查鎖單例模式(DCL)
/**
* @author:
* @date: 2022/2/21 19:25
* @description:
*/
public class LazySafeSingleton {
/**
* 類成員屬性
*/
private static volatile LazySafeSingleton instance;
/**
* 私有化構(gòu)造方法
*/
private LazySafeSingleton() {
}
/**
* 對(duì)外暴露拿到實(shí)例的方法
* @return
*/
public static LazySafeSingleton getInstance() {
if (instance == null) {
synchronized (LazySafeSingleton.class) {
if (instance == null) {
instance = new LazySafeSingleton();
}
}
}
return instance;
}
}
多線程測試:
public class Test {
public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i < 1000; i++) {
new Thread(()->{
Test.println();
}).start();
countDownLatch.await();
}
countDownLatch.countDown();
}
private static void println(){
System.out.println(LazySafeSingleton.getInstance());
}
}
多線程測試結(jié)果:

可以看到結(jié)果如我們的預(yù)期,大家可以多次執(zhí)行看看會(huì)不會(huì)出現(xiàn)不一樣的對(duì)象。
這兩種單例真的就不能被創(chuàng)建出多例嗎?
我們嘗試用反射去創(chuàng)建對(duì)象,看看效果
代碼:
public class Test {
public static void main(String[] args) throws Exception {
// 通過單例模式拿到對(duì)象
HungrySingleton singleton1 = HungrySingleton.getInstance();
// 通過反射創(chuàng)建對(duì)象
Class singletonClass = HungrySingleton.class;
Constructor[] cts = singletonClass.getDeclaredConstructors();
// 訪問權(quán)限打開setAccessible(true),就可以訪問私有構(gòu)造函數(shù)
cts[0].setAccessible(true);
HungrySingleton singleton2 = (HungrySingleton)cts[0].newInstance();
System.out.println(singleton1);
System.out.println(singleton2);
}
}
執(zhí)行結(jié)果:
design.singleton.HungrySingleton@2b193f2d
design.singleton.HungrySingleton@355da254
Process finished with exit code 0
兩種方式拿到的對(duì)象地址不一樣,說明不是一個(gè)對(duì)象。
結(jié)論:
一般的餓漢式和懶漢式單例模式都是可以被反射給破壞的。
如何解決?
可以使用 枚舉類單例模式 解決這個(gè)問題
枚舉類單例模式:
/**
* @author:
* @date: 2022/2/21 15:45
* @description: 枚舉式單例模式
*/
public enum EnumSingleton {
/**
* 枚舉自身
*/
INSTANCE;
/**
* 做業(yè)務(wù)邏輯
* @return
*/
public void doSomeThing(){
System.out.println("實(shí)現(xiàn)你這個(gè)類需要做的一些事");
}
}
測試類:
/**
* @author: dingshitai
* @date: 2022/2/21 15:50
* @description:
*/
public class Test {
public static void main(String[] args) throws Exception {
// 只能通過這種方式去調(diào)用類里面的方法
EnumSingleton.INSTANCE.doSomeThing();
}
可以看出我們使用了枚舉類單例模式,只能用這種方式去調(diào)用類內(nèi)部的方法。
那可以被反射破壞嗎?
不能。
上代碼:
public class Test {
public static void main(String[] args) throws Exception {
// 通過反射創(chuàng)建對(duì)象
Class EnumSingletonClass = EnumSingleton.class;
Constructor[] cts = EnumSingletonClass.getDeclaredConstructors();
// 訪問權(quán)限打開setAccessible(true),就可以訪問私有構(gòu)造函數(shù)
cts[0].setAccessible(true);
EnumSingleton singleton1 = (EnumSingleton) cts[0].newInstance();
System.out.println(singleton1);
}
}
測試結(jié)果:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at design.singleton.Test.main(Test.java:18)
Process finished with exit code 1
枚舉類是不能被反射創(chuàng)建出實(shí)例對(duì)象的??!
為啥呢?
原因就在這行
EnumSingleton singleton1 = (EnumSingleton) cts[0].newInstance();
進(jìn)入 newInstance() 方法中
可以看到反射的源碼:

類對(duì)象類型是ENUM類型的時(shí)候,會(huì)直接報(bào)出錯(cuò)誤,從而阻止反射生成實(shí)例化對(duì)象,這就是最根本的原因。
所以如果想要用到單例模式,既考慮到安全性又考慮到并發(fā)性,建議使用枚舉類單例模式。
最后聊一聊spring源碼中用到單例模式的地方
在DefaultSingletonBeanRegistry這個(gè)注冊類中去根據(jù)beanName生成對(duì)象的時(shí)候

spring用到了單例模式。