Java單例模式深入詳解

2021-01-10 CSDN

一.問題引入

偶然想想到的如果把Java的構造方法弄成private,那裡面的成員屬性是不是只有通過static來訪問呢;如果構造方法是private的話,那麼有什麼好處呢;如果構造方法是private的話,會不更好的封裝該內呢?我主要是應用在使用普通類模擬枚舉類型裡,後來發現這就是傳說中的單例模式。構造函數弄成private 就是單例模式,即不想讓別人用new 方法來創建多個對象,可以在類裡面先生成一個對象,然後寫一個public static方法把這個對象return出去。(eg:public 類名 getInstancd(){return 你剛剛生成的那個類對象;}),用static是因為你的構造函數是私有的,不能產生對象,所以只能用類名調用,所有只能是靜態函數。成員變量也可以寫getter/setter供外界訪問的。

第一個代碼不是單例模式,也就是說不一定只要構造方法是private的就是單例模式。

class A(){ private A(){} public name; pulbic static A creatInstance(){ return new A(); } } A a = A.createInstance(); a.name; //name 屬性

public class single{ private static final single s=new single(); private single(){ } public static single getInstance(){ return s; } }

二.單例模式概念及特點

java中單例模式是一種常見的設計模式,單例模式分三種:懶漢式單例、餓漢式單例、登記式單例三種。

單例模式有一下特點:

1、單例類只能有一個實例。

2、單例類必須自己自己創建自己的唯一實例。

3、單例類必須給所有其他對象提供這一實例。

單例模式確保某個類只有一個實例,而且自行實例化並向整個系統提供這個實例。在計算機系統中,線程池、緩存、日誌對象、對話框、印表機、顯卡的驅動程序對象常被設計成單例。這些應用都或多或少具有資源管理器的功能。每臺計算機可以有若干個印表機,但只能有一個Printer Spooler,以避免兩個列印作業同時輸出到印表機中。每臺計算機可以有若干通信埠,系統應當集中管理這些通信埠,以避免一個通信埠同時被兩個請求同時調用。總之,選擇單例模式就是為了避免不一致狀態,避免政出多頭。

正是由於這個特 點,單例對象通常作為程序中的存放配置信息的載體,因為它能保證其他對象讀到一致的信息。例如在某個伺服器程序中,該伺服器的配置信息可能存放在資料庫或 文件中,這些配置數據由某個單例對象統一讀取,服務進程中的其他對象如果要獲取這些配置信息,只需訪問該單例對象即可。這種方式極大地簡化了在複雜環境 下,尤其是多線程環境下的配置管理,但是隨著應用場景的不同,也可能帶來一些同步問題。

三.典型例題

首先看一個經典的單例實現。

public class Singleton { private static Singleton uniqueInstance = null; private Singleton() { // Exists only to defeat instantiation. } public static Singleton getInstance() { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; } // Other methods... }

Singleton通過將構造方法限定為private避免了類在外部被實例化,在同一個虛擬機範圍內,Singleton的唯一實例只能通過getInstance()方法訪問。(事實上,通過Java反射機制是能夠實例化構造方法為private的類的,那基本上會使所有的Java單例實現失效。此問題在此處不做討論,姑且掩耳盜鈴地認為反射機制不存在。)

但是以上實現沒有考慮線程安全問題。所謂線程安全是指:如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。或者說:一個類或者程序所提供的接口對於線程來說是原子操作或者多個線程之間的切換不會導致該接口的執行結果存在二義性,也就是說我們不用考慮同步的問題。顯然以上實現並不滿足線程安全的要求,在並發環境下很可能出現多個Singleton實例。

/** * Java學習交流QQ群:589809992 我們一起學Java! */ public class TestStream { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } //該類只能有一個實例 private TestStream(){} //私有無參構造方法 //該類必須自行創建 //有2種方式 /*private static final TestStream ts=new TestStream();*/ private static TestStream ts1=null; //這個類必須自動向整個系統提供這個實例對象 public static TestStream getTest(){ if(ts1==null){ ts1=new TestStream(); } return ts1; } public void getInfo(){ System.out.println("output message "+name); } }

public class TestMain { public static void main(String [] args){ TestStream s=TestStream.getTest(); s.setName("張孝祥"); System.out.println(s.getName()); TestStream s1=TestStream.getTest(); s1.setName("張孝祥"); System.out.println(s1.getName()); s.getInfo(); s1.getInfo(); if(s==s1){ System.out.println("創建的是同一個實例"); }else if(s!=s1){ System.out.println("創建的不是同一個實例"); }else{ System.out.println("application error"); } } }

運行結果:

張孝祥 張孝祥 output message 張孝祥 output message 張孝祥 創建的是同一個實例

結論:由結果可以得知單例模式為一個面向對象的應用程式提供了對象惟一的訪問點,不管它實現何種功能,整個應用程式都會同享一個實例對象。

其次,下面是單例的三種實現。

1.餓漢式單例類

飛哥下面這個可以不加final,因為靜態方法只在編譯期間執行一次初始化,也就是只會有一個對象。

//餓漢式單例類.在類初始化時,已經自行實例化 public class Singleton1 { //私有的默認構造子 private Singleton1() {} //已經自行實例化 private static final Singleton1 single = new Singleton1(); //靜態工廠方法 public static Singleton1 getInstance() { return single; } }

2.懶漢式單例類

那個if判斷確保對象只創建一次。

//懶漢式單例類.在第一次調用的時候實例化 public class Singleton2 { //私有的默認構造子 private Singleton2() {} //注意,這裡沒有final private static Singleton2 single=null; //靜態工廠方法 public synchronized static Singleton2 getInstance() { if (single == null) { single = new Singleton2(); } return single; } }

3.登記式單例類

import java.util.HashMap; import java.util.Map; //登記式單例類. Java學習交流QQ群:589809992 我們一起學Java! //類似Spring裡面的方法,將類名註冊,下次從裡面直接獲取。 public class Singleton3 { private static Map<String,Singleton3> map = new HashMap<String,Singleton3>(); static{ Singleton3 single = new Singleton3(); map.put(single.getClass().getName(), single); } //保護的默認構造子 protected Singleton3(){} //靜態工廠方法,返還此類惟一的實例 public static Singleton3 getInstance(String name) { if(name == null) { name = Singleton3.class.getName(); System.out.println("name == null"+"--->name="+name); } if(map.get(name) == null) { try { map.put(name, (Singleton3) Class.forName(name).newInstance()); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } return map.get(name); } //一個示意性的商業方法 public String about() { return "Hello, I am RegSingleton."; } public static void main(String[] args) { Singleton3 single3 = Singleton3.getInstance(null); System.out.println(single3.about()); } }

四.單例對象作配置信息管理時可能會帶來的幾個同步問題

1.在多線程環境下,單例對象的同步問題主要體現在兩個方面,單例對象的初始化和單例對象的屬性更新。

本文描述的方法有如下假設:

a. 單例對象的屬性(或成員變量)的獲取,是通過單例對象的初始化實現的。也就是說,在單例對象初始化時,會從文件或資料庫中讀取最新的配置信息。

b. 其他對象不能直接改變單例對象的屬性,單例對象屬性的變化來源於配置文件或配置資料庫數據的變化。

1.1單例對象的初始化

首先,討論一下單例對象的初始化同步。單例模式的通常處理方式是,在對象中有一個靜態成員變量,其類型就是單例類型本身;如果該變量為null,則創建該單例類型的對象,並將該變量指向這個對象;如果該變量不為null,則直接使用該變量。

這種處理方式在單線程的模式下可以很好的運行;但是在多線程模式下,可能產生問題。如果第一個線程發現成員變量為null,準備創建對象;這是第二 個線程同時也發現成員變量為null,也會創建新對象。這就會造成在一個JVM中有多個單例類型的實例。如果這個單例類型的成員變量在運行過程中變化,會 造成多個單例類型實例的不一致,產生一些很奇怪的現象。例如,某服務進程通過檢查單例對象的某個屬性來停止多個線程服務,如果存在多個單例對象的實例,就 會造成部分線程服務停止,部分線程服務不能停止的情況。

1.2單例對象的屬性更新

通常,為了實現配置信息的實時更新,會有一個線程不停檢測配置文件或配置資料庫的內容,一旦發現變化,就更新到單例對象的屬性中。在更新這些信 息的時候,很可能還會有其他線程正在讀取這些信息,造成意想不到的後果。還是以通過單例對象屬性停止線程服務為例,如果更新屬性時讀寫不同步,可能訪問該 屬性時這個屬性正好為空(null),程序就會拋出異常。

下面是解決方法

//單例對象的初始化同步 public class GlobalConfig { private static GlobalConfig instance = null; private Vector properties = null; private GlobalConfig() { //Load configuration information from DB or file //Set values for properties } private static synchronized void syncInit() { if (instance == null) { instance = new GlobalConfig(); } } public static GlobalConfig getInstance() { if (instance == null) { syncInit(); } return instance; } public Vector getProperties() { return properties; } }

這種處理方式雖然引入了同步代碼,但是因為這段同步代碼只會在最開始的時候執行一次或多次,所以對整個系統的性能不會有影響。

單例對象的屬性更新同步。

參照讀者/寫者的處理方式,設置一個讀計數器,每次讀取配置信息前,將計數器加1,讀完後將計數器減1.只有在讀計數器為0時,才能更新數據,同時要阻塞所有讀屬性的調用。

代碼如下:

/** * Java學習交流QQ群:589809992 我們一起學Java! */ public class GlobalConfig { private static GlobalConfig instance; private Vector properties = null; private boolean isUpdating = false; private int readCount = 0; private GlobalConfig() { //Load configuration information from DB or file //Set values for properties } private static synchronized void syncInit() { if (instance == null) { instance = new GlobalConfig(); } } public static GlobalConfig getInstance() { if (instance==null) { syncInit(); } return instance; } public synchronized void update(String p_data) { syncUpdateIn(); //Update properties } private synchronized void syncUpdateIn() { while (readCount > 0) { try { wait(); } catch (Exception e) { } } } private synchronized void syncReadIn() { readCount++; } private synchronized void syncReadOut() { readCount--; notifyAll(); } public Vector getProperties() { syncReadIn(); //Process data syncReadOut(); return properties; } }

採用」影子實例」的辦法具體說,就是在更新屬性時,直接生成另一個單例對象實例,這個新生成的單例對象實例將從資料庫或文件中讀取最新的配置信息;然後將這些配置信息直接賦值給舊單例對象的屬性。

public class GlobalConfig { private static GlobalConfig instance = null; private Vector properties = null; private GlobalConfig() { //Load configuration information from DB or file //Set values for properties } private static synchronized void syncInit() { if (instance = null) { instance = new GlobalConfig(); } } public static GlobalConfig getInstance() { if (instance = null) { syncInit(); } return instance; } public Vector getProperties() { return properties; } public void updateProperties() { //Load updated configuration information by new a GlobalConfig object GlobalConfig shadow = new GlobalConfig(); properties = shadow.getProperties(); } }

注意:在更新方法中,通過生成新的GlobalConfig的實例,從文件或資料庫中得到最新配置信息,並存放到properties屬性中。上面兩個方法比較起來,第二個方法更好,首先,編程更簡單;其次,沒有那麼多的同步操作,對性能的影響也不大。

相關焦點

  • Java的單例模式
    一、什麼是單例模式?
  • java設計模式中的單例模式,收藏起來慢慢看!
    在java中,單例模式算是比較基礎和簡單的,今天就來簡單聊聊什麼是單例模式。比如說,一個應用程式中,某個類的實例對象只有一個,而我們沒有辦法new,因為構造器已經被private聲明了,通過getInstance()的方法可以獲取它們的實例。
  • 深入解析單例模式的七種實現
    來源:http://t.cn/E4J3ffX什麼是單例模式如何實現單例模式呢?單例模式的七種實現總結什麼是單例模式什麼是單例模式呢?我們引用一下維基百科:單例模式,也叫單子模式,是一種常用的軟體設計模式。在應用這個模式時,單例對象的類必須保證只有一個實例存在。許多時候整個系統只需要擁有一個的全局對象,這樣有利於我們協調系統整體的行為。
  • 你真的理解了java單例模式嗎?講別人都忽略的細節
    前言:老劉這篇文章敢做保證,java的單例模式講的比大多數的技術博客都要好,講述別人技術博客都沒有的細節!!!1 java單例模式直接講實現單例模式的兩種方法:懶漢式和餓漢式,單例模式的概念自己上網搜吧這裡就不講了!
  • 設計模式一:單例模式
    什麼是單例模式單例模式是指系統中的某個類只能有一個對象實例。
  • Java 實現單例模式的 9 種方法
    什麼是單例模式二. 單例模式的特點三. 單例模式VS靜態類四. 單例模式的實現一. 什麼是單例模式因進程需要,有時我們只需要某個類同時保留一個對象,不希望有更多對象,此時,我們則應考慮單例模式的設計。二. 單例模式的特點單例模式只能有一個實例。單例類必須創建自己的唯一實例。
  • Android設計模式(1)——單例模式
    單例模式使用的場景確保某個類有且只有一個對象的場景,避免產生多個對象消耗過多的資源,或者某種類型對象只應該有且只有一個,例如,創建一個對象需要消耗的資源過多,,如要訪問IO和資料庫等資源,這時就需要考慮使用單例模式。
  • Java 連接 Redis 工具包之 Jedis 的單例模式用法,非常好用哦~
    為了避免使用時每次都要連接redis,可以使用單例模式,下面介紹這種模式的編程方法,先下載 Jedis 工具包(下載網址見附錄)。1、單例模式的服務類import redis.clients.jedis.Jedis;import redis.clients.jedis.JedisPool;import redis.clients.jedis.JedisPoolConfig
  • 單例(Singleton)設計模式及在Java中的實現
    設計模式(Design Pattern)是在經過大量的實踐後總結出來,並且經過理論化分析優選出的編程風格、代碼結構以及解決問題的思考方式。一、何為單例模式?在編程世界中,目前經典的共有23種設計模式,單例模式只是其中一種。
  • 為什麼要用單例模式?
    我們在編程中最常用的模式就是單例模式了,然而單例模式都用在什麼場合?為什麼不用靜態方法而要用單例模式呢?
  • 高並發下線程安全的單例模式(最全最經典,值得收藏)
    在所有的設計模式中,單例模式是我們在項目開發中最為常見的設計模式之一,而單例模式有很多種實現方式,你是否都了解呢?高並發下如何保證單例模式的線程安全性呢?如何保證序列化後的單例對象在反序列化後任然是單例的呢?這些問題在看了本文之後都會一一的告訴你答案,趕快來閱讀吧!什麼是單例模式?
  • 大神詳解,這麼詳細的Java設計模式不收藏可惜了
    為什麼再看一遍設計模式,主要有幾個原因:***,很多優秀的源碼基本都使用了設計模式,明確設計模式能夠更好的看源碼。第二,很多中間件設計理念也是基於設計模式的,還有其他的語言,都有自己的設計優秀實踐。對於我來說,設計模式始於java,不止於java。第三,有了這種規範,可以更好的和他人溝通,言簡意賅。
  • 設計模式:單例模式
    基本概念1.1 原理單例模式可以說是所有設計模式中最簡單的一個了,這裡我們先直接給出它的概念然後再對它進行詳細的講解。單例模式就是:一個類只能有一個實例,並提供對該實例的全局訪問點。通俗地說,就是一個類只能創建一個對象,並且在程序的任何地方都能夠訪問到該對象。在某些情況下一些類只需要一個實例就夠了,我們以一個簡化的文件管理器作為例子來說明。
  • Java Singleton設計模式DEMO
    java.awt.Desktop:Desktop該類允許Java應用程式使用在本機桌面上註冊的應用程式(如用戶的默認瀏覽器或郵件客戶端)啟動URI或文件。本機桌面和相關應用程式是獨一無二的。所以必須只有一個Desktop類的實例。單例設計模式的實現你如何確保一個類只有一個實例?好吧,在Java中有幾種方法可以做到這一點。
  • 手寫單例模式
    手寫單例模式為什麼要有單例模式: 在編程中,有些場景,是這樣的:
  • 《java多線程編程核心技術》之單例模式
    相關概念立即加載/餓漢模式使用類的時候已將對象創建完畢,常見的實現辦法就是直接new實例化private static MyObject myObject = new MyObjectpublic static MyObject getInstance() {//沒有synchronized,可能出現非線程安全問題return myObject}延遲加載/懶漢模式
  • PHP 單例模式的解析和實戰
    1、含義   作為對象的創建模式,單例模式確保某一個類只有一個實例,而且自行實例化並向整個系統全局地提供這個實例。它不會創建實例副本,而是會向單例類內部存儲的實例返回一個引用。2、單例模式的三個要點:(1).
  • Java反射機制深入詳解
    ()字節碼已經加載到java虛擬機中,去得到字節碼;java虛擬機中還沒有生成字節碼 用類加載器進行加載,加載的字節碼緩衝到虛擬機中。java.util.Stack.push(java.lang.Object) 3 public boolean java.util.Stack.empty() 4 public synchronized java.lang.Object java.util.Stack.peek() 5 public synchronized int java.util.Stack.search(java.lang.Object
  • 單例模式襲來
    吹一波單例模式❝確保某個類只有一個實例,而且自行實例化並向整個系統提供整個實例。說白了,就是"獨苗"!什麼是"自行實例化"?就是整個實例是在類內部自己創建的,不是在別的類中創建的。❞單例誰都會寫,而且五花八門,且隨洒家來瞅瞅。
  • Python設計模式之單例模式
    什麼場景會用到單例模式呢?就是在系統工程中只想創建單個實例對象,比如資料庫連接池,Redis連接池,服務監控中心等。這個場景下,如果存在多個實例對象,那麼會有無法預測的異常。同時,在設計單例模式時,需要考慮的很重要的因素就是並發性,即多線程調用時是否會存在多個線程。那麼,如何設計使用單例模式呢?