精通Java設計模式從初見到相愛之單例設計模式(1)
序言:針對java初級工程師以上的級別(熟悉java基礎,會用ssm或者ssh增刪改查)!單例三大特性:自由序列化,執行緒安全,保證單例。
1、java設計模式是個什麼東西
你瞭解java的框架嗎?請問框架是幹嘛的?我的理解是,古代帶兵打仗,有偵察兵,有糧草兵,有打仗的兵,負責整個軍隊打勝仗的各個關鍵!同理,java的框架也是如此,分別是:sprinmvc/struts2負責連線前段,mybatis/hibernate負責操作資料庫,還有就是連線這兩個框架做到承上啟下的作用,明白了嗎?
然後再講下什麼是設計模式!它是個什麼東西呢,我的理解是,軍隊打仗,我大唐雄兵百萬,如何攻打楊廣皇帝的千萬大軍呢,哼哼,使用我的一字長蛇陣還是我的北斗七星陣,如果這些陣法打不贏話,那我們就用十面埋伏陣,乾的他不要不要的!所以,設計模式就相當於一個陣法,在程式中就相當於一個最優的解決方案!
2、單例設計模式的種類
總共五種:懶漢、餓漢、雙重校驗鎖、列舉、靜態內部類
我大LPL隊伍出征LCK那天,到底選誰呢,有的人說還有其他的種類,在我看來都是按照這五大種設計有一部分的改進而已,我大EDG不是還有一個而對叫EDE嗎?LCK隊伍中原來不是還有一個SSW 和SSB嗎,所以根基大致相同,就是這五種,如果別人說六種,多出來的那一種也沒有多大的區別。
第五步在講具體程式碼實現!
3、單例設計模式需要用的介面Serializable
嗯哼?這特麼單例設計模式還要實現介面?不不不!我的意思是這個設計模式實現了序列化,會造成什麼影響呢?你可以實現序列化介面,當然也可以不實現,但是!這曾經是一道面試題,來自《自如網》一家目前最大的租房公司的最基礎面試題之一,肯定比鏈家還要強勢一點。
implements Serializable 序列化
概念:把java物件轉換為位元組序列的過程,什麼場景用?你編寫程式把這個物件資料放到一個txt檔案中,就要把這個物件資料轉換為位元組碼檔案,寫到檔案中,然後讀取該檔案的時候把位元組碼檔案轉換為物件資料儲存到資料庫中,這就是序列化與反序列化。
但是我們都會想,單列設計模式序列化怎麼了?有啥影響呢?這就對了,你有這樣的想法就說明你思考了,我問你,反序列化在獲取這個單列設計模式的物件,但是你有沒有想過,我反序列化可以獲取多個單列設計模式的物件,然後你再想一下,單列設計模式的一個作用,那就是我只能設計一個物件,所以多個物件和一個物件之間,不成立,這就是問題,你可以說我不實現序列化介面不就ok了嗎,這種實現序列化的模式也不常見,對,一般工作也用不到,但是面試的時候你不得不知道!而且,我給你們截圖看這個設計模式的序列化
這個日期類算不算序列化呢,所以好多地方也會用到設計模式的序列化!但是怎麼解決呢,到第五步我在細說,我先把概念都說完。
4、單例設計模式的執行緒安全問題
什麼是執行緒安全?哼哼,這也是一個面試題!想想?好,那就想想!
你先想,我說一個別的東西,你理解單執行緒和多執行緒嗎?他們的區別我就不說了,如果多個執行緒同時訪問一個方法修改,這個執行緒修改中但此刻被另外一個執行緒修改這個方法,最終會錯誤!所以我們需要加鎖或者synchronized,所以執行緒安全的意思就是同步。 為什麼說這個,看第五步!
5、每種單例設計模式的程式碼實現
5.1 懶漢 真特懶,大學不到期末不抓緊學習,上班不到最後不交工期!
public class LazyPerson { private static LazyPerson lazyPerson = null; //私有構造器 private LazyPerson(){} //提供一個對外的公共的靜態方法訪問該變數,如果該變數沒有物件,則建立該物件 public static LazyPerson getLazyPerson(){ if (null == lazyPerson) { lazyPerson = new LazyPerson(); } return lazyPerson; } }
這個就是執行緒不安全,我多個執行緒同時一訪問,又會建立多個物件
當然你可以在上面的方法上加鎖,synchronized,這就ok了,但是影響效率
5.2 餓漢,餓的我都著急了,先幹了!
//餓漢 public class HungryPerson { private static HungryPerson instance = new HungryPerson(); private HungryPerson() {} public static HungryPerson getInstance(){ return instance; } }
5.3 雙重校驗將synchronized關鍵字加在了內部,呼叫的時候是不需要加鎖的,只有在instance為null,並建立物件的時候才需要加鎖,效能有一定的提升,但也沒什麼用!
//雙重校驗 public class Two { private static Two instance = null; private Two() { } public static Two getInstance() { if (null == instance) { synchronized (instance) { if (null == instance) { instance = new Two(); } } } return instance; } }
5.4 靜態內部類 解決雙重校驗低效能問題,使用內部類來維護單例的實現,JVM內部的機制能夠保證當一個類被載入的時候,這個類的載入過程是執行緒互斥的,這樣當我們第一次呼叫getInstance的時候,JVM能夠幫我們保證instance只被建立一次,並且會保證把賦值給instance的記憶體初始化完畢,這樣我們就不用擔心上面的問題,同時該方法也只會在第一次呼叫的時候使用互斥機制,這樣就解決了低效能問題。
public class StaticSignle { private StaticSignle() { } private static class SingeletonHolder{ public static StaticSignle instance() { return new StaticSignle(); } } public static StaticSignle getInstance(){ return SingeletonHolder.instance(); } }
5.5 列舉的單例設計模式
它在《Effective Java》中有提到,因為其功能完整、使用簡潔、無償地提供了序列化機制、在面對複雜的序列化或者反射攻擊時仍然可以絕對防止多次例項化等優點,單元素的列舉型別被作者認為是實現Singleton的最佳方法。
用到三個類Person EnumSignle 和一個main主程式類
public class Person { }
public enum EnumSingle { INSTANCE; private Person person =null; private EnumSingle(){ person= new Person(); } public Person getPerson(){ return person; } }
public class Main { public static void main(String[] args){ Person person1 = EnumSingle.INSTANCE.getPerson(); Person person2 = EnumSingle.INSTANCE.getPerson(); System.out.println(person1==person2); } }
結果是true!
5.6 解決單例設計模式的序列化問題,使用靜態內部類
import java.io.ObjectStreamException; import java.io.Serializable; public class MakeSignle implements Serializable { private static class SingletonClassInstance { private static final MakeSignle instance = new MakeSignle(); } // 方法沒有同步,呼叫效率高 public static MakeSignle getInstance() { return SingletonClassInstance.instance; } // 防止反序列化獲取多個物件的漏洞。 // 無論是實現Serializable介面,或是Externalizable介面,當從I/O流中讀取物件時,readResolve()方法都會被呼叫到。 // 實際上就是用readResolve()中返回的物件直接替換在反序列化過程中建立的物件。 // 當你把物件儲存到txt檔案中,物件轉換成位元組碼儲存,當你在獲取物件的資料的時候,位元組碼轉換為該物件,但是這個物件已經不是 // 之前的那個物件,所以用 readResolve可以替換成和之前一樣的物件 private Object readResolve() throws ObjectStreamException { return SingletonClassInstance.instance; } }
5.7有些安全問題,通過反射會訪問單列的私有構造方法,所以我們同5.6一樣也是構造一個靜態內部類
就加了一個方法,防止反射的
import java.io.ObjectStreamException; import java.io.Serializable; public class MakeSignle implements Serializable { private static class SingletonClassInstance { private static final MakeSignle instance = new MakeSignle(); } // 方法沒有同步,呼叫效率高 public static MakeSignle getInstance() { return SingletonClassInstance.instance; } // 防止反射獲取多個物件的漏洞 private MakeSignle() { if (null != SingletonClassInstance.instance) throw new RuntimeException(); } // 防止反序列化獲取多個物件的漏洞。 // 無論是實現Serializable介面,或是Externalizable介面,當從I/O流中讀取物件時,readResolve()方法都會被呼叫到。 // 實際上就是用readResolve()中返回的物件直接替換在反序列化過程中建立的物件。 // 當你把物件儲存到txt檔案中,物件轉換成位元組碼儲存,當你在獲取物件的資料的時候,位元組碼轉換為該物件,但是這個物件已經不是 // 之前的那個物件,所以用 readResolve可以替換成和之前一樣的物件 private Object readResolve() throws ObjectStreamException { return SingletonClassInstance.instance; } }
測試這個反射,在main函式中程式碼如下:
/** * 通過反射的方式直接呼叫私有構造器(通過在構造器裡丟擲異常可以解決此漏洞) */ Class<MakeSignle> clazz = MakeSignle.class; Constructor<MakeSignle> c = clazz.getDeclaredConstructor(null); c.setAccessible(true); // 跳過許可權檢查 MakeSignle sc3 = c.newInstance(); MakeSignle sc4 = c.newInstance(); System.out.println(sc3==sc4); }
執行結果:
如果我們把拋異常的登出掉,如圖,main執行結果為false,並不是一個物件,所以這種拋異常的方法解決了反射入侵的問題
這基本涵蓋了所有,目前我認為靜態內部類和列舉這兩個單例設計模式不錯!
轉載於:https://my.oschina.net/mdxlcj/blog/1786451