一個(gè)類只能有一個(gè)對(duì)象?
1、什么是單例模式
Ensure a class has only one instance, and provide a global point of access to it.
采取一定的辦法保證在整個(gè)軟件系統(tǒng)中,確保對(duì)于某個(gè)類只能存在一個(gè)實(shí)例。單例模式有如下三個(gè)特點(diǎn):
①、單例類只能有一個(gè)實(shí)例
②、單例類必須自己創(chuàng)建自己的實(shí)例
③、單例類必須提供外界獲取這個(gè)實(shí)例的方法
2、單例類的設(shè)計(jì)思想(Singleton)
①、外界不能創(chuàng)建這個(gè)類的實(shí)例,那么必須將構(gòu)造器私有化。
public class Singleton {
//構(gòu)造器私有化
private Singleton(){
}
}
②、單例類必須自己創(chuàng)建自己的實(shí)例,不能允許在類的外部修改內(nèi)部創(chuàng)建的實(shí)例。
比如將這個(gè)實(shí)例用 private 聲明。為了外界能訪問到這個(gè)實(shí)例,我們還必須提供 get 方法得到這個(gè)實(shí)例。因?yàn)橥饨绮荒?new 這個(gè)類,所以我們必須用 static 來修飾字段和方法。
//在類的內(nèi)部自己創(chuàng)建實(shí)例
private static Singleton singleton = new Singleton();
//提供get 方法以供外界獲取單例
public Singleton getInstance(){
return singleton;
}
③、是否支持延遲加載?
有些情況下,創(chuàng)建某個(gè)實(shí)例耗時(shí)長(zhǎng),占用資源多,用的時(shí)候也少,我們會(huì)考慮在用到的時(shí)候才會(huì)去創(chuàng)建,這就是延遲加載。
但有些情況,按照 fail-fast 的設(shè)計(jì)原則(有問題及早暴露),比如某個(gè)實(shí)例占用資源很多,如果延遲加載,會(huì)在程序運(yùn)行一段時(shí)間后OOM,如果在程序啟動(dòng)的時(shí)候就創(chuàng)建這個(gè)實(shí)例,我們就可以立即去修復(fù),不會(huì)導(dǎo)致程序運(yùn)行之后的系統(tǒng)奔潰。
所以,是否支持延遲加載需要結(jié)合實(shí)際情況考慮。
④、保證線程安全
這個(gè)是一定要考慮的,如果你寫的單例類存在線程安全問題,那就是偽單例了。
3、單例類的幾種實(shí)現(xiàn)方式
3.1 單例模式之餓漢模式
public class Singleton {
//構(gòu)造器私有化
private Singleton(){
}
//在類的內(nèi)部自己創(chuàng)建實(shí)例
private static Singleton singleton = new Singleton();
//提供get 方法以供外界獲取單例
public static Singleton getInstance(){
return singleton;
}
}
測(cè)試:
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1.equals(s2)); //true
}
這種模式在類加載的時(shí)候?qū)嵗?singleton 就已經(jīng)創(chuàng)建并初始化好了,所以是線程安全的。
不過這種模式不支持延遲加載,有可能這個(gè)實(shí)例化過程很長(zhǎng),那么就會(huì)加大類裝載的時(shí)間;有可能這個(gè)實(shí)例現(xiàn)階段根本用不到,那么創(chuàng)建了這個(gè)實(shí)例,也會(huì)浪費(fèi)內(nèi)存。但是還是我們前面說的,是否支持延遲加載,需要結(jié)合實(shí)際情況考慮。
3.2 單例模式之懶漢模式(線程不安全)
//懶漢模式
public class Singleton {
//構(gòu)造器私有化
private Singleton(){
}
//在類的內(nèi)部自己創(chuàng)建實(shí)例的引用
private static Singleton singleton = null;
//提供get 方法以供外界獲取單例
public static Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
這種方法達(dá)到了 lazy-loading 的效果,即我們?cè)诘谝淮涡枰玫竭@個(gè)單例的時(shí)候,才回去創(chuàng)建它的實(shí)例,以后再需要就可以不用創(chuàng)建,直接獲取了。但是這種設(shè)計(jì)在多線程的情況下是不安全的。

我們可以創(chuàng)建兩個(gè)線程來看看這種情況:
public class ThreadSingleton extends Thread{
@Override
public void run() {
try {
System.out.println(Singleton.getInstance());
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ThreadSingleton s1 = new ThreadSingleton();
s1.start(); //com.ys.pattern.Singleton@5994a1e9
ThreadSingleton s2 = new ThreadSingleton();
s2.start(); //com.ys.pattern.Singleton@40dea6bc
}
}
很明顯:最后輸出結(jié)果的兩個(gè)實(shí)例是不同的。這便是線程安全問題。那么怎么解決這個(gè)問題呢?
參考這篇博客:Java多線程同步:http://www.cnblogs.com/ysocean/p/6883729.html
3.3 單例模式之懶漢模式(線程安全)
這里我們采用同步代碼塊來達(dá)到線程安全
//懶漢模式線程安全
public class Singleton {
//構(gòu)造器私有化
private Singleton(){
}
//在類的內(nèi)部自己創(chuàng)建實(shí)例的引用
private static Singleton singleton = null;
//提供get 方法以供外界獲取單例
public static Singleton getInstance() throws Exception{
synchronized (Singleton.class) {
if(singleton == null){
singleton = new Singleton();
}
}
return singleton;
}
}
我們給 getInstance() 方法創(chuàng)建實(shí)例時(shí)加了一把鎖 synchronzed,這樣會(huì)導(dǎo)致這個(gè)方法的并發(fā)為1,相當(dāng)于串行操作,如果這個(gè)單例在實(shí)際項(xiàng)目中會(huì)頻繁被調(diào)用,那就會(huì)頻繁加鎖,釋放鎖,會(huì)有性能瓶頸,不推薦此種方式。
3.4 單例模式之懶漢模式(線程安全)--雙重校驗(yàn)鎖
分析:上面的例子我們可以看到,synchronized 其實(shí)將方法內(nèi)部的所有語(yǔ)句都已經(jīng)包括了,每一個(gè)進(jìn)來的線程都要單獨(dú)進(jìn)入同步代碼塊,判斷實(shí)例是否存在,這就造成了性能的浪費(fèi)。那么我們可以想到,其實(shí)在第一次已經(jīng)創(chuàng)建了實(shí)例的情況下,后面再獲取實(shí)例的時(shí)候,可不可以不進(jìn)入這個(gè)同步代碼塊?
//懶漢模式線程安全--雙重鎖校驗(yàn)
public class Singleton {
//構(gòu)造器私有化
private Singleton(){
}
//在類的內(nèi)部自己創(chuàng)建實(shí)例的引用
private static Singleton singleton = null;
//提供get 方法以供外界獲取單例
public static Singleton getInstance() throws Exception{
if(singleton == null){
synchronized (Singleton.class) {
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
以上的真的完美解決了單例模式嗎?其實(shí)并沒有,請(qǐng)看下面:
3.5 單例模式之最終版
我們知道編譯就是將源代碼翻譯成機(jī)械碼的過程,而Java虛擬機(jī)的目標(biāo)代碼不是本地機(jī)器碼,而是虛擬機(jī)代碼。編譯原理里面有個(gè)過程是編譯優(yōu)化,就是指在不改變?cè)瓉碚Z(yǔ)義的情況下,通過調(diào)整語(yǔ)句的順序,來讓程序運(yùn)行的更快,這個(gè)過程稱為 reorder。
JVM 只是一個(gè)標(biāo)準(zhǔn),它并沒有規(guī)定有關(guān)編譯器優(yōu)化的內(nèi)容,也就是說,JVM可以自由的實(shí)現(xiàn)編譯器優(yōu)化。
那么我們來再來考慮一下,創(chuàng)建一個(gè)變量需要哪些步驟?
①、申請(qǐng)一塊內(nèi)存,調(diào)用構(gòu)造方法進(jìn)行初始化
②、分配一個(gè)指針指向該內(nèi)存
而這兩步誰先誰后呢?也就是存在這樣一種情況:先開辟一塊內(nèi)存,然后分配一個(gè)指針指向該內(nèi)存,最后調(diào)用構(gòu)造方法進(jìn)行初始化。
那么針對(duì)單例模式的設(shè)計(jì),就會(huì)存在這樣一個(gè)問題:線程 A 開始創(chuàng)建 Singleton 的實(shí)例,此時(shí)線程 B已經(jīng)調(diào)用了 getInstance的()方法,首先判斷 instance 是否為 null。而我們上面說的那種模型, A 已經(jīng)把 instance 指向了那塊內(nèi)存,只是還沒來得及調(diào)用構(gòu)造方法進(jìn)行初始化,因此 B 檢測(cè)到 instance 不為 null,于是直接把 instance 返回了。那么問題出現(xiàn)了:盡管 instance 不為 null,但是 A 并沒有構(gòu)造完成,就像一套房子已經(jīng)給了你鑰匙,但是里面還沒有裝修,你并不能住進(jìn)去。
解決方案:使用 volatile 關(guān)鍵字修飾 instance
我們知道在當(dāng)前的Java內(nèi)存模型下,線程可以把變量保存在本地內(nèi)存(比如機(jī)器的寄存器)中,而不是直接在主存中進(jìn)行讀寫。這就可能造成一個(gè)線程在主存中修改了一個(gè)變量的值,而另外一個(gè)線程還繼續(xù)使用它在寄存器中的變量值的拷貝,造成數(shù)據(jù)的不一致。
volatile修飾的成員變量在每次被線程訪問時(shí),都強(qiáng)迫從共享內(nèi)存中重讀該成員變量的值。而且,當(dāng)成員變量發(fā)生變化時(shí),強(qiáng)迫線程將變化值回寫到共享內(nèi)存。這樣在任何時(shí)刻,兩個(gè)不同的線程總是看到某個(gè)成員變量的同一個(gè)值。
//懶漢模式線程安全--volatile
public class Singleton {
//構(gòu)造器私有化
private Singleton(){
}
//在類的內(nèi)部自己創(chuàng)建實(shí)例的引用
private static volatile Singleton singleton = null;
//提供get 方法以供外界獲取單例
public static Singleton getInstance() throws Exception{
if(singleton == null){
synchronized (Singleton.class) {
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
到此我們完美的解決了單例模式的問題。但是 volatile 關(guān)鍵字是 JDK1.5 才有的,也就是 JDK1.5 之前是不能這樣用的
3.6 2021年4月補(bǔ)充
上面我們說要加關(guān)鍵字 volatile ,禁止指令重排,防止單例對(duì)象new 出來后,并且賦值給 singleton,但是還沒來得及初始化這個(gè)問題。
現(xiàn)在高版本的 Java(JDK9) 已經(jīng)在 JDK 內(nèi)部實(shí)現(xiàn)中解決了這個(gè)問題,把對(duì)象的 new 操作和初始化操作設(shè)計(jì)為 原子操作。
相關(guān)參考鏈接:
https://shipilev.net/blog/2014/safe-public-construction https://chriswhocodes.com/vm-options-explorer.html
3.7 單例模式之枚舉類
public enum Singleton{
INSTANCE;
private Singleton(){}
}
通過Java枚舉類的自身特性,保證實(shí)例創(chuàng)建的線程安全和唯一性。
3.8 單例模式之靜態(tài)內(nèi)部類
public class InnerSingleton {
private InnerSingleton() {
}
public static InnerSingleton getInstance() {
return Inner.instance;
}
static class Inner {
static InnerSingleton instance = new InnerSingleton();
}
public static void main(String[] args) {
System.out.println(InnerSingleton.getInstance() == InnerSingleton.getInstance());//true
System.out.println(InnerSingleton.getInstance().equals(InnerSingleton.getInstance()));//true
}
}
4、單例模式的應(yīng)用
說了那么多,那么單例模式在實(shí)際項(xiàng)目中有啥用呢?
還是根據(jù)其核心概念,某個(gè)數(shù)據(jù)在系統(tǒng)中只能存在一份,就可以設(shè)計(jì)為單例。
1、windows 系統(tǒng)的回收站,我們能在任何盤符刪除數(shù)據(jù),但是最后都是到了回收站中
2、網(wǎng)站的計(jì)數(shù)器,不采用單例模式,很難實(shí)現(xiàn)同步
3、數(shù)據(jù)庫(kù)連接池,可以節(jié)省打開或關(guān)閉數(shù)據(jù)庫(kù)連接所引起的效率損耗,用單例模式來維護(hù),可以大大降低這種損耗。當(dāng)然對(duì)于海量數(shù)據(jù)系統(tǒng),會(huì)存在多個(gè)數(shù)據(jù)庫(kù)連接池,比如一個(gè)能夠快速執(zhí)行SQL的連接池,還有一個(gè)是慢SQL,如果都放在一個(gè)池里面,會(huì)導(dǎo)致慢SQL執(zhí)行的時(shí)候,長(zhǎng)時(shí)間占用數(shù)據(jù)庫(kù)連接資源,導(dǎo)致其他SQL請(qǐng)求無法響應(yīng)。
4、系統(tǒng)的配置信息類,通常只存在一個(gè)。
