在一些場景中,咱們但願建立的對象在整個軟件系統只保存一份實例,如線程池, 日誌對象、緩存等。建立並保存對象單一主要有兩個做用:節省系統資源;防止多個對象產生衝突。**單例模式(Singleton Pattern)**就能夠確保只有一個實例對象會被建立。今天,咱們重點聊聊單例模式的8種實現方式(java語言)java
咱們都知道,能夠經過 new 的方式建立對象。若是類對new方式建立對象不加以約束的話,就不能保證系統只建立一個對象。private修飾類的構造方法,就能夠確保該類不能任意建立對象。緩存
餓漢式實現單例模式的原理:利用靜態常量在類加載時生成全局惟一實例特性安全
具體代碼markdown
// 單例模式實現1,餓漢式(靜態常量)
public class Singleton1 {
// 類加載時,實例化對象
private static Singleton1 instance = new Singleton1();
public static Singleton1 getInstance() {
return instance;
}
private Singleton1() {
System.out.println("單例模式實現1,餓漢式(靜態常量)");
}
public static void main(String[] args) {
System.out.println("開始演示靜態常量方式建立單例對象:");
Singleton1 instance1 = Singleton1.getInstance();
Singleton1 instance2 = Singleton1.getInstance();
System.out.println(instance1 == instance2);
}
}
// 運行main方法,結果以下:
單例模式實現1,餓漢式(靜態變量)
開始演示靜態變量方式建立單例對象:
true
複製代碼
從運行結果(輸出打印的1,2行順序)能夠看出,咱們想要獲取的實例對象在真正獲取以前已經實例化。(靜態常量在類加載過程當中賦值)多線程
這也是餓漢式實現單例模式很差的一點:不能懶加載。jvm
基本與上面的實現方式同樣,只是語法有點區別,靜態代碼塊替換靜態變量直接賦值。oop
// 單例模式實現2,餓漢式(靜態代碼塊)
public class Singleton2 {
static {
instance = new Singleton2();
}
private static Singleton2 instance;
private Singleton2() {
System.out.println("單例模式實現2,餓漢式(靜態代碼塊)");
}
public static Singleton2 getInstance() {
return instance;
}
public static void main(String[] args) {
System.out.println("開始演示靜態代碼塊方式建立單例對象:");
Singleton2 instance1 = Singleton2.getInstance();
Singleton2 instance2 = Singleton2.getInstance();
System.out.println(instance1 == instance2);
}
}
// 運行main方法,結果以下:
單例模式實現2,餓漢式(靜態代碼塊)
開始演示靜態代碼塊方式建立單例對象:
true
複製代碼
上面的兩種寫法,都是不支持懶加載的。接下來的幾種方式,都是懶加載的方式。首先看看最簡單的一種實現方式優化
// 單例模式實現3,懶漢式(常規寫法,線程不安全)
public class Singleton3 {
private static Singleton3 instance;
private Singleton3() {
System.out.println("單例模式實現3,懶漢式(常規寫法,線程不安全)。當前線程:" + Thread.currentThread().getName());
}
public static Singleton3 getInstance() {
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
public static void main(String[] args) {
System.out.println("開始演示常規懶加載方式建立單例對象:");
Singleton3 instance1 = Singleton3.getInstance();
Singleton3 instance2 = Singleton3.getInstance();
System.out.println(instance1 == instance2);
}
}
// 運行結果
開始演示常規懶加載方式建立單例對象:
單例模式實現3,懶漢式(常規寫法,線程不安全)。當前線程:main
true
複製代碼
從運行結果來看,這種方式彷佛沒有問題。即實現了懶加載,又保證了對象單一。spa
咱們換種演示方式,修改main方法:線程
public static void main(String[] args) {
System.out.println("開始演示常規懶加載方式建立單例對象:");
for (int i = 0; i < 50; i++) {
new Thread(() -> {
Singleton3.getInstance();
}).start();
}
}
// 運行結果(有可能須要多運行幾回,纔會出現相似效果)
開始演示常規懶加載方式建立單例對象:
單例模式實現3,懶漢式(常規寫法,線程不安全)。當前線程:Thread-1
單例模式實現3,懶漢式(常規寫法,線程不安全)。當前線程:Thread-0
複製代碼
從運行結果能夠看出,這種單例模式的實現方式是線程不安全的,在多線程環境下,有可能會建立多個實例。
方式三建立單例對象,線程不安全的緣由是:當instance在完成實例化以前,多個線程同時判斷if (instance == null)
結果都爲true,致使這些線程都往下繼續執行建立實例對象。簡單粗暴的解決方式,在getInstance方法加鎖(用synchronized關鍵字修飾方法)。具體代碼:
// 單例模式實現4,懶漢式(同步方法,線程安全)
public class Singleton4 {
private static Singleton4 instance;
private Singleton4() {
System.out.println("單例模式實現4,懶漢式(同步方法,線程安全)。當前線程:" + Thread.currentThread().getName());
}
public static synchronized Singleton4 getInstance() {
if (instance == null) {
instance = new Singleton4();
}
return instance;
}
public static void main(String[] args) {
System.out.println("開始演示懶加載-同步方法方式建立單例對象:");
for (int i = 0; i < 50; i++) {
new Thread(() -> {
Singleton4.getInstance();
}).start();
}
}
}
// 運行結果
開始演示懶加載-同步方法方式建立單例對象:
單例模式實現4,懶漢式(同步方法,線程安全)。當前線程:Thread-1
複製代碼
這種方式,雖然解決了線程安全問題,可是每次獲取實例對象時,都須要加鎖,這大大影響了系統運行效率。接下來的實現方式,將逐步優化線程安全下懶加載效率低的問題。
在靜態方法加鎖,鎖粒度太大,形成資源浪費。所以,咱們嘗試把鎖粒度縮小,在代碼塊加鎖。
示例代碼:
public class Singleton5 {
private static Singleton5 instance;
private Singleton5() {
System.out.println("單例模式實現5,懶漢式(同步代碼塊,線程安全)。當前線程:" + Thread.currentThread().getName());
}
public static Singleton5 getInstance() {
if (instance == null) {
synchronized (Singleton5.class){
instance = new Singleton5();
}
}
return instance;
}
public static void main(String[] args) {
System.out.println("開始演示懶加載-同步代碼塊方式建立單例對象:");
for (int i = 0; i < 50; i++) {
new Thread(() -> {
Singleton5.getInstance();
}).start();
}
}
}
// 運行結果(有可能須要多運行幾回,纔會出現相似效果):
開始演示懶加載-同步代碼塊方式建立單例對象:
單例模式實現5,懶漢式(同步代碼塊,線程安全)。當前線程:Thread-0
單例模式實現5,懶漢式(同步代碼塊,線程安全)。當前線程:Thread-1
複製代碼
從運行結果來看,這種實現方式也是線程不安全的。緣由分析:
關鍵代碼
if (instance == null) { // 第1行
synchronized (Singleton5.class){ // 第2行
instance = new Singleton5(); // 第3行
}
}
複製代碼
雖然在2行加上了鎖,但這隻保證了同一時刻,只有一個線程能夠執行第3行代碼。在第3行代碼執行前,不一樣的線程仍是能夠判斷if是true,而後執行到第2行,等待有鎖的線程釋放鎖,得到鎖以後繼續建立對象。
若要線程安全,改造以下:
public static Singleton5 getInstance() {
synchronized (Singleton5.class) {
if (instance == null) {
instance = new Singleton5();
}
}
return instance;
}
複製代碼
然而,這種實現方式與第四種效果一致,鎖的粒度是整個getInstance方法。
前面三種懶加載實現單例的方式,都有各自的不足,不是線程不安全就是獲取單例效率低。線程不安全的地方在於已有線程建立實例,繼續建立實例。效率低的地方在於,已經建立好實例,還加鎖獲取實例。而雙重檢查就避免了這兩種問題。
示例代碼
// 懶漢式(雙重檢查)
public class Singleton6 {
private static volatile Singleton6 instance;
private Singleton6() {
System.out.println("單例模式實現6,懶漢式(雙重檢查)。當前線程:" + Thread.currentThread().getName());
}
public static Singleton6 getInstance() {
if (instance == null) {
System.out.println("嘗試建立實例...");
synchronized (Singleton6.class){
if (instance == null) {
instance = new Singleton6();
}
}
}
return instance;
}
public static void main(String[] args) {
System.out.println("開始演示懶加載-雙重檢查方式建立單例對象:");
for (int i = 0; i < 50; i++) {
new Thread(() -> {
Singleton6.getInstance();
}).start();
}
}
}
// 運行結果
開始演示懶加載-雙重檢查方式建立單例對象:
嘗試建立實例...
單例模式實現6,懶漢式(雙重檢查)。當前線程:Thread-0
複製代碼
特別注意一點,咱們靜態變量用了「volatile」關鍵詞修飾,爲何要用volatile修飾呢,能夠參考文章:《雙重檢查鎖定與延遲初始化》
利用靜態內部類的方式,咱們也能夠實現線程安全的單例模式
示例代碼
// 單例模式實現7,靜態內部類
public class Singleton7 {
private Singleton7(){
System.out.println("單例模式實現7靜態內部類。當前線程:" + Thread.currentThread().getName());
}
private static class InstanceHolder{
private static Singleton7 instance = new Singleton7();
}
public static Singleton7 getInstance() {
return InstanceHolder.instance;
}
public static void main(String[] args) {
System.out.println("開始演示靜態內部類建立單例對象:");
Singleton7 instance1 = Singleton7.getInstance();
Singleton7 instance2 = Singleton7.getInstance();
System.out.println(instance1 == instance2);
}
}
// 運行結果
開始演示靜態內部類建立單例對象:
單例模式實現7靜態內部類。當前線程:main
true
複製代碼
JVM 幫助咱們保證了內部類建立的線程安全性
枚舉在jvm裏是自然的單例,因此利用枚舉實現單例也是線程安全的。《 Effective Java》這本書就提倡用枚舉的方式建立單例對象
示例代碼
public enum Singleton8 {
INSTANCE();
Singleton8(){
System.out.println("單例模式實現8,枚舉方式");
}
}
複製代碼
單例模式的實現方式有多種,保證線程安全和運行效率狀況下(文中的第三種方式線程不安全,第4、五種方式效率低)),各類實現方式的實際效果差異並不大,選擇本身順手的實現方式就能夠!而「懶加載」和「雙重檢查」思想,在咱們開發中常用到的,但願你們好好理解這兩種思想。