- 目錄
- 一.什么是單例?
- 二.有幾種?
- 三.應用場景
- 四.注意的地方
一.什么是單例?
單例模式 保證一個類在內存中只有一個實例(對象),并提供一個訪問它的全局訪問點。
通常我們可以讓一個全局變量使得一個對象被訪問,但它不能防止你實例化多個對象。一個最好的辦法就是,讓類自身負責保存它的唯一實例。這個類可以保證沒有其他實例可以被創建,并且它可以提供一個訪問該實例的方法 —— 大話設計模式--第21章
二.單例有幾種?
具體區分為下面幾種:
- 餓漢式
-
懶(飽)漢式
- 線程不安全(單線程下操作的)
-
線程安全
- 方法加鎖式
- 雙重判斷加鎖式DCL(Double-Check Locking)--- 方法加鎖式加強版
- 靜態嵌套類式(推薦使用)
- 枚舉式(推薦使用)
- 使用容器單例
不和你多bb ,上代碼
1.餓漢式
public class Signleton{
//對于一個final變量。
// 如果是基本數據類型的變量,則其數值一旦在初始化之后便不能更改;
// 如果是引用類型的變量,則在對其初始化之后便不能再讓其指向另一個對象。
private final static Signleton instance = new Signleton();
private Signleton(){
}
public static Signleton getInstance(){
return instance;
}
}
解釋: 拿時間換空間,因為實例對象在類加載過程中就會被創建,在getInstance()方法中只是直接返回對象引用。因為這種創建實例對象方式比較‘急’,所以稱為餓漢式。
優點:快很準,無需關心線程安全問題。
缺點:
??1.無論對象會不會被使用,在類加載的時候就創建對象了,這樣會降低內存的使用率。
?? 2.如果在一個大環境下使用了過多的餓漢單例,則會生產出過多的實例對象,無論你是否要使用他們
2.懶(飽)漢式
(1)線程不安全(單線程下操作的)
public class Signleton{
private static Signleton instance = null;
private Signleton(){
}
public static Signleton getInstance(){
if(instance==null){
instance = new Signleton();
}
return instance;
}
}
解釋: Singleton的靜態屬性instance中,只有instance為null的時候才創建一個實例,構造函數私有,確保每次都只創建一個,避免重復創建。之所以被稱為“懶漢”,因為它很懶,不急著生產實例,在需要的時候才會生產。
優點:延遲加載
缺點:只在單線程的情況下正常運行,在多線程的情況下,就會出問題。例如:當兩個線程同時運行到判斷instance是否為空的if語句,并且instance確實沒有創建好時,那么兩個線程都會創建一個實例。
(2)線程安全
方法加鎖式
public class Signleton{
private static Signleton instance = null;
private Signleton(){
}
//給方法加鎖 若有ABCD線程使用 A線程先進入 BCD線程都需要等待A線程執行完畢釋放鎖才能獲得鎖執行該方法
//這樣效率較低
public syschronized static Signleton getInstance(){
if(instance==null){
instance = new Signleton();
}
return instance;
}
}
解釋:給方法添加[synchronized],使之成為同步函數。兩個線程同時想創建實例,由于在一個時刻只有一個線程能得到同步鎖,當第一個線程加上鎖以后,第二個線程只能等待。第一個線程發現實例沒有創建,創建之。第一個線程釋放同步鎖,第二個線程才可以加上同步鎖,執行下面的代碼。由于第一個線程已經創建了實例,所以第二個線程不需要創建實例。保證在多線程的環境下也只有一個實例。
優點: 保證了線程安全。延時加載,用的時候才會生產對象。
缺點: 需要保證同步,付出效率的代價。加鎖是很耗時的。。。
雙重判斷加鎖式DCL
public class Signleton {
private static Signleton instance = null;
private Signleton(){
}
//假設有ABCD 個線程 使用這個方法
public static Signleton getInstance(){
//BCD都進入了這個方法
if(instance==null){
//而A線程已經給第二個的判斷加鎖了
syschronized(Signleton.class){
//這時A掛起,對象instance還沒創建 ,故BCD都進入了第一個判斷里面,并排隊等待A釋放鎖
//A喚醒繼續執行并創建了instance對象,執行完畢釋放鎖。
//此時到B線程進入到第二個判斷并加鎖,但由于B進入第二個判斷時instance 不為null了 故需要再判斷多一次 不然會再創建一次實例
if(instance==null){
instance = new Signleton();
}
}
}
return instance;
}
}
解釋:方法加鎖式的優化。只有當instance為null時,需要獲取同步鎖,創建一次實例。當實例被創建,則無需試圖加鎖。。
優點: 保證了線程安全。延時加載,用的時候才會生產對象。進行雙重判斷,當已經創建過實例對象后就無需加鎖,提高效率。
缺點: 編寫復雜、難記憶。雖然是優化加鎖式,但加鎖始終會耗時。
3.靜態嵌套類式(推薦使用)
public class Signleton{
private Signleton{
}
//靜態嵌套類 這里給個鏈接 區分靜態嵌套類和內部類[靜態嵌套類和內部類](http://blog.csdn.net/iispring/article/details/46490319)
private static class SignletonHolder{
public static final Signleton instance = new Signleton();
}
public static Signleton getInstance(){
return SignletonHolder.instance;
ins't
}
}
解釋: 定義一個私有的靜態嵌套類,在第一次用這個嵌套類時,會創建一個實例。而類型為SingletonHolder的類,只有在Singleton.getInstance()中調用,由于私有的屬性,他人無法使用SingleHolder,不調用Singleton.getInstance()就不會創建實例。
優點: 保證了線程安全。無需加鎖,延遲加載,第一次調用Singleton.getInstance才創建實例。推薦使用。
缺點: 無。
4.枚舉式(推薦使用)
為了方便理解枚舉式 這邊簡單介紹一下枚舉(在jdk1.5后)
//枚舉Type
public enum Type{
A,B,C;
private String type;
Type(type){
this.type = type;
}
public String getType(){
return type;
}
}
//可認為等于下面的
public class Type{
public static final Type A=new Type(A);
public static final Type B=new Type(B);
public static final Type C=new Type(C);
ins't
}
所以Type.A.getType()為A.
推薦去了解一下
Java學習整理系列之Java枚舉類型的使用
Java學習整理系列之Java枚舉類型的原理
好了 開始介紹枚舉式了 看代碼
public class Signleton{
public static Signleton getInstance(){
return SignletonEnum.INSTANCE.getInstance();
}
public enum SignletonEnum{
INSTANCE;
private Signleton instance;
//由于JVM只會初始化一次枚舉實例,所以instance無需加static
private SignletonEnum(){
instance = new Signleton();
}
public getInstance(){
return instance;
}
}
}
解釋: 定義內部的枚舉,由于類加載時JVM只會初始化一次枚舉實例,所以在構造函數中創建Signgleton對象并保證了這個對象實例唯一。
通過調用枚舉INSTANCE方法getInstance (SignletonEnum.INSTANCE.getInstance())獲取實例對象。
優點: 枚舉提供了序列化機制--例如在我們要通過網絡傳輸一個數據庫連接的句柄,會提供很多幫助。
單元素的枚舉類型已經成為實現Singleton的最佳方法。Effective Java
5.使用容器單例
public class Singleton {
//用Map保存該單例
private static Map<String, Object> objMap = new HashMap<>();
private Singleton() {
}
public static void putObject(String key, String instance){
if(!objMap.containsKey(key)){
objMap.put(key, instance);
}
}
public static Object getObject(String key){
return objMap.get(key);
}
}
解釋: 在程序開始的時候將單例類型注入到一個容器之中 ,在使用的時候再根據 key 值獲取對應的實例,這種方式可以使我們很方便的管理很多單例對象,也對用戶隱藏了具體實現類,降低了耦合度。
缺點: 會造成內存泄漏。(所以我們一般在生命周期銷毀的時候也要去銷毀它) 。
三.應用場景
一般創建一個對象需要消耗過多的資源,如:訪問I0和數據庫等資源或者有很多個地方都用到了這個實例。
四.注意的地方
雙重判斷加鎖式DCL :這種寫法也并不是保證完全100%的可靠,由于 java 編譯器允許執行無序,并且 jdk1.5之前的jvm ( java 內存模型)中的 Cache,寄存器到主內存的回寫順序規定,第二個和第三個執行是無法保證按順序執行的,也就是說有可能1-2-3也有可能是1-3-2; 這時假如有 A 和 B 兩條線程, A線程執行到3的步驟,但是未執行2,這時候 B 線程來了搶了權限,直接取走 instance 這時候就有可能報錯。
簡單總結就是說jdk1.5之前會造成兩個問題:
1、線程間共享變量不可見性;
2、無序性(執行順序無法保證);
當然這個bug已經修復了,SUN官方調整了JVM,具體了Volatile關鍵字,因此在jdk1.5之前只需要寫成這樣既可, private Volatitle static Singleton instance; 這樣就可以保證每次都是從主內存中取,當然這樣寫或多或少的回影響性能,但是為了安全起見,這點性能犧牲還是值得。