單例模式雖然簡單,卻是面試中經常出現的一類問題。
1 單例模式
單例模式的特點:
- 一是某個類只能有一個實例
- 二是它必須自行創建這個實例
- 三是它必須自行向整個系統提供這個實例
應用情況:對於多個對象使用同一個配置信息時,就需要保證該對象的唯一性。
如何保證對象的唯一性?
- 一不允許其他程序用new創建該類對象。
- 二在該類創建一個本類實例
- 三對外提供一個方法讓其他程序可以獲取該對象
實現的方法:
- 一是構造函數私有化
- 二是類定義中含有一個該類的靜態私有對象
- 三是該類提供了一個靜態的公共的函數用於創建或獲取它本身的靜態私有對象
方法一 餓漢式
public class Person1 { //定義該類的靜態私有對象 private static final Person1 person1 =new Person1(); //構造函數私有化 private Person1(){ }; //一個靜態的公共的函數用於創建或獲取它本身的靜態私有對象 public static Person1 getPerson1() { return person1; } }
該方法雖然在多線程下也能正確運行但是不能實現延遲加載(什么是延遲加載?)
資源效率不高,可能getPerson1()永遠不會執行到,但執行該類的其他靜態方法或者加載了該類(class.forName),那么這個實例仍然初始化
方法二 懶漢式
public class Person1 { private static Person1 person1 =null; private Person1(){ } public static Person1 getPerson1() { if(person1==null){ person1=new Person1(); } return person1; } }
該方法只能在單線程下運行,當在多線程下運行時可能會出現創建多個實例的情況。
對該方法可以進行優化
懶漢式 (優化一)
public class Person1 { private static Person1 person1 =null; private Person1(){ } public static synchronized Person1 getPerson1() { if(person1==null){ person1=new Person1(); } return person1; } }
該方法雖然能保證多線程下正常運行,但是效率很低,因為 person1=new Person1(); 這句話在整個程序運行中只執行一次,但是所有調用getPerson1的線程都要進行同步,這樣會大大減慢程序的運行效率。所以雖然該優化解決了問題但是並不好。
懶漢式 (優化二)
public class Person1 { private static Person1 person1 =null; private Person1(){ } public static Person1 getPerson1() { if(person1==null){ synchronized(Person1.class){ if(person1==null) person1=new Person1(); } } return person1; } }
這個優化比較好的解決了多線程問題,而且效率也很好,同時也兼顧了lazy loading。
今天看了《大話設計模式》終於明白雙重判空的的意義:
對於person1存在的情況,就直接返回。當person1為null並且同時存在兩個線程調用getPerson1()方法時,它們都將通過第一重的person1==null的判斷。
然后由於類鎖機制,這兩個線程只有一個可以獲得鎖並進入,另一個在外排隊等候,必須要其中一個進入並出來后,另一個才能進入。
而此時如果沒有了第二重的person1==null是否為null的判斷,則第一個線程創建了實例,而第二個線程獲得鎖后還是可以繼續再創建新的實例,這就沒有達到單例的目的。
ps:這講解真的是通俗易懂。看完之后,對於單例怎么寫,為什么要這么設計,都有了個清晰的認識。知識在書中往往有一個比較透徹的講解,I like it。
對於getPerson1()方法的訪問控制符,之前也一直停留在知道的階段,為什么這么用並不清楚。然后自己寫了個代碼,使用private修飾,發現這個方法只能在當前類中引用,在類外就無法使用了。所以必須用public,實踐是學習代碼最快的方式,只看不練跟沒學一樣!!!!
volatile關鍵字在多線程中防止指令重排序。
public class Singleton { private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { if (uniqueInstance == null) { synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
考慮下面的實現,也就是只使用了一個 if 語句。在 uniqueInstance == null 的情況下,如果兩個線程都執行了 if 語句,那么兩個線程都會進入 if 語句塊內。雖然在 if 語句塊內有加鎖操作,但是兩個線程都會執行 uniqueInstance = new Singleton();
這條語句,只是先后的問題,那么就會進行兩次實例化。因此必須使用雙重校驗鎖,也就是需要使用兩個 if 語句。
if (uniqueInstance == null) { synchronized (Singleton.class) { uniqueInstance = new Singleton(); } }
uniqueInstance 采用 volatile 關鍵字修飾也是很有必要的, uniqueInstance = new Singleton();
這段代碼其實是分為三步執行:
- 為 uniqueInstance 分配內存空間
- 初始化 uniqueInstance
- 將 uniqueInstance 指向分配的內存地址
但是由於 JVM 具有指令重排的特性,執行順序有可能變成 1>3>2。指令重排在單線程環境下不會出現問題,但是在多線程環境下會導致一個線程獲得還沒有初始化的實例。例如,線程 T1 執行了 1 和 3,此時 T2 調用 getUniqueInstance() 后發現 uniqueInstance 不為空,因此返回 uniqueInstance,但此時 uniqueInstance 還未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保證在多線程環境下也能正常運行。
happens-before規則,指令重排序在多線程中的不安全性
方法三縮小同步鎖的范圍
我們只是需要在實例還沒有創建之前需要加鎖操作,以保證只有一個線程創建出實例。而當實例已經創建之后,我們已經不需要再做加鎖操作了。
使用Lock與使用同步方法有點相似,只是使用Lock時顯式使用Lock對象作為同步鎖,而使用同步方法時系統隱式使用當前對象作為同步監視器,同樣都符合“加鎖”——>修改——>釋放鎖 的操作模式。
在實現線程安全的控制中,比較常用的是ReentrantLock(可重入鎖)。使用該Lock對象可以顯式地加鎖、釋放鎖。使用格式如下;
import java.util.concurrent.locks.ReentrantLock; /** * ClassName:X <br/> * Function: ReentrantLock 語法格式 * Date: 2017年11月30日 上午11:17:45 <br/> * @author prd-lxw * @version 1.0 * @since JDK 1.7 * @see */ public class X { //定義對象 private final ReentrantLock lock = new ReentrantLock(); //.. //定義需要保證線程安全的方法 public void m(){ //加鎖 lock.lock(); try{ //需要保證線程安全的代碼 // ...method body } //使用finally塊來保證釋放鎖 finally{ lock.unlock(); } } }
使用ReentrantLock 對象來進行同步,加鎖和釋放鎖出現在不同的作用范圍內時,通常建議使用finally塊來確保在必要時釋放鎖。
這里我們使用Java中的Lock對象,並進行雙重校驗:
import java.util.concurrent.locks.ReentrantLock; public class Person1 { private static Person1 person1 =null; private static ReentrantLock lock = new ReentrantLock(false); // 創建可重入鎖,false代表非公平鎖 private Person1(){ } public static Person1 getPerson1() { if(person1==null){ lock.lock(); try{ if(person1==null) person1=new Person1(); }finally{ lock.unlock(); } } return person1; } }
該方法和懶漢式 (優化二)非常類似。因為一個類中 lock對象是唯一的,相當於一把類鎖。
可行的解決辦法;類裝載時初始化實例
/** * ClassName:Singleton4 <br/> * Function: 類裝載時初始化 * Date: 2017年11月30日 下午3:08:05 <br/> * @author prd-lxw * @version 1.0 * @since JDK 1.7 * @see */ public class Singleton4 { private static Singleton4 instance = new Singleton4(); private Singleton4() { System.out.println("初始化"); } public static Singleton4 getInstance() { return instance; } public static void main(String[] args) { // 任何代碼都不寫,此時打印一次"初始化",不能達到延遲加載 // getInstance(); //注釋打開,只打印一次"初始化" } }
這個方法是在類裝載時就初始化instance,雖然避免了多線程同步問題,但是沒有達到lazy loading的效果
方法四 靜態內部類方法實現
/** * ClassName:Singleton5 <br/> * Function: 靜態內部類來實現單例,能夠達到延遲加載和多線程的目的 * Date: 2017年11月30日 下午3:02:56 <br/> * @author prd-lxw * @version 1.0 * @since JDK 1.7 * @see */ public class Singleton5 { private Singleton5() { System.out.println("初始化成員"); } private static class SingletonHolder { private static final Singleton5 INSTANCE = new Singleton5(); } public static Singleton5 getInstance() { return SingletonHolder.INSTANCE; } public static void main(String[] args){ //不打印任何內容,可以達到延遲加載的目的 } }
這種方法也能保證線程安全,而且也達到了lazy loading的效果
參考:https://www.cnblogs.com/paul011/p/8574650.html
方法五 枚舉
public enum Person1 { person1; String name=new String("ssss"); public String getName() { return name; } public void setName(String name) { this.name = name; } }
public class test { public static void main(String[] args) { Person1 person1 =Person1.person1; Person1 person2=Person1.person1 ; person1.setName("aaa"); person2.setName("bbb"); System.out.println(person1.getName()); System.out.println(person2.getName()); } }
輸出結果都為bbb
總結:相對來說懶漢式 (優化二),縮小同步鎖范圍,靜態內部類,枚舉等方法是比較好的方法。
2 其它問題
延遲加載機制是為了避免一些無謂的性能開銷而提出來的,所謂延遲加載就是當在真正需要數據的時候,才真正執行數據加載操作。可以簡單理解為,只有在使用的時候,才會發出sql語句進行查詢。
懶加載---即為延遲加載,顧名思義在需要的時候才加載,這樣做效率會比較低,但是占用內存低,iOS設備內存資源有限,如果程序啟動使用一次性加載的方式可能會耗盡內存,這時可以使用懶加載,先判斷是否有,沒有再去創建
延遲加載也叫動態函數加載,它是一種模式允許開發者指定程序的什么組件不應該在程序啟動的時候默認讀入內存。通常情況下,系統加載程序會同時自動加載初始程序和從屬組件。在遲加載中這些組件只在調用的時候才加載。當程序有許多從屬組件而且並不常用的時候,遲加載可以用於提高程序的性能。
延遲加載可能出現的問題:
第一,延遲加載搞不好就容易導致N+1 select問題,性能反而不能保障 第二,延遲加載一般是在ORM中采用字節碼增強方式來實現,這種方式處理后的對象往往會導致對象序列化失效,而在大型web應用中一般都會采用 獨立的緩存架構,一但應用系統引入獨立的緩存系統對應用數據對象進行緩存,采用延遲加載后的對象序列化將失效,導致緩存失敗。 第三,ORM中的延遲加載將業務對象的加載關系搞得不清不楚,如果某天想換ORM,那么還得針對不同的ORM處理延遲加載關系,即使不還ORM后來人想理解加載關系也會很頭疼。 第四,延遲加載目的是一方面是對了使應用只加載必要的數據,減少數據傳輸量,提高查詢速度。另一方面,為了減輕數據庫的進行不必要查詢而進行運行增加的壓力,避免一次性進行過多的查詢,減少系統消耗。對於第一個問題,通過必要的緩存一般可以解決,對於這點系統消耗一般還是可以承受;對於第二個問題,通過在業務層進行單表查詢配合必要的索引一般也是不存在問題的。 第五,從另外一方面考慮,ORM需要承擔的僅僅是O R M,和事務、緩存等特性一樣,它們應該由其他更有資格的家伙來承擔,不需要搞那么負載,否則對於以后的底層擴展,那可是一個艱巨的工作。
/** * ClassName:Singleton4 <br/> * Function: 類裝載時初始化 * Date: 2017年11月30日 下午3:08:05 <br/> * @author prd-lxw * @version 1.0 * @since JDK 1.7 * @see */public class Singleton4 {private static Singleton4 instance = new Singleton4(); private Singleton4() { System.out.println("初始化"); } public synchronized static Singleton4 getInstance() { return instance; }
public static void main(String[] args) {//任何代碼都不寫,此時打印一次"初始化"//getInstance();//注釋打開,只打印一次"初始化"}
}