高並發下線程安全的單例模式(最全最經典,值得收藏)

2021-02-08 Java基基

在所有的設計模式中,單例模式是我們在項目開發中最為常見的設計模式之一,而單例模式有很多種實現方式,你是否都了解呢?高並發下如何保證單例模式的線程安全性呢?如何保證序列化後的單例對象在反序列化後任然是單例的呢?這些問題在看了本文之後都會一一的告訴你答案,趕快來閱讀吧!

什麼是單例模式?

在文章開始之前我們還是有必要介紹一下什麼是單例模式。單例模式是為確保一個類只有一個實例,並為整個系統提供一個全局訪問點的一種模式方法。

從概念中體現出了單例的一些特點:

為了便於讀者更好的理解這些概念,下面給出這麼一段內容敘述:

在計算機系統中,線程池、緩存、日誌對象、對話框、印表機、顯卡的驅動程序對象常被設計成單例。這些應用都或多或少具有資源管理器的功能。每臺計算機可以有若干個印表機,但只能有一個Printer Spooler,以避免兩個列印作業同時輸出到印表機中。

每臺計算機可以有若干通信埠,系統應當集中管理這些通信埠,以避免一個通信埠同時被兩個請求同時調用。總之,選擇單例模式就是為了避免不一致狀態,避免政出多頭。

正是由於這個特點,單例對象通常作為程序中的存放配置信息的載體,因為它能保證其他對象讀到一致的信息。

例如在某個伺服器程序中,該伺服器的配置信息可能存放在資料庫或文件中,這些配置數據由某個單例對象統一讀取,服務進程中的其他對象如果要獲取這些配置信息,只需訪問該單例對象即可。

這種方式極大地簡化了在複雜環境 下,尤其是多線程環境下的配置管理,但是隨著應用場景的不同,也可能帶來一些同步問題。

1、餓漢式單例

餓漢式單例是指在方法調用前,實例就已經創建好了。

下面是實現代碼:

package org.mlinge.s01;

public class MySingleton {

private static MySingleton instance = new MySingleton();

private MySingleton(){}

public static MySingleton getInstance() {
return instance;
}

}

以上是單例的餓漢式實現,我們來看看餓漢式在多線程下的執行情況,給出一段多線程的執行代碼:

package org.mlinge.s01;

public class MyThread extends Thread{

@Override
public void run() {
System.out.println(MySingleton.getInstance().hashCode());
}

public static void main(String[] args) {

MyThread[] mts = new MyThread[10];
for(int i = 0 ; i < mts.length ; i++){
mts[i] = new MyThread();
}

for (int j = 0; j < mts.length; j++) {
mts[j].start();
}
}
}

以上代碼運行結果:

1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954

從運行結果可以看出實例變量額hashCode值一致,這說明對象是同一個,餓漢式單例實現了。

2、懶漢式單例

懶漢式單例是指在方法調用獲取實例時才創建實例,因為相對餓漢式顯得「不急迫」,所以被叫做「懶漢模式」。

下面是實現代碼:

package org.mlinge.s02;

public class MySingleton {

private static MySingleton instance = null;

private MySingleton(){}

public static MySingleton getInstance() {
if(instance == null){//懶漢式
instance = new MySingleton();
}
return instance;
}
}

這裡實現了懶漢式的單例,但是熟悉多線程並發編程的朋友應該可以看出,在多線程並發下這樣的實現是無法保證實例實例唯一的,甚至可以說這樣的失效是完全錯誤的,下面我們就來看一下多線程並發下的執行情況,這裡為了看到效果,我們對上面的代碼做一小點修改:

package org.mlinge.s02;

public class MySingleton {

private static MySingleton instance = null;

private MySingleton(){}

public static MySingleton getInstance() {
try {
if(instance != null){//懶漢式

}else{
//創建實例之前可能會有一些準備性的耗時工作
Thread.sleep(300);
instance = new MySingleton();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return instance;
}
}

這裡假設在創建實例前有一些準備性的耗時工作要處理,多線程調用:

package org.mlinge.s02;

public class MyThread extends Thread{

@Override
public void run() {
System.out.println(MySingleton.getInstance().hashCode());
}

public static void main(String[] args) {

MyThread[] mts = new MyThread[10];
for(int i = 0 ; i < mts.length ; i++){
mts[i] = new MyThread();
}

for (int j = 0; j < mts.length; j++) {
mts[j].start();
}
}
}

執行結果如下:

1210420568
1210420568
1935123450
1718900954
1481297610
1863264879
369539795
1210420568
1210420568
602269801

從這裡執行結果可以看出,單例的線程安全性並沒有得到保證,那要怎麼解決呢?

3、線程安全的懶漢式單例

要保證線程安全,我們就得需要使用同步鎖機制,下面就來看看我們如何一步步的解決 存在線程安全問題的懶漢式單例(錯誤的單例)。

1.方法中聲明synchronized關鍵字

出現非線程安全問題,是由於多個線程可以同時進入getInstance()方法,那麼只需要對該方法進行synchronized的鎖同步即可:

package org.mlinge.s03;

public class MySingleton {

private static MySingleton instance = null;

private MySingleton(){}

public synchronized static MySingleton getInstance() {
try {
if(instance != null){//懶漢式

}else{
//創建實例之前可能會有一些準備性的耗時工作
Thread.sleep(300);
instance = new MySingleton();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return instance;
}
}

此時任然使用前面驗證多線程下執行情況的MyThread類來進行驗證,將其放入到org.mlinge.s03包下運行,執行結果如下:

1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373

從執行結果上來看,問題已經解決了,但是這種實現方式的運行效率會很低。同步方法效率低,那我們考慮使用同步代碼塊來實現:

2.同步代碼塊實現
package org.mlinge.s03;

public class MySingleton {

private static MySingleton instance = null;

private MySingleton(){}

//public synchronized static MySingleton getInstance() {
public static MySingleton getInstance() {
try {
synchronized (MySingleton.class) {
if(instance != null){//懶漢式

}else{
//創建實例之前可能會有一些準備性的耗時工作
Thread.sleep(300);
instance = new MySingleton();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return instance;
}
}

這裡的實現能夠保證多線程並發下的線程安全性,但是這樣的實現將全部的代碼都被鎖上了,同樣的效率很低下。

3.針對某些重要的代碼來進行單獨的同步(可能非線程安全)

針對某些重要的代碼進行單獨的同步,而不是全部進行同步,可以極大的提高執行效率,我們來看一下:

package org.mlinge.s04;

public class MySingleton {

private static MySingleton instance = null;

private MySingleton(){}

public static MySingleton getInstance() {
try {
if(instance != null){//懶漢式

}else{
//創建實例之前可能會有一些準備性的耗時工作
Thread.sleep(300);
synchronized (MySingleton.class) {
instance = new MySingleton();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return instance;
}
}

此時同樣使用前面驗證多線程下執行情況的MyThread類來進行驗證,將其放入到org.mlinge.s04包下運行,執行結果如下:

1481297610
397630378
1863264879
1210420568
1935123450
369539795
590202901
1718900954
1689058373
602269801

從運行結果來看,這樣的方法進行代碼塊同步,代碼的運行效率是能夠得到提升,但是卻沒能保住線程的安全性。看來還得進一步考慮如何解決此問題。

4.Double Check Locking 雙檢查鎖機制(推薦)

為了達到線程安全,又能提高代碼執行效率,我們這裡可以採用DCL的雙檢查鎖機制來完成,代碼實現如下:

package org.mlinge.s05;

public class MySingleton {

//使用volatile關鍵字保其可見性
volatile private static MySingleton instance = null;

private MySingleton(){}

public static MySingleton getInstance() {
try {
if(instance != null){//懶漢式

}else{
//創建實例之前可能會有一些準備性的耗時工作
Thread.sleep(300);
synchronized (MySingleton.class) {
if(instance == null){//二次檢查
instance = new MySingleton();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return instance;
}
}

將前面驗證多線程下執行情況的MyThread類放入到org.mlinge.s05包下運行,執行結果如下:

369539795
369539795
369539795
369539795
369539795
369539795
369539795
369539795
369539795
369539795

從運行結果來看,該中方法保證了多線程並發下的線程安全性。

這裡在聲明變量時使用了volatile關鍵字來保證其線程間的可見性;在同步代碼塊中使用二次檢查,以保證其不被重複實例化。集合其二者,這種實現方式既保證了其高效性,也保證了其線程安全性。

4、使用靜態內置類實現單例模式

DCL解決了多線程並發下的線程安全問題,其實使用其他方式也可以達到同樣的效果,代碼實現如下:

package org.mlinge.s06;

public class MySingleton {

//內部類
private static class MySingletonHandler{
private static MySingleton instance = new MySingleton();
}

private MySingleton(){}

public static MySingleton getInstance() {
return MySingletonHandler.instance;
}
}

以上代碼就是使用靜態內置類實現了單例模式,這裡將前面驗證多線程下執行情況的MyThread類放入到org.mlinge.s06包下運行,執行結果如下:

1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954

從運行結果來看,靜態內部類實現的單例在多線程並發下單個實例得到了保證。

5、序列化與反序列化的單例模式實現

靜態內部類雖然保證了單例在多線程並發下的線程安全性,但是在遇到序列化對象時,默認的方式運行得到的結果就是多例的。

代碼實現如下:

package org.mlinge.s07;

import java.io.Serializable;

public class MySingleton implements Serializable {

private static final long serialVersionUID = 1L;

//內部類
private static class MySingletonHandler{
private static MySingleton instance = new MySingleton();
}

private MySingleton(){}

public static MySingleton getInstance() {
return MySingletonHandler.instance;
}
}

序列化與反序列化測試代碼:

package org.mlinge.s07;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SaveAndReadForSingleton {

public static void main(String[] args) {
MySingleton singleton = MySingleton.getInstance();

File file = new File("MySingleton.txt");

try {
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(singleton);
fos.close();
oos.close();
System.out.println(singleton.hashCode());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

try {
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
MySingleton rSingleton = (MySingleton) ois.readObject();
fis.close();
ois.close();
System.out.println(rSingleton.hashCode());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

}
}

運行以上代碼,得到的結果如下:

865113938
1442407170

從結果中我們發現,序列號對象的hashCode和反序列化後得到的對象的hashCode值不一樣,說明反序列化後返回的對象是重新實例化的,單例被破壞了。那怎麼來解決這一問題呢?

解決辦法就是在反序列化的過程中使用readResolve()方法,單例實現的代碼如下:

package org.mlinge.s07;

import java.io.ObjectStreamException;
import java.io.Serializable;

public class MySingleton implements Serializable {

private static final long serialVersionUID = 1L;

//內部類
private static class MySingletonHandler{
private static MySingleton instance = new MySingleton();
}

private MySingleton(){}

public static MySingleton getInstance() {
return MySingletonHandler.instance;
}

//該方法在反序列化時會被調用,該方法不是接口定義的方法,有點兒約定俗成的感覺
protected Object readResolve() throws ObjectStreamException {
System.out.println("調用了readResolve方法!");
return MySingletonHandler.instance;
}
}

再次運行上面的測試代碼,得到的結果如下:

865113938
調用了readResolve方法!
865113938

從運行結果可知,添加readResolve方法後反序列化後得到的實例和序列化前的是同一個實例,單個實例得到了保證。

6、使用static代碼塊實現單例

靜態代碼塊中的代碼在使用類的時候就已經執行了,所以可以應用靜態代碼塊的這個特性的實現單例設計模式。

package org.mlinge.s08;

public class MySingleton{

private static MySingleton instance = null;

private MySingleton(){}

static{
instance = new MySingleton();
}

public static MySingleton getInstance() {
return instance;
}
}

測試代碼如下:

package org.mlinge.s08;

public class MyThread extends Thread{

@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(MySingleton.getInstance().hashCode());
}
}

public static void main(String[] args) {

MyThread[] mts = new MyThread[3];
for(int i = 0 ; i < mts.length ; i++){
mts[i] = new MyThread();
}

for (int j = 0; j < mts.length; j++) {
mts[j].start();
}
}
}

運行結果如下:

1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954

從運行結果看,單例的線程安全性得到了保證。

7、使用枚舉數據類型實現單例模式

枚舉enum和靜態代碼塊的特性相似,在使用枚舉時,構造方法會被自動調用,利用這一特性也可以實現單例:

package org.mlinge.s09;

public enum EnumFactory{

singletonFactory;

private MySingleton instance;

private EnumFactory(){//枚舉類的構造方法在類加載是被實例化
instance = new MySingleton();
}

public MySingleton getInstance(){
return instance;
}

}

class MySingleton{//需要獲實現單例的類,比如資料庫連接Connection
public MySingleton(){}
}

測試代碼如下:

package org.mlinge.s09;

public class MyThread extends Thread{

@Override
public void run() {
System.out.println(EnumFactory.singletonFactory.getInstance().hashCode());
}

public static void main(String[] args) {

MyThread[] mts = new MyThread[10];
for(int i = 0 ; i < mts.length ; i++){
mts[i] = new MyThread();
}

for (int j = 0; j < mts.length; j++) {
mts[j].start();
}
}
}

執行後得到的結果:

1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610

運行結果表明單例得到了保證,但是這樣寫枚舉類被完全暴露了,據說違反了「職責單一原則」,那我們來看看怎麼進行改造呢。

8、完善使用enum枚舉實現單例模式

不暴露枚舉類實現細節的封裝代碼如下:

package org.mlinge.s10;

public class ClassFactory{

private enum MyEnumSingleton{
singletonFactory;

private MySingleton instance;

private MyEnumSingleton(){//枚舉類的構造方法在類加載是被實例化
instance = new MySingleton();
}

public MySingleton getInstance(){
return instance;
}
}

public static MySingleton getInstance(){
return MyEnumSingleton.singletonFactory.getInstance();
}
}

class MySingleton{//需要獲實現單例的類,比如資料庫連接Connection
public MySingleton(){}
}

驗證單例實現的代碼如下:

package org.mlinge.s10;

public class MyThread extends Thread{

@Override
public void run() {
System.out.println(ClassFactory.getInstance().hashCode());
}

public static void main(String[] args) {

MyThread[] mts = new MyThread[10];
for(int i = 0 ; i < mts.length ; i++){
mts[i] = new MyThread();
}

for (int j = 0; j < mts.length; j++) {
mts[j].start();
}
}
}

驗證結果:

1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
1935123450

驗證結果表明,完善後的單例實現更為合理。

以上就是本文要介紹的所有單例模式的實現,相信認真閱讀的讀者都已經明白文章開頭所引入的那幾個問題了,祝大家讀得開心:-D!

備註:本文的編寫思路和實例源碼參照《Java多線程編程核心技術》-(高洪巖)一書中第六章的學習案例撰寫。

相關焦點

  • 深入解析單例模式的七種實現
    這種方式簡化了在複雜環境下的配置管理。還有就是我們經常使用的servlet就是單例多線程的。使用單例能夠節省很多內存。如何實現單例模式呢?Show me the code讓我們來看看單例模式的7種實現方式單例模式的七種實現第一種:懶漢式加載懶漢式加載:最簡單的單例模式:2步,1.把自己的構造方法設置為私有的,不讓別人訪問你的實例,2.提供一個static方法給別人獲取你的實例.
  • Android設計模式(1)——單例模式
    單例模式的UML類圖 實現單例模式主要有如下幾個關鍵點:構造函數不對外開放,一般為Private通過一個靜態方法或者枚舉返回單例對象確保單例類的對象有且只有一個,尤其在多線程環境下確保單例對象在反序列化時不會重新構建對象示例示例類圖:  示例實現代碼//普通員工類public class Staff
  • Java創建線程安全的單例singleton
    2、多線程下的安全單例,第一種方式就是方法加鎖來實現,比如添加關鍵字synchronized。方法加鎖來實現線程安全的單例添加了一個synchronized關鍵字,實現了多線程下的安全單例,每次獲取會鎖住Class對象,導致性能不高。
  • 單例模式絕對沒有想像的那麼簡單!不服來看!
    一、前言單例模式(Singleton Pattern)是 Java 中最常用的設計模式之一,同時也是面試的重災區。有些人可能覺的單例模式很簡單,沒有什麼難的。其實不然,因為牽扯到線程安全的問題,所以單例模式絕對能體現出你的功底。不信接著往下看。
  • 大話設計模式之愛你一萬年:第二章 創建型模式:單例模式::我的女朋友只有你一個:3.4.5.單例模式的實現-餓漢/靜態/枚舉
    >👇)國內最全的Spring Boot系列之三2020上半年發文匯總「值得收藏」SpringBoot框架開發的優秀的項目「值得收藏學習」 - 第3351天學會別人1個月學會的設計模式大話設計模式之愛你一萬年:第一章 設計模式基本概念:
  • Java 實現單例模式的 9 種方法
    什麼是單例模式二. 單例模式的特點三. 單例模式VS靜態類四. 單例模式的實現一. 什麼是單例模式因進程需要,有時我們只需要某個類同時保留一個對象,不希望有更多對象,此時,我們則應考慮單例模式的設計。二. 單例模式的特點單例模式只能有一個實例。單例類必須創建自己的唯一實例。
  • Java單例模式深入詳解
    java中單例模式是一種常見的設計模式,單例模式分三種:懶漢式單例、餓漢式單例、登記式單例三種。這種方式極大地簡化了在複雜環境 下,尤其是多線程環境下的配置管理,但是隨著應用場景的不同,也可能帶來一些同步問題。三.典型例題首先看一個經典的單例實現。
  • 我向面試官講解了單例模式,他對我豎起了大拇指
    什麼是單例模式面試官問什麼是單例模式時,千萬不要答非所問,給出單例模式有兩種類型之類的回答,要圍繞單例模式的定義去展開。單例模式是指在內存中只會創建且僅創建一次對象的設計模式。總結(1)單例模式常見的寫法有兩種:懶漢式、餓漢式(2)懶漢式:在需要用到對象時才實例化對象,正確的實現方式是:Double Check + Lock,解決了並發安全和性能低下問題(3)餓漢式:在類加載時已經創建好該單例對象,在獲取單例對象時直接返回對象即可
  • 單例模式襲來
    常見的單例模式1 懶漢式單例❝太尼瑪懶了,你不要我就不創建,要了再說,應該叫"拖延症單例"更合適。餓漢式",也就是利用了JVM<clinit>()是線程安全的特性,做到了"線程安全",可以說是最合適的單例模式了,極其推薦。
  • 手寫單例模式
    手寫單例模式為什麼要有單例模式: 在編程中,有些場景,是這樣的:
  • Python設計模式之單例模式
    什麼場景會用到單例模式呢?就是在系統工程中只想創建單個實例對象,比如資料庫連接池,Redis連接池,服務監控中心等。這個場景下,如果存在多個實例對象,那麼會有無法預測的異常。同時,在設計單例模式時,需要考慮的很重要的因素就是並發性,即多線程調用時是否會存在多個線程。那麼,如何設計使用單例模式呢?
  • 一文帶你讀懂單例模式
    單例模式單例模式可以說是設計模式中最簡單的一個了,它屬於創建型模式,其定義如下:單例模式確保某個類只有一個實例,而且自行實例化並向整個系統提供這個實例。在應用這個模式時,單例對象的類必須保證只有一個實例存在。許多時候整個系統只需要擁有一個的全局對象,這樣有利於我們協調系統整體的行為。
  • Spring的Controller是單例還是多例?怎麼保證並發的安全
    (給ImportNew加星標,提高Java技能)轉自:riemann_連結:http://blog.csdn.net/riemann_/article/details/97698560答案:controller默認是單例的
  • 並發的本質:線程和進程?
    本篇收錄於《offer快到碗裡來》寫在之前 "進程和線程有何區別?" 這個問題是校招面試中最最常見的問題了。很多人討厭這種背誦課本概念的問題,還請看管打住,稍後再噴;該問題還真是一個值得思考的問題。我們常常掛在嘴邊的,你有沒有經歷過什麼高並發項目,有沒有比較難以解決的高並發問題。面試時,如果說沒有遇到高並發問題似乎低人一等。
  • 再見面試官:單例模式有幾種寫法?
    飽漢模式飽漢是變種最多的單例模式。我們從飽漢出發,通過其變種逐漸了解實現單例模式時需要關注的問題。基礎的飽漢飽漢,即已經吃飽,不著急再吃,餓的時候再吃。所以他就先不初始化單例,等第一次使用的時候再初始化,即「懶加載」。
  • 《java多線程編程核心技術》之單例模式
    相關概念立即加載/餓漢模式使用類的時候已將對象創建完畢,常見的實現辦法就是直接new實例化private static MyObject myObject = new MyObject();public static MyObject getInstance() {//沒有synchronized,可能出現非線程安全問題return myObject}
  • java設計模式中的單例模式,收藏起來慢慢看!
    在java中,單例模式算是比較基礎和簡單的,今天就來簡單聊聊什麼是單例模式。比如說,一個應用程式中,某個類的實例對象只有一個,而我們沒有辦法new,因為構造器已經被private聲明了,通過getInstance()的方法可以獲取它們的實例。
  • 設計模式:單例模式
    基本概念1.1 原理單例模式可以說是所有設計模式中最簡單的一個了,這裡我們先直接給出它的概念然後再對它進行詳細的講解。單例模式就是:一個類只能有一個實例,並提供對該實例的全局訪問點。通俗地說,就是一個類只能創建一個對象,並且在程序的任何地方都能夠訪問到該對象。在某些情況下一些類只需要一個實例就夠了,我們以一個簡化的文件管理器作為例子來說明。
  • Java的單例模式
    一、什麼是單例模式?
  • 設計模式一:單例模式
    什麼是單例模式單例模式是指系統中的某個類只能有一個對象實例。