前言
本篇文章主要介紹的是設計模式中的單例模式的實現(xiàn)方式。
什么是設計模式
設計模式其實就是前輩們長時間的試驗和錯誤總結出來的,針對軟件開發(fā)過程中面臨的一般問題的解決方案。
設計模式分類
根據(jù)其目的(模式是用來做什么的)可分為創(chuàng)建型,結構型和行為型三種:
? 創(chuàng)建型模式主要用于創(chuàng)建對象。
? 結構型模式主要用于處理類或對象的組合。
? 行為型模式主要用于描述對類或對象怎樣交互和怎樣分配職責。
單例模式
單例模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對象的最佳方式。這種模式保證一個系統(tǒng)中的某個類只有一個能夠被外界訪問的實例。
單例模式的使用場景
在程序中比較常用的是數(shù)據(jù)庫連接池、線程池、日志對象等等。
單例模式的實現(xiàn)
單例模式的實現(xiàn)有5種方式,分別是懶漢式、餓漢式、雙重鎖、靜態(tài)內(nèi)部類、枚舉。
1.懶漢式
這種方式是最基本的實現(xiàn)方式,但是不支持多線程,線程不安全。
實現(xiàn):定義一個私有的構造方法,定義一個該類靜態(tài)私有的變量,然后定義一個公共的靜態(tài)方法,在靜態(tài)方法內(nèi)對變量的值進行空判斷,不為空直接返回,如果為空重新構建并賦值給改該變量。
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
測試:
public class SingletonTest {
public static void main(String[] args) {
System.out.println(Singleton.getInstance()==Singleton.getInstance());
}
}
這里輸出的是true,可以看到兩次獲取的實例其實是同一個。
采用多線程方式:
public class SingletonTest {
public static void main(String[] args) {
new Thread(new SingletonThread()).start();
new Thread(new SingletonThread()).start();
}
}
class SingletonThread implements Runnable {
@Override
public void run() {
System.out.println(Singleton.getInstance().hashCode());
}
}
此時兩次輸出的哈希值時而相同時而不同,出現(xiàn)線程不安全問題。
這種方式可以在靜態(tài)方法的方法聲明上加synchronized關鍵字來確保線程安全,但是效率低下。
2.餓漢式
這種方式?jīng)]有加鎖,所以效率會提高。雖然沒有加鎖,但是也是線程安全的,這是因為它在類加載時就初始化了,而一個類在整個生命周期中只會被加載一次,因此該單例類只會創(chuàng)建一個實例。所以餓漢式天生就是線程安全的。但也正是因為它在類加載的時候就初始化了,會一直占用內(nèi)存,導致內(nèi)存浪費。
定義一個私有的構造方法,并將自身的實例對象設置為一個靜態(tài)私有屬性,然后通過公共的靜態(tài)方法調用返回實例。
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return singleton;
}
}
測試:和懶漢式的測試代碼一樣,普通方式獲取的兩個實例是同一個,輸出true,多線程方式獲取的哈希值是一樣的。
3.雙重鎖
定義一個私有構造方法,通過volatile定義靜態(tài)私有變量,保證了該變量的可見性,然后定義一個共有的靜態(tài)方法,第一次對該對象實例化時與否判斷,不為空直接返回,提升效率;然后使用synchronized 進行同步代碼塊,防止對象未初始化時,在多線程訪問該對象在第一次創(chuàng)建后,再次重復的被創(chuàng)建;然后第二次對該對象實例化時與否判斷,如果未初始化,則初始化,否則直接返回該實例。
第一次判空是為了提升效率,只有第一次實例化的時候需要加鎖,而不是每次請求都加鎖
第二次是為了進行同步,避免多線程問題。
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) { //#1
synchronized (Singleton.class){ //#2
if (singleton == null) { //#3
singleton = new Singleton(); //#4
System.out.println(Thread.currentThread().getName() + ": singleton is initalized...");//#5.1
} else {
System.out.println(Thread.currentThread().getName() + ": singleton is not null now...");//#5.2
}
}
}
return singleton;
}
}
雙重鎖這種方式實現(xiàn)單例的關鍵點在于兩次判空、加鎖、以及volatile關鍵字,這里解釋一下volatile關鍵字在這種方式實現(xiàn)單例起到的作用。
volatile有兩個特性
可見性:證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
有序性:禁止進行指令重排序。
假設不加volatile關鍵字,這段代碼可能輸出的是
thread1: uniqueInstance is initalized...
thread2: uniqueInstance is initalized...
過程分析:
1.thread1進入#1,獲取到singleton為空,此時thread1讓出CPU資源給thread2,thread1進入#1,卻在#2外等待。
2.thread2進入#1,獲取到singleton為空,此時thread2讓出CPU資源給thread1,
thread2進入#1,卻在#2外等待。
3.thread1會依次執(zhí)行#2,#3,#4,#5.1,最終在thread2里面實例化了singleton。thread1執(zhí)行完畢讓出CPU資源給thread2。
4.thread2接著#1跑下去,跑到#3的時候,由于#1里面拿到的singleton還是空(并沒有及時從thread1里面拿到最新的),所以thread2仍然會執(zhí)行#4,#5.1
5.最后在thread1和thread2都實例化了singleton。
這樣的話,singleton實例化了兩次。而volatile關鍵字修飾變量的作用就是讓第四步中thread2及時拿到thread1更新的的singleton,使它最后執(zhí)行#5.2,這里利用的就是可見性。
volatile使singleton重排序被禁止,所有的寫(write)操作都將發(fā)生在讀(read)操作之前。也就確保了thread1的實例化是發(fā)生在thread2第二次判空之前的。
當然,這只是一種假設的情況,沒有重現(xiàn)過,太難模擬了,但是確實存在。
4.靜態(tài)內(nèi)部類
這種方式也是利用了類加載機制,只不過它不像餓漢式一樣,Singleton類被加載就實例化,這樣就沒有達到懶加載的效果。外部類加載時并不需要立即加載內(nèi)部類,內(nèi)部類不被加載則不去初始化instance,因此不占內(nèi)存。而靜態(tài)內(nèi)部類實現(xiàn)單例保證線程安全,是由JVM決定的。
public class Singleton {
private Singleton() {}
private static class SingletonInner {
private static Singleton singleton = new Singleton();
}
public static Singleton getInstance() {
return SingletonInner.singleton;
}
}
5.枚舉
枚舉是JDK1.5之后的特性。無償提供序列化機制,防止多次實例化。
public enum Singleton {
INSTANCE;
}
測試:輸出的哈希值都是一樣的。
public class SingletonTest {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new SingletonThread()).start();
}
}
}
class SingletonThread implements Runnable {
@Override
public void run() {
System.out.println(Singleton.INSTANCE.hashCode());
}
}
PS:第2.3.4種測試代碼和第一種是一樣的。
總結:
一般情況下使用餓漢式;如果要求實現(xiàn)懶加載,則使用靜態(tài)內(nèi)部類;如果涉及到反序列化創(chuàng)建對象時,可以嘗試使用枚舉方式。
CSDN:https://blog.csdn.net/qq_27682773
簡書:http://www.itdecent.cn/u/e99381e6886e
博客園:https://www.cnblogs.com/lixianguo