單例模式應(yīng)該是我們平時聽過最多的設(shè)計模式,也是最簡單的設(shè)計模式疆偿,面試的時候也經(jīng)常會被問到,有的面試官還動不動讓你手寫一個單例模式搓幌。我們來一起了解一下單例模式吧杆故。
一、什么是單例模式溉愁?
單例顧名思義就是單個的實例(對象)处铛。單例模式屬于創(chuàng)建型模式,它只涉及到單個的類,不通過new的方式創(chuàng)建對象撤蟆,而是由類本身提供的方法創(chuàng)建和訪問自己的對象奕塑,同時保證對象不會被多次創(chuàng)建,多次訪問該方法時訪問的是同一個對象枫疆。
二爵川、單例模式的優(yōu)缺點
優(yōu)點:
1.有效的減少資源消耗。
2.方便對象狀態(tài)的共享息楔。
3.避免對資源的多重占用(如寫文件操作時文件被多個對象占用)寝贡。
缺點:
1.因為狀態(tài)共享,所以不適合同一個類需要在不同場景保存各自狀態(tài)的情況值依。
2.不能繼承(登記式除外)圃泡,因此無法通過子類擴(kuò)展功能。
3.違反了單一職責(zé)原則愿险。因為實例化自身的職責(zé)是由自身完成的颇蜡。
4.對于保存狀態(tài)的單例對象,如果長時間未調(diào)用辆亏,對象有可能會被回收導(dǎo)致狀態(tài)丟失风秤。
三、單例模式的應(yīng)用場景版本
單例模式的主要應(yīng)用場景如下:
1.對象需要頻繁創(chuàng)建扮叨、銷毀的時候缤弦,例如被spring管理的bean默認(rèn)都是單例模式,如果不是單例模式我們每次http請求的時候就需要創(chuàng)建很多對象彻磁,請求結(jié)束又要銷毀這些對象碍沐。
2.創(chuàng)建對象需要時消耗時間較長或消耗資源較多且使用較頻繁的對象。例如一些從文件讀取數(shù)據(jù)的配置類衷蜓,i/o操作就比較耗時耗資源累提。
3.需要全局共享狀態(tài)或者資源的類,例如線程池磁浇、數(shù)據(jù)庫連接池斋陪、保存了狀態(tài)的工具類等。
四扯夭、單例模式的實現(xiàn)方式
1.懶漢式
懶漢式鳍贾,在系統(tǒng)啟動的時候不會實例化對象,只有在第一次獲取對象時才會創(chuàng)建對象.
1.1線程不安全
這種實現(xiàn)方式適合用于單線程場景交洗,多線程場景會創(chuàng)建多個實例骑科。
public class Singleton1 {
private static Singleton1 instance;
private Singleton1() {
System.out.println("調(diào)用構(gòu)造函數(shù)創(chuàng)建對象");
}
public static Singleton1 getInstance() {
if (instance == null) {
instance = new Singleton1();
}
return instance;
}
}
我們創(chuàng)建10個線程調(diào)用getInstance()方法進(jìn)行測試,代碼如下:
public static void main(String[] args) {
singleton1();
}
private static void singleton1(){
List<Thread> threads = new ArrayList<>(10);
for (int i = 0; i < 10; i++) {
threads.add(new Thread(Singleton1::getInstance));
}
threads.forEach(Thread::start);
}
測試結(jié)果如下:
Task :Test.main()
調(diào)用構(gòu)造函數(shù)創(chuàng)建對象
調(diào)用構(gòu)造函數(shù)創(chuàng)建對象
調(diào)用構(gòu)造函數(shù)創(chuàng)建對象
調(diào)用構(gòu)造函數(shù)創(chuàng)建對象
調(diào)用構(gòu)造函數(shù)創(chuàng)建對象
可以看出構(gòu)造函數(shù)被調(diào)用了多次构拳,也就是說對象被多次實例化咆爽,并未實現(xiàn)單例梁棠。
1.2線程安全
下述方式對getInstance()方法加了鎖,因此是線程安全的斗埂。加鎖會影響效率符糊,適用于getInstance()方法的性能對程序不是很重要的場景。
public class Singleton2 {
private static Singleton2 instance;
private Singleton2() {
System.out.println("調(diào)用構(gòu)造函數(shù)創(chuàng)建對象");
}
public static synchronized Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
我們創(chuàng)建10個線程調(diào)用getInstance()方法進(jìn)行測試呛凶,代碼如下:
public static void main(String[] args) {
singleton2();
}
private static void singleton2(){
List<Thread> threads = new ArrayList<>(10);
for (int i = 0; i < 10; i++) {
threads.add(new Thread(Singleton2::getInstance));
}
threads.forEach(Thread::start);
}
反復(fù)執(zhí)行多次結(jié)果均如下:
Task :Test.main()
調(diào)用構(gòu)造函數(shù)創(chuàng)建對象
可以看出對象只被創(chuàng)建了一次男娄,因此時線程安全的。
2.餓漢式
餓漢式是在類被加載的時候就創(chuàng)建好了對象漾稀,因此是線程安全的模闲,并且沒有加鎖,對不會影響程序效率崭捍,但是因為是在未使用之前加載的尸折,所以會浪費(fèi)一些內(nèi)存。
public class Singleton3 {
private static Singleton3 instance = new Singleton3();
private Singleton3() {
System.out.println("調(diào)用構(gòu)造函數(shù)創(chuàng)建對象");
}
public static Singleton3 getInstance() {
return instance;
}
}
3.雙重校驗鎖
這種方式采用雙鎖機(jī)制殷蛇,只鎖了創(chuàng)建對象的一部分代碼实夹,因此在多線程情況下依然能保持高性能。下面的例子粒梦,有的童鞋可能覺得和方法上加鎖性能差不多亮航,如果在標(biāo)記1和標(biāo)記4處有一些耗時操作,那么第一個線程在創(chuàng)建對象的時候第二個線程已經(jīng)可以進(jìn)入方法在標(biāo)記1處執(zhí)行耗時操作了匀们,等到第一個線程創(chuàng)建完對象兩個線程又可以同時執(zhí)行標(biāo)記4處的耗時操作塞赂,如果是方法上加鎖第二個線程必須等到第一個線程執(zhí)行完標(biāo)記1和標(biāo)記4處的耗時操作才能再進(jìn)入方法。
標(biāo)記3處的第二個null判斷的意義又是什么呢昼蛀?此處的null判斷非常關(guān)鍵,如果有兩個線程同時執(zhí)行到了標(biāo)記2處圆存,其中一個線程拿到了鎖叼旋,創(chuàng)建了對象,釋放鎖后另一個線程拿到鎖后如果沒有標(biāo)記3處的null判斷沦辙,第二個線程就會再創(chuàng)建一個對象夫植,就無法保證單例。
public class Singleton4 {
private volatile static Singleton4 instance = new Singleton4();
private Singleton4() {
System.out.println("調(diào)用構(gòu)造函數(shù)創(chuàng)建對象");
}
public static Singleton4 getInstance() {
//標(biāo)記1
if (instance == null){
//標(biāo)記2
synchronized (Singleton4.class){
//標(biāo)記3
if (instance==null){
instance = new Singleton4();
}
}
}
//標(biāo)記4
return instance;
}
}
4.登記式/靜態(tài)內(nèi)部類
這種方式的功效跟雙重校驗鎖一樣油讯,但實現(xiàn)方式更為簡單详民。利用了classloader加載類的原理,保證了初始化Singleton5時只有一個線程陌兑,同時保證了了初始化Singleton5時它的靜態(tài)內(nèi)部類SingletonInner不會被加載沈跨,這樣Singleton5對象(INSTANCE)也就不會被創(chuàng)建。
public class Singleton5 {
public static class SingletonInner {
public static final Singleton5 INSTANCE = new Singleton5();
}
private Singleton5() {
System.out.println("調(diào)用構(gòu)造函數(shù)創(chuàng)建對象");
}
public static Singleton5 getInstance() {
return SingletonInner.INSTANCE;
}
}
5.枚舉
這種方式利用了枚舉類本身的特性兔综,支持序列化機(jī)制可以在反序列化時避免重復(fù)創(chuàng)建對象饿凛,同時還能避免線程同步問題狞玛。但這種方式在實際使用中用的比較少。
public enum Singleton6 {
INSTANCE
//下面可以寫一些方法
}