淺談Java常見設計模式(二)
你,發(fā)如雪,凄美了離別
我焚香感動了誰
邀明月,讓回憶皎潔
愛在月光下完美
你,發(fā)如雪,紛飛了眼淚
我等待蒼老了誰
紅塵醉,微醺的歲月
我用無悔,刻永世愛你的碑
接淺談Java常見設計模式(一),上文說到了關(guān)于如何創(chuàng)建城市的對比信息,一般來說,對比城市,需要創(chuàng)建所有的信息保證唯一性,以免到時候維度不唯一了,導致對比性出現(xiàn)了差異。比如,如果創(chuàng)建城市基本信息,那么關(guān)于每一個城市,只需要創(chuàng)建出唯一的一個就好了,這時候,就需要用到單例模式,即在程序運行時,對象只有一份!
一般來說,單例模式具有4種實現(xiàn)方式:
餓漢式單例:
餓漢式單例,即對象被加載到內(nèi)存中后,初始化改對象,然后使用的時候直接獲取:
package com.lgli.create.single;/*** 單例模式* @author lgli*/public class Single {public static void main(String[] args) {ChongQingCityBase base = ChongQingCityBase.getInstance();System.out.println(base);}}/*** 重慶市基本信息* @author lgli*/class?ChongQingCityBase?extends?CityBase?{private static final ChongQingCityBase chongQingCityBase????????????=?new?ChongQingCityBase();private ChongQingCityBase (){}public static ChongQingCityBase getInstance(){return chongQingCityBase;}void base() {System.out.println("重慶市基本信息");}}/*** 抽象城市基本信息類*/abstract class CityBase{abstract void base();}
這里用多線程來測試下,是否都只實例化了一個對象:

這里定義了1000個線程運行,得到的對象都是一個
餓漢式單例,是無論是否需要用到,在類被加載到內(nèi)存中的時候,就已經(jīng)在堆中實例化了這個對象,
這時候,就有點尷尬了,假設從來沒有用到過,那么這就有點浪費內(nèi)存了,
所以,懶漢式單例應運而生
懶漢式單例:
懶漢式單例,是需要用到這個對象的時候,才實例化這個對象
package com.lgli.create.single.singlev2;/**** 單例模式--懶漢式* @author lgli*/public class Single {public static void main(String[] args) {ChongQingCityBase base = ChongQingCityBase.getInstance();System.out.println(base);}}/*** 重慶市基本信息* @author lgli*/class ChongQingCityBase extends CityBase {private static ChongQingCityBase chongQingCityBase;private ChongQingCityBase (){}public static ChongQingCityBase getInstance(){if(chongQingCityBase == null){chongQingCityBase = new ChongQingCityBase();}return chongQingCityBase;}}/*** 抽象城市基本信息類*/abstract class CityBase{}
這里先不初始化對象,當調(diào)用getInstance方法的時候,才初始化這個對象,同時判斷當前對象是否存在,如果存在,則直接返回
這種情況,在單線程的情況下,是沒有什么問題的,可是當遇到多個線程并發(fā)的時候,就出現(xiàn)問題了:

如上圖所示,這里用100個線程模擬并發(fā),出現(xiàn)了多個實例
來看下這個代碼:

分析下,并發(fā)的情況下,
當某個線程執(zhí)行到44行的時候,這個時候chongQingCityBase是null的,此時,線程切換,另外一個線程也走到44行,判斷也是為空的,那么這2個線程,均符合if判斷,去new一個對象,這時候就出現(xiàn)了多個實例的情況。
那么解決這個問題吧
首先想到的就是加鎖吧,首先可能想到把這個實例化方法加鎖,這樣子肯定是可能滿足只產(chǎn)生一個對象的,因為畢竟方法鎖的話,多個線程都會等待方法鎖的釋放才能調(diào)用這個方法。

如果,實例化方法里面可能還有其他邏輯,這樣子暴力加鎖肯定不是一個很好的解決方案,
所以只需要在關(guān)鍵的地方加鎖:
比如:

這里將加鎖加到初始化實例對象的地方,然后同樣的用多線程去運行下結(jié)果,這里為了解決可能因為多線程導致JVM指令重排<JVM中將不影響單線程代碼實際效果的指令下,顛倒執(zhí)行順序>出錯,這里將這個成員變量加上volatile。

這里可以看見,依然有多個實例的產(chǎn)生,即此種加鎖的方式,是存在問題的,可能導致初始化多個對象。
簡單分析下原因:
當多個線程同時走過if判斷,此時chongQingCityBase是null的,然后進入到加鎖的方法,這時候出現(xiàn)的結(jié)果就是會有多個對象被實例化了
即:

線程1和2在chongQingCityBase為null時,同時進入if判斷。
如何解決這個問題呢?
這就是懶漢式加載的雙重判斷機制,即:

這里加了雙重的校驗,即在加鎖的方法內(nèi)部,也同樣執(zhí)行判斷。
此時,看下運行結(jié)果<記住這里的volatile關(guān)鍵字,否則也是有可能出現(xiàn)多個實例化對象的,具體原因,后續(xù)會說到>:

此時,實例化的對象就只有一個了,
那么這個地方有個問題就是,這個雙重判斷外面這個判斷的意義是什么呢?
如果沒有外面的判斷,意味著每次執(zhí)行方法的時候,都會進去到線程等待的方法,和在方法上加鎖沒有太大的區(qū)別,所以為了性能考慮,加上這個外層判斷
內(nèi)部類單例:
看下面代碼:
package com.lgli.create.single.singlev3;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;/*** 單例模式 -- 內(nèi)部類* @author lgli*/public class Single {public static void main(String[] args) {ExecutorService executorService = Executors.newCachedThreadPool();for(int i = 0 ; i < 100 ; i++){executorService.execute(()->{ChongQingCityBase base = ChongQingCityBase.getInstance();try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().toString()+base);});}executorService.shutdown();}}/*** 重慶市基本信息* @author lgli*/class ChongQingCityBase extends CityBase {private ChongQingCityBase (){}public static ChongQingCityBase getInstance(){return ChongQingCityBaseHolder.CHONGQINGCITYBASE;}private static class ChongQingCityBaseHolder{private static final ChongQingCityBaseCHONGQINGCITYBASE = new ChongQingCityBase();}}/*** 抽象城市基本信息類*/abstract class CityBase{}
運行結(jié)果,也是只有一個實例的產(chǎn)生,
那么這種靜態(tài)內(nèi)部類的實現(xiàn)原理是什么樣子的呢?
涉及到JVM類加載的一些邏輯,大致情況是這樣子的:
當ChongQingCityBase被加載的時候,會優(yōu)先加載內(nèi)部類
ChongQingCityBaseHolder
當有程序調(diào)用
com.lgli.create.single.singlev3.ChongQingCityBase#getInstance
方法的時候,
執(zhí)行
ChongQingCityBaseHolder.CHONGQINGCITYBASE
此時才會去執(zhí)行內(nèi)部類的
private static final ChongQingCityBase CHONGQINGCITYBASE = new ChongQingCityBase();
所以這里的內(nèi)部類方式,其實也屬于懶漢式加載
暴力破壞單例-->反射:
有時候,可能一個單例對象,會被人誤用反射機制暴力破解,創(chuàng)建出多個實例,這里以為內(nèi)部類為例,代碼指出反射暴力創(chuàng)建對象導致單例失效的情況,其他懶漢/餓漢類似也可以通過此方法破解:
package com.lgli.create.single.breaksingle;import java.lang.reflect.Constructor;/*** 反射破壞單例* @author lgli*/public class BreakSingle {public static void main(String[] args) throws Exception {//反射創(chuàng)建對象//根據(jù)對象路徑獲取類Class<?> aClass = Class.forName("com.lgli.create.single.breaksingle.ChongQingCityBase");//獲取申明的沒有參數(shù)的構(gòu)造方法Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(null);//設置強訪問declaredConstructor.setAccessible(true);//獲取實例對象ChongQingCityBase o = (ChongQingCityBase)declaredConstructor.newInstance();//正常創(chuàng)建對象ChongQingCityBase p = ChongQingCityBase.getInstance();System.out.println(o);System.out.println(p);System.out.println(o==p);}}/*** 重慶市基本信息* @author lgli*/class ChongQingCityBase extends CityBase {private ChongQingCityBase (){}public static ChongQingCityBase getInstance(){return ChongQingCityBase.ChongQingCityBaseHolder.CHONGQINGCITYBASE;}/*** 內(nèi)部類的單例*/private static class ChongQingCityBaseHolder{private static final ChongQingCityBaseCHONGQINGCITYBASE = new ChongQingCityBase();}}/*** 抽象城市基本信息類*/abstract class CityBase{}
運行可以獲得兩個不同的對象:

這里可以看到通過反射實例化了一個對象,然后通過正常創(chuàng)建又獲取了一個對象。
那么如何規(guī)避這個問題呢?
可以在構(gòu)造方法中加入判斷:
比如:

可以直接在構(gòu)造方法中,拋出異常,不允許構(gòu)建實例。
這時候,反射機制構(gòu)建實例就會拋出異常,只能正常的構(gòu)建了。

除此之外,還可以通過序列化的方式,破壞單例
暴力破壞單例-->序列化:
看下面代碼:
package com.lgli.create.single.breaksinglev2;import java.io.*;/*** 序列化破壞單例* @author lgli*/public class SerializableSingle {public static void main(String[] args) {//根據(jù)正常構(gòu)建方法獲取的對象ChongQingCityBase base1 = ChongQingCityBase.getInstance();//序列化后反序列化的對象ChongQingCityBase base2 = null;try{//序列化對象FileOutputStream fileOutputStream = new FileOutputStream("../ChongQingCityBase.obj");ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);objectOutputStream.writeObject(base1);objectOutputStream.flush();objectOutputStream.close();fileOutputStream.close();//反序列化實例對象FileInputStream fileInputStream = new FileInputStream("../ChongQingCityBase.obj");ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);base2 = (ChongQingCityBase)objectInputStream.readObject();fileInputStream.close();objectInputStream.close();//對比兩個結(jié)果對象System.out.println(base1);System.out.println(base2);System.out.println(base1 == base2);}catch (Exception e){e.printStackTrace();}}}/*** 重慶市基本信息* @author lgli*/class ChongQingCityBase extends CityBase implements Serializable {private ChongQingCityBase (){if(ChongQingCityBase.ChongQingCityBaseHolder.CHONGQINGCITYBASE != null){throw new RuntimeException("不允許構(gòu)建多個實例");}}public static ChongQingCityBase getInstance(){return ChongQingCityBase.ChongQingCityBaseHolder.CHONGQINGCITYBASE;}/*** 內(nèi)部類的單例*/private static class ChongQingCityBaseHolder{private static final ChongQingCityBaseCHONGQINGCITYBASE = new ChongQingCityBase();}}/*** 抽象城市基本信息類*/abstract class CityBase{}
運行代碼結(jié)果輸出:

那么針對這種破壞單例的方法,如何預防呢?
這時候需要在實例中寫個方法:

如上圖所示,這里需要寫這個readResolve方法,
這時候運行程序:

顯然,這個時候,只獲取了一個實例。
那么這個是為啥呢?
這里簡單描述下,畫個圖

這里是反序列化的時候,調(diào)用對象創(chuàng)建方法的源碼,在
java.io.ObjectInputStream#readObject0
調(diào)用
java.io.ObjectInputStream#readOrdinaryObject
之后,會返回這個創(chuàng)建的對象
那么看下
java.io.ObjectInputStream#readOrdinaryObject
這個方法究竟做了些什么事呢?
private Object readOrdinaryObject(boolean unshared)throws IOException{if (bin.readByte() != TC_OBJECT) {throw new InternalError();}ObjectStreamClass desc = readClassDesc(false);desc.checkDeserialize();Class<?> cl = desc.forClass();if (cl == String.class || cl == Class.class|| cl == ObjectStreamClass.class) {throw new InvalidClassException("invalid class descriptor");}Object obj;try {obj = desc.isInstantiable() ? desc.newInstance() : null;} catch (Exception ex) {throw (IOException) new InvalidClassException(desc.forClass().getName(),"unable to create instance").initCause(ex);}passHandle = handles.assign(unshared ? unsharedMarker : obj);ClassNotFoundException resolveEx = desc.getResolveException();if (resolveEx != null) {handles.markException(passHandle, resolveEx);}if (desc.isExternalizable()) {readExternalData((Externalizable) obj, desc);} else {readSerialData(obj, desc);}handles.finish(passHandle);if (obj != null &&handles.lookupException(passHandle) == null &&desc.hasReadResolveMethod()){Object rep = desc.invokeReadResolve(obj);if (unshared && rep.getClass().isArray()) {rep = cloneArray(rep);}if (rep != obj) {// Filter the replacement objectif (rep != null) {if (rep.getClass().isArray()) {filterCheck(rep.getClass(), Array.getLength(rep));} else {filterCheck(rep.getClass(), -1);}}handles.setObject(passHandle, obj = rep);}}return obj;}
上面代碼是JDK源碼,這里主要看關(guān)鍵部分

這里先判斷是否滿足isInstantiable()
這個方法其實是判斷是否有構(gòu)造方法的,這里就不往下看了
很明顯,是有構(gòu)造方法的,雖然是private的,但是對JDK來說,這都不重要
然后滿足有構(gòu)造方法,則創(chuàng)建實例對象,這時候創(chuàng)建的實例對象和正常創(chuàng)建的肯定是兩回事。所以序列化再反序列化后的對象,肯定不是同一個了,這里就理解到了
再接著看下面的代碼:

這里緊接著上面的代碼走,有一個判斷,然后滿足判斷之后,則調(diào)用
java.io.ObjectStreamClass#invokeReadResolve
方法,實例化新的對象出來,
當對象寫了readResolve方法后,就滿足了這里的if條件,然后會調(diào)用對象中的readResolve方法,而我們寫的readResolve方法就是返回單例對象,所以這里同樣返回了單例對象,覆蓋了前面所創(chuàng)建的實例對象:

所以得到的對象是一個單例對象。
枚舉單例-->最牛單例寫法
最后介紹一個Java推薦的單例模式的寫法,枚舉單例
看下代碼:
package com.lgli.create.single.singlev4;import java.io.*;/**** 枚舉單例* @author lgli*/public class Single {public static void main(String[] args) {//根據(jù)正常構(gòu)建方法獲取的對象EnumSingle base1 = EnumSingle.getInstance();base1.setObj(ChongQingCityBase.getInstance());//序列化后反序列化的對象EnumSingle base2 = null;try{//序列化對象FileOutputStream fileOutputStream = new FileOutputStream("../ChongQingCityBase.obj");ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);objectOutputStream.writeObject(base1);objectOutputStream.flush();objectOutputStream.close();fileOutputStream.close();//反序列化實例對象FileInputStream fileInputStream = new FileInputStream("../ChongQingCityBase.obj");ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);base2 = (EnumSingle)objectInputStream.readObject();fileInputStream.close();objectInputStream.close();System.out.println(base1.getObj());//對比兩個結(jié)果對象System.out.println(base2.getObj());System.out.println(base1 == base2);}catch (Exception e){e.printStackTrace();}}}/**** 枚舉單例*/enum EnumSingle{INSTANCE;private Object obj;public Object getObj() {return obj;}public void setObj(Object obj) {this.obj = obj;}public static EnumSingle getInstance(){return INSTANCE;}}/*** 重慶市基本信息* @author lgli*/class ChongQingCityBase extends CityBase implements Serializable {private ChongQingCityBase (){}public static ChongQingCityBase getInstance(){return ChongQingCityBase.ChongQingCityBaseHolder.CHONGQINGCITYBASE;}private static class ChongQingCityBaseHolder{private static final ChongQingCityBaseCHONGQINGCITYBASE = new ChongQingCityBase();}}/*** 抽象城市基本信息類*/abstract class CityBase{}
這里,申明了一個枚舉,
同時枚舉類中有一個成員變量Object,
用來設置初始化的單例對象的
然后先測試下,這里通過序列化破壞單例,是否可以成功,
運行上述代碼:

這里,可以很明顯的看到,兩個對象實例是一樣的,即,在通過序列化,同時沒有寫readResolve方法,依然獲得了兩個一樣的對象,即序列化和反序列化并沒有破壞枚舉單例。
為何枚舉單例可以防止序列化呢?
同樣的,看下反序列化時,創(chuàng)建對象的時候,做了些什么事
和前面的邏輯調(diào)用圖基本一致,只不過在最后一個是調(diào)用
java.io.ObjectInputStream#readEnum
源碼如下:

這里,可以看到返回的對象是通過一個類名,和一個唯一的
名字name,構(gòu)建了這個返回對象,很顯然,在同一個應用中,這是唯一的,
所以構(gòu)建出來的對象也是唯一的。
那么線程安全么?
這里使用反編譯工具,將寫的這個枚舉單例類反編譯出來,這里使用的是JAD,其安裝使用就不多說了,記住一點就好,在window環(huán)境下,需要把jad.exe放到本機jdk的bin文件下
反編譯這個EnumSingle.class

在當前目錄下生成了一個EnumSingle.jad文件
打開這個文件:

關(guān)鍵位置,如上圖后面一個圖標識所示,這種方式屬于餓漢式單例,所以一定是線程安全的
然后又不能通過反序列化
那么反射呢?
測試下反射,這里使用反射獲取構(gòu)造方法是時候,獲取到的應該是帶2個參數(shù)的構(gòu)造方法,String和int,上面截圖標識有構(gòu)造方法。
看下代碼:
package com.lgli.create.single.singlev4;import java.io.*;import java.lang.reflect.Constructor;/**** 枚舉單例* @author lgli*/public class Single {public static void main(String[] args) {// serializableType();reflectType();}/*** 反射方式*/public static void reflectType(){try{Class<?> aClass = Class.forName("com.lgli.create.single.singlev4.EnumSingle");Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(String.class,int.class);declaredConstructor.setAccessible(true);EnumSingle lgli = (EnumSingle)declaredConstructor.newInstance("lgli", 520);System.out.println(lgli);}catch (Exception e){e.printStackTrace();????????}}/*** 序列化和反序列化破幻枚舉單例方式*/public static void serializableType(){//根據(jù)正常構(gòu)建方法獲取的對象EnumSingle base1 = EnumSingle.getInstance();base1.setObj(ChongQingCityBase.getInstance());//序列化后反序列化的對象EnumSingle base2 = null;try{//序列化對象FileOutputStream fileOutputStream = new FileOutputStream("../ChongQingCityBase.obj");ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);objectOutputStream.writeObject(base1);objectOutputStream.flush();objectOutputStream.close();fileOutputStream.close();//反序列化實例對象FileInputStream fileInputStream = new FileInputStream("../ChongQingCityBase.obj");ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);base2 = (EnumSingle)objectInputStream.readObject();fileInputStream.close();objectInputStream.close();System.out.println(base1.getObj());//對比兩個結(jié)果對象System.out.println(base2.getObj());System.out.println(base1 == base2);}catch (Exception e){e.printStackTrace();}}}/**** 枚舉單例*/enum EnumSingle{INSTANCE;private Object obj;public Object getObj() {return obj;}public void setObj(Object obj) {this.obj = obj;}public static EnumSingle getInstance(){return INSTANCE;}}/*** 重慶市基本信息* @author lgli*/class ChongQingCityBase extends CityBase implements Serializable {private ChongQingCityBase (){}public static ChongQingCityBase getInstance(){return ChongQingCityBase.ChongQingCityBaseHolder.CHONGQINGCITYBASE;}private static class ChongQingCityBaseHolder{private static final ChongQingCityBaseCHONGQINGCITYBASE = new ChongQingCityBase();}}/*** 抽象城市基本信息類*/abstract class CityBase{}
運行測試類,結(jié)果報錯了:

程序異常:Cannot reflectively create enum objects
無法通過反射創(chuàng)建枚舉對象
。。。。。。
這說明什么呢?說明JDK在從根本上解決了妄圖通過反射去構(gòu)建枚舉對象的幻想!
看下
java.lang.reflect.Constructor#newInstance
源碼:

所以,反射是不可行的。
總結(jié)起來,通過枚舉創(chuàng)建單例,無論是多線程、序列化、反射都無法改變單例,所以枚舉應該是最牛的吧。(#^.^#)
其實還要一種單例,屬于線程內(nèi)單例,但是線程之間不是單例的-->
ThreadLocal
這里就先不詳細解釋了,后續(xù)會說到這個問題
歡迎轉(zhuǎn)發(fā)關(guān)注,謝謝!
點個贊咯!
