以前文章已經說過了設計模式的七大原則,即接口屏蔽原則,開閉原則,依賴倒轉原則,迪米特原則,里氏替換原則,單一職責原則,合成複用原則,不明白的,能夠移至XXXX(代寫)。從今天開始咱們就要學習一些常見的設計模式,方便咱們之後看源碼使用,固然,也能夠指導咱們日常的編碼任務。
設計模式
咱們常見的設計模式主要有23種,分爲3種類型,咱也不全說,只寫重要的幾個把。緩存
建立型:單例模式,工廠模式,原型模式安全
結構型:適配器模式,裝飾模式,代理模式bash
行爲型:模板模式,觀察者模式,狀態模式,責任鏈模式markdown
系統中只須要一個全局的實例,好比一些工具類,Converter,SqlSession等。多線程
咱們須要將構造方法私有化,而默認不寫的話,是公有的構造方法,外部能夠顯式的調用來建立對象,咱們的目的是讓外部不能建立對象。框架
對外只提供一個公有的的方法,用來獲取實例,而這個實例是不是惟一的,單例的,由方法決定,外部無需關心。jvm
public class A { //私有的構造方法 private A(){} //私有的靜態變量 private final static A a=new A(); //對外的公有方法 public static A getInstance(){ return a; } }複製代碼
public class test { public static void main(String[] args){ A a1=A.getInstance(); System.out.println(a1.hashCode()); A a2=A.getInstance(); System.out.println(a2.hashCode()); } }複製代碼
該方法採用的靜態常量的方法來生成對應的實例,其只在類加載的時候就生成了,後續並不會再生成,因此其爲單例的。函數
在類加載的時候,就完成實例化,避免線程同步問題。工具
沒有達到懶加載的效果,若是從始到終都沒有用到這個實例,可能會致使內存的浪費。
public class A { //私有的構造方法 private A(){} //私有的靜態變量 private final static A a; //靜態代碼塊 static{ a=new A(); } //對外的公有方法 public static A getInstance(){ return a; } }複製代碼
public class test { public static void main(String[] args){ A a1=A.getInstance(); System.out.println(a1.hashCode()); A a2=A.getInstance(); System.out.println(a2.hashCode()); } }複製代碼
該靜態代碼塊的餓漢式單例模式與靜態變量的餓漢式模式大同小異,只是將初始化過程移到了靜態代碼塊中。
與靜態變量餓漢式的優缺點相似。
public class A { //私有的構造方法 private A(){} //私有的靜態變量 private static A a; //對外的公有方法 public static A getInstance(){ if(a==null){ a=new A(); } return a; } }複製代碼
同上。
該方法的確作到了用到即加載,也就是當調用getInstance的時候,才判斷是否有該對象,若是不爲空,則直接放回,若是爲空,則新建一個對象並返回,達到了懶加載的效果。
當多線程的時候,可能會產生多個實例。好比我有兩個線程,同時調用getInstance方法,並都到了if語句,他們都新建了對象,那這裏就不是單例的啦。
public class A { //私有的構造方法 private A(){} //私有的靜態變量 private static A a; //對外的公有方法 public synchronized static A getInstance(){ if(a==null){ a=new A(); } return a; } }複製代碼
同上。
經過synchronize關鍵字,解決了線程不安全的問題。若是兩個線程同時調用getInstance方法時,那就先執行一個線程,另外一個等待,等第一個線程運行結束了,另外一個等待的開始執行。
這種方法是解決了線程不安全的問題,卻給性能帶來了很大的問題,效率過低了,getInstance常常發生,每一次都要同步這個方法。
咱們想着既然是方法同步致使了性能的問題,咱們核心的代碼就是新建對象的過程,也就是new A();
的過程,咱們能不能只對部分代碼進行同步呢?
那就是方法5啦。
public class A { //私有的構造方法 private A(){} //私有的靜態變量 private static A a; public static A getInstance(){ if(a==null){ synchronized (A.class){ a=new A(); } } return a; } } 複製代碼
如上。
懶漢式的通用優勢,用到才建立,達到懶加載的效果。
這個沒有意義,並無解決多線程的問題。咱們能夠看到若是兩個線程同時調用getInstance方法,而且都已經進入了if語句,即synchronized的位置,即使同步了,第一個線程先執行,進入synchronized同步的代碼塊,建立了對象,另外一個進入等待狀態,等第一個線程執行結束,第二個線程仍是會進入synchronized同步的代碼塊,建立對象。這個時候咱們能夠發現,對這代碼塊加了synchronized沒有任何意義,仍是建立了多個對象,並不符合單例。
public class A { //私有的構造方法 private A() { } //私有的靜態變量 private volatile static A a; //對外的公有方法 public static A getInstance() { if (a == null) { synchronized (A.class) { if (a == null) { a = new A(); } } } return a; } } 複製代碼
同上。
強烈推薦使用,這種寫法既避免了在多線程中出現線程不安全的狀況,也能提升性能。
咱具體來講,若是兩個線程同時調用了getInstance方法,而且都已到達了if語句以後,synchronized語句以前,此時第一個線程進入synchronized之中,先判斷是否爲空,很顯然第一次確定爲空,那麼則新建了對象。等到第二個線程進入synchronized之中,先判斷是否爲空,顯然第一個已經建立了,因此即不新建對象。下次,不論是一個線程或者多個線程,在第一個if語句那就判斷出有對象了,便直接返回啦,根本進不了裏面的代碼。
就是這麼完美,沒有缺點,哈哈哈。
咱先來看一個概念,重排序
,也就是語句的執行順序會被從新安排。其主要分爲三種:
1.編譯器優化的重排序:能夠從新安排語句的執行順序。
2.指令級並行的重排序:現代處理器採用指令級並行技術,將多條指令重疊執行。
3.內存系統的重排序:因爲處理器使用緩存和讀寫緩衝區,因此看上去多是亂序的。
上面代碼中的a = new A();可能被被JVM分解成以下代碼:
// 能夠分解爲如下三個步驟
1 memory=allocate();// 分配內存 至關於c的malloc
2 ctorInstanc(memory) //初始化對象
3 s=memory //設置s指向剛分配的地址複製代碼
// 上述三個步驟可能會被重排序爲 1-3-2,也就是:
1 memory=allocate();// 分配內存 至關於c的malloc
3 s=memory //設置s指向剛分配的地址
2 ctorInstanc(memory) //初始化對象 複製代碼
一旦假設發生了這樣的重排序,好比線程A在執行了步驟1和步驟3,可是步驟2尚未執行完。這個時候線程B有進入了第一個if語句,它會判斷a不爲空,即直接返回了a。其實這是一個未初始化完成的a,即會出現問題。
因此咱們會將入volatile關鍵字,來禁止這樣的重排序,便可正常運行。
public class A { //私有構造函數 private A() { } //私有的靜態內部類 private static class B { //私有的靜態變量 private static A a = new A(); } //對外的公有方法 public static A getInstance() { return B.a; } }複製代碼
B在A裝載的時候並不會裝載,而是會在調用getInstance的時候裝載,這利用了JVM的裝載機制。這樣一來,優勢有兩點,其一就是沒有A加載的時候,就裝載了a對象,而是在調用的時候才裝載,避免了資源的浪費。其二是多線程狀態下,沒有線程安全性的問題。
沒有缺點,太完美啦。
若是不明白反射,能夠查看我以前的文章,傳送門,萬字總結之反射(框架之魂)。
若是咱們的對象是經過反射方法invoke出來,這樣新建的對象與經過調用getInstance新建的對象是不同的,具體咱來看代碼。
public class test { public static void main(String[] args) throws Exception { A a=A.getInstance(); A b=A.getInstance(); System.out.println("a的hash:"+a.hashCode()+",b的hash:"+b.hashCode()); Constructor<A> constructor=A.class.getDeclaredConstructor(); constructor.setAccessible(true); A c=constructor.newInstance(); System.out.println("a的hash:"+a.hashCode()+",c的hash:"+c.hashCode()); } }複製代碼
咱們來看下運行結果:
咱們能夠看到c的hashcode是和a,b不同,由於c是經過構造器反射出來的,由此能夠證實私有構造器所組成的單例模式並非十分安全的。
咱們先將A類實現一個Serializable接口,具體代碼以下,跟以前的雙重if檢查同樣,只是多了個接口。
public class A implements Serializable { //私有的構造方法 private A() { } //私有的靜態變量 private volatile static A a; //對外的公有方法 public static A getInstance() { if (a == null) { synchronized (A.class) { if (a == null) { a = new A(); } } } return a; } } 複製代碼
測試類:
public class test { public static void main(String[] args) throws Exception { A s = A.getInstance(); //寫 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("學習Java的小姐姐")); oos.writeObject(s); oos.flush(); oos.close(); //讀 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("學習Java的小姐姐")); A s1 = (A)ois.readObject(); ois.close(); System.out.println(s+"\n"+s1); System.out.println("序列化先後兩個是否同一個:"+(s==s1)); } }複製代碼
咱們來看下運行結果,很顯然序列化先後兩個對象並不相等。爲何會出現這種問題呢?這個講起來,又能夠寫一篇文章了。簡單來講,任何一個readObject方法,不論是顯式的仍是默認的,它都會返回一個新建的實例,這個新建的實例不一樣於該類初始化時建立的實例。
public enum A { a; public A getInstance(){ return a; } } 複製代碼
看着代碼量不多,咱們將其編譯下,代碼以下:
public final class A extends Enum< A> { public static final A a; public static A[] values(); public static AvalueOf(String s); static {};
}複製代碼
public class test { public static void main(String[] args) throws Exception { A a1 = A.a; A a2 = A.a; System.out.println("正常狀況下,實例化兩個實例是否相同:" + (a1 == a2)); Constructor<A> constructor = null; constructor = A.class.getDeclaredConstructor(); constructor.setAccessible(true); A a3 = null; a3 = constructor.newInstance(); System.out.println("a1的hash:" + a1.hashCode() + ",a2的hash:" + a2.hashCode() + ",a3的hash:" + a3.hashCode()); System.out.println("經過反射攻擊單例模式狀況下,實例化兩個實例是否相同:" + (a1 == a3)); } }複製代碼
運行結果:
咱們看到報錯了,是在尋找構造函數的時候報錯的,即沒有無參的構造方法,那咱們看下他繼承的父類ENUM有沒有構造函數,看下源碼,發現有個兩個參數String和int類型的構造方法,咱們再看下是否是構造方法的問題。
咱們再用父類的有參構造方法試下,代碼以下:
public class test { public static void main(String[] args) throws Exception { A a1 = A.a; A a2 = A.a; System.out.println("正常狀況下,實例化兩個實例是否相同:" + (a1 == a2)); Constructor<A> constructor = null; constructor = A.class.getDeclaredConstructor(String.class,int.class);//其父類的構造器 constructor.setAccessible(true); A a3 = null; a3 = constructor.newInstance("學習Java的小姐姐",1); System.out.println("a1的hash:" + a1.hashCode() + ",a2的hash:" + a2.hashCode() + ",a3的hash:" + a3.hashCode()); System.out.println("經過反射攻擊單例模式狀況下,實例化兩個實例是否相同:" + (a1 == a3)); } }複製代碼
運行結果以下:
咱們發現報錯信息的位置已經換了,如今是已經有構造方法,而是在newInstance方法的時候報錯了,咱們跟下源碼發現,人家已經明確寫明瞭若是是枚舉類型,直接拋出異常,代碼以下,因此是沒法使用反射來操做枚舉類型的數據的。
public class test { public static void main(String[] args) throws Exception { A s = A.a; //寫 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("學習Java的小姐姐")); oos.writeObject(s); oos.flush(); oos.close(); //讀 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("學習Java的小姐姐")); A s1 = (A)ois.readObject(); ois.close(); System.out.println(s+"\n"+s1); System.out.println("序列化先後兩個是否同一個:"+(s==s1)); } }複製代碼
運行結果;
避免了反射帶來的對象不一致問題和反序列問題,簡單來講,就是簡單高效沒問題。
看到這裏的都是真愛的,在這裏先謝謝各位大佬啦。
單例模式是最簡單的一種設計模式,主要包括八種形式,分別是餓漢式靜態變量,餓漢式靜態代碼塊,懶漢式線程不安全,懶漢式線程安全,懶漢式線程不安全(沒啥意義),懶漢式雙重否認線程安全,內部靜態類,枚舉類型。
這幾種最優的是枚舉類型和內部靜態類,其次是懶漢式雙重否認,剩下的都差很少啦。
若是有說的不對的地方,還請各位指正,我好繼續學習去。
小姐姐陪你學習,陪你走心。