一、簡述
單例模式是 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ì)象。即兩私一公:①私有的構(gòu)造函數(shù)②私有靜態(tài)實(shí)例屬性③公共的獲取實(shí)例的靜態(tài)方法。
1??注意
- 單例類只能有一個(gè)實(shí)例。
- 單例類必須自己創(chuàng)建自己的唯一實(shí)例。
- 單例類必須給所有其他對(duì)象提供這一實(shí)例。
2??資源加載和性能
餓漢式在類創(chuàng)建的同時(shí)就實(shí)例化一個(gè)靜態(tài)對(duì)象出來,無論會(huì)不會(huì)用到,都會(huì)占據(jù)一定的內(nèi)存。相應(yīng)的,在第一次調(diào)用時(shí)速度更快,因?yàn)槠滟Y源已經(jīng)初始化完成。
懶漢式,會(huì)延遲加載,在初次使用該單例時(shí)才會(huì)實(shí)例化對(duì)象出來。首次調(diào)用時(shí)要做初始化,如果要做的工作比較多,性能會(huì)有所延遲,之后就和餓漢式一樣了。
主要解決:一個(gè)全局使用的類頻繁地創(chuàng)建與銷毀。
何時(shí)使用:當(dāng)想控制實(shí)例數(shù)目,節(jié)省系統(tǒng)資源的時(shí)候。
如何解決:判斷系統(tǒng)是否已經(jīng)有這個(gè)單例,有則返回,沒有則創(chuàng)建。
關(guān)鍵代碼:構(gòu)造函數(shù)是私有的。
3??應(yīng)用實(shí)例
- 一個(gè)男人只能有一個(gè)妻子。
- Windows 是多進(jìn)程多線程的,難免會(huì)出現(xiàn)多個(gè)進(jìn)程或線程同時(shí)操作一個(gè)文件的現(xiàn)象,所以所有文件的處理必須通過唯一的實(shí)例來進(jìn)行。
- 一些設(shè)備管理器常常設(shè)計(jì)為單例模式。比如一個(gè)電腦有兩臺(tái)打印機(jī),在輸出的時(shí)候就要處理不能兩臺(tái)打印機(jī)打印同一個(gè)文件。
4??優(yōu)點(diǎn)
- 在內(nèi)存里只有一個(gè)實(shí)例,減少了內(nèi)存的開銷,尤其是頻繁的創(chuàng)建和銷毀實(shí)例(比如管理學(xué)院首頁頁面緩存)。
- 避免對(duì)資源的多重占用(比如寫文件操作)。
5??缺點(diǎn)
沒有接口,不能繼承,與單一職責(zé)原則沖突,一個(gè)類應(yīng)該只關(guān)心內(nèi)部邏輯,而不關(guān)心外面怎么樣來實(shí)例化。
6??使用場景
- 要求生產(chǎn)唯一序列號(hào)。
- WEB 中的計(jì)數(shù)器,不用每次刷新都在數(shù)據(jù)庫里加一次,用單例先緩存起來。
- 創(chuàng)建的一個(gè)對(duì)象需要消耗的資源過多,比如 I/O 與數(shù)據(jù)庫的連接等。
二、餓漢式---線程安全
餓漢就是類一旦加載,就把單例初始化完成,保證 getInstance 的時(shí)候,單例已經(jīng)存在。
- 描述:這種方式比較常用,但容易產(chǎn)生垃圾對(duì)象。
- 優(yōu)點(diǎn):沒有加鎖,執(zhí)行效率會(huì)提高。
- 缺點(diǎn):類加載時(shí)就初始化,浪費(fèi)內(nèi)存。
- 它基于 classloder 機(jī)制避免了多線程的同步問題。不過,instance 在類裝載時(shí)就實(shí)例化,雖然導(dǎo)致類裝載的原因有很多種,在單例模式中大多數(shù)都是調(diào)用 getInstance 方法, 但是也不能確定有其他的方式(或者其他的靜態(tài)方法)導(dǎo)致類裝載,這時(shí)候初始化 instance 顯然沒有達(dá)到 lazy loading 的效果。
public class EagerSigleton() {
//持有自己的引用
private static final EagerSigleton m_instatnce = new EagerSigleton();
//構(gòu)造器私有化,不能在類的外部隨意創(chuàng)建對(duì)象
private EagerSigleton() {
}
//提供一個(gè)全局的訪問點(diǎn)來獲得這個(gè)“唯一”的對(duì)象
public static EagerSigleton getInstance() {
System.out.println("加載餓漢式....");
return m_instatnce;
}
}
靜態(tài)代碼塊實(shí)現(xiàn):
public class EagerSigleton{
private static EagerSigletoninstance;
static {
m_instatnce = new EagerSigleton();
}
private EagerSigleton() {}
public static EagerSigletongetInstance() {
return m_instatnce;
}
}
這種方式和上面的方式其實(shí)類似,只不過將類實(shí)例化的過程放在了靜態(tài)代碼塊中,也是在類裝載的時(shí)候,就執(zhí)行靜態(tài)代碼塊中的代碼,初始化類的實(shí)例。優(yōu)缺點(diǎn)和上面是一樣的。
三、懶漢式---非線程安全
- Lazy 初始化。
- 描述:最基本的實(shí)現(xiàn)方式,最大的問題就是不支持多線程。因?yàn)闆]有加 synchronized 鎖,所以嚴(yán)格意義上它并不算單例模式。
- 這種方式 lazy loading 很明顯,不要求線程安全,在多線程不能正常工作。
public class LazySigleton(){
private static LazySigleton instatnce=null;
// 構(gòu)造器私有化,不能在類的外部隨意創(chuàng)建對(duì)象
private LazySigleton(){}
// 提供一個(gè)全局的訪問點(diǎn)來獲得這個(gè)"唯一"的對(duì)象
public static LazySigleton getInstance(){
if(instatnce == null){ //1:讀取instance的值
instatnce = new LazySigleton(); //2: 實(shí)例化instance
}
return instatnce;
}
}
懶漢比較懶,只有當(dāng)調(diào)用 getInstance 的時(shí)候,才會(huì)去初始化這個(gè)單例。
四、懶漢非線程安全原因(兩點(diǎn))
對(duì)于以上代碼注釋部分,如果此時(shí)有兩個(gè)線程,線程甲執(zhí)行到 1 處讀取了 instance 為 null,然后 cpu 就被線程乙搶去了,此時(shí)線程甲還沒有對(duì) instance 進(jìn)行實(shí)例化。因此,線程乙讀取 instance 時(shí)仍然為 null,于是它對(duì) instance 進(jìn)行實(shí)例化了。然后,cpu 時(shí)間片輪到線程甲。此時(shí),線程甲已經(jīng)讀取了 instance 的值并且認(rèn)為它為 null,再次對(duì) instance 進(jìn)行實(shí)例化。所以,線程甲和線程乙返回的不是同一個(gè)實(shí)例。
如何解決呢?
- 在方法前面加 synchronized 修飾。這樣肯定不會(huì)再有線程安全問題。
public class LazySigleton(){
private static LazySigleton instatnce=null;
private LazySigleton(){}
public static synchronized LazySigleton getInstance(){
if(instatnce == null){
instatnce = new LazySigleton();
}
return instatnce;
}
}
這種解決方式有個(gè)問題:假如有 100 個(gè)線程同時(shí)執(zhí)行,那么每次去執(zhí)行 getInstance 方法時(shí)都要先獲得鎖再去執(zhí)行方法體。如果沒有鎖就要等待,耗時(shí)長,像是變成了串行處理。
特點(diǎn):
性能不高,同步范圍太大。在實(shí)例化后,獲取實(shí)例仍然是同步的,效率太低,需要縮小同步的范圍。
- 加同步代碼塊,減少鎖的顆粒大小。判斷 instance 是否為 null 是讀的操作,不存在線程安全問題。因此,只需要對(duì)創(chuàng)建實(shí)例的代碼進(jìn)行同步代碼塊的處理,也就是所謂的對(duì)可能出現(xiàn)線程安全的代碼進(jìn)行同步代碼塊的處理。
public class LazySigleton(){
private static LazySigleton instatnce=null;
private LazySigleton(){}
public static LazySigleton getInstance(){
if(instatnce == null){
synchronized (LazySigleton.class){
instatnce = new LazySigleton();
}
}
return instatnce;
}
}
這樣處理就沒有問題了嗎?同樣的原理,線程甲讀取 instance 值為 null,此時(shí) cpu 被線程乙搶去了,線程乙判斷 instance 值也為 null,于是它開始執(zhí)行同步代碼塊,對(duì) instance 進(jìn)行實(shí)例化。此時(shí),線程甲獲得 cpu,由于線程甲之前已經(jīng)判斷 instance 值為 null,于是開始執(zhí)行它后面的同步代碼塊。它也會(huì)去對(duì) instance 進(jìn)行實(shí)例化。這樣就導(dǎo)致了還是會(huì)創(chuàng)建兩個(gè)不一樣的實(shí)例。
特點(diǎn):
縮小同步范圍,來提高性能,但是仍然存在多次執(zhí)行instance=new Singleton()
的可能,由此引出 double check。
如何解決上面的問題?
很簡單,在同步代碼塊中 instance 實(shí)例化之前進(jìn)行判斷,如果 instance 為 null,才對(duì)其進(jìn)行實(shí)例化。這樣,就能保證 instance 只會(huì)實(shí)例化一次了。也就是所謂的雙重檢查加鎖機(jī)制。
再次分析上面的場景:
線程甲讀取 instance 值為 null,此時(shí) cpu 被線程乙搶去了,線程乙再來判斷 instance 值為 null。于是,它開始執(zhí)行同步代碼塊,對(duì) instance 進(jìn)行了實(shí)例化。這時(shí)線程甲獲得 cpu 執(zhí)行權(quán),當(dāng)線程甲去執(zhí)行同步代碼塊時(shí),它再去判斷 instance 的值,由于線程乙執(zhí)行完后已經(jīng)將這個(gè)共享資源 instance 實(shí)例化了,所以 instance 不再為 null,所以,線程甲就不會(huì)再次實(shí)行實(shí)例化代碼了。
public class LazySigleton() {
private LazySigleton(){}
private static LazySigleton instatnce=null;
public static LazySigleton getInstance(){
if(instatnce== null) {
synchronized (LazySigleton.class){
if (instatnce == null){
instatnce = new LazySigleton();
}
}
}
return instatnce;
}
}
雙重檢查加鎖并不表示一定沒有線程安全問題了。因?yàn)椋?a href="http://www.lxweimin.com/p/e657743298ef" target="_blank">Java內(nèi)存模型(JVM)并不限制處理器重排序。instatnce = new LazySigleton()
并不是原子語句,其實(shí)可以分為下面的步驟:
- 申請一塊內(nèi)存空間;
- 在這塊空間里實(shí)例化對(duì)象;
- instatnce 的引用指向這塊空間地址(instatnce 指向分配的內(nèi)存空間后就不為 null 了)。
【由此可理解,Java 中 new 操作是不具有原子性的】
指令重排序存在的問題是:
對(duì)于以上步驟,指令重排序很有可能不是按上面【1、2、3】步驟依次執(zhí)行的。比如,先執(zhí)行 1 申請一塊內(nèi)存空間,然后執(zhí)行 3,instatnce 的引用去指向剛剛申請的內(nèi)存空間地址。那么,當(dāng)它再去執(zhí)行 2,判斷 instatnce 時(shí),由于 instatnce 已經(jīng)指向了某一地址,它就不會(huì)再為 null 了,因此,也就不會(huì)實(shí)例化對(duì)象了。這就是所謂的指令重排序安全問題。那么,如何解決這個(gè)問題呢?
加上 volatile 關(guān)鍵字,因?yàn)?volatile 可以禁止指令重排序。volatile 可以保證【1、2、3】的執(zhí)行順序,沒執(zhí)行完 1、2 就肯定不會(huì)執(zhí)行 3,也就是沒有執(zhí)行完 1、2,instance 一直為空。這樣就可以保證 3(instance 賦值操作)是最后一步完成,這樣就不會(huì)出現(xiàn) instance 在對(duì)象沒有初始化時(shí)就不為 null 的情況了。這樣也就實(shí)現(xiàn)了正確的單例模式。具體代碼如下:
public class LazySigleton() {
private LazySigleton(){}
private static volatile LazySigleton instatnce=null;
public static LazySigleton getInstance(){
if(instatnce== null) {
synchronized (LazySigleton.class){
if (instatnce == null){
instatnce = new LazySigleton();
}
}
}
return instatnce;
}
}
附:
1??靜態(tài)內(nèi)部類懶漢模式
public class Singleton{
private Singleton(){}
public static Singleton getInstance(){
return InstanceHolder.instance;
}
static class InstanceHolder{
private static Singleton instance=new Singleton();
}
}
靜態(tài)內(nèi)部類在沒有顯示調(diào)用的時(shí)候是不會(huì)進(jìn)行加載的,當(dāng)執(zhí)行了
return InstanceHolder.instance后才加載初始化,這樣就實(shí)現(xiàn)了正確的單例模式。
2??利用枚舉的特性在 JVM 層保證絕對(duì)的單例
class EnumSingleton {
//私有構(gòu)造函數(shù),防止new對(duì)象
private EnumSingleton() {}
public static EnmuSingleton getInstance() {
return Singleton.INSTANCE.getSingleton();
}
//JVM層保證絕對(duì)單例
private enum Singleton {
INSTANCE;
private EnumSingleton singleton;
Singleton() {
singleton = new EnumSingleton();
}
public EnumSingleton getSingleton() {
return singleton;
}
}
}