【原標(biāo)題:java單例模式并不難 Java設(shè)計(jì)模式系列演示】為什么單例
1、在內(nèi)存中只有一個(gè)對(duì)象,節(jié)省內(nèi)存空間。避免頻繁的創(chuàng)建銷毀對(duì)象,可以提高性能。避免對(duì)共享資源的多重占用。可以全局訪問。
2、確保一個(gè)類只有一個(gè)實(shí)例,自行實(shí)例化并向系統(tǒng)提供這個(gè)實(shí)例
單例需要注意的問題
1、線程安全問題
2、資源使用問題
實(shí)際上本文就是在討論這兩個(gè)問題
1、餓漢式
package com;public class Singleton { private static Singleton instance = new Singleton() ; private Singleton(){ } public static Singleton getInstance() { return instance ; } }
優(yōu)點(diǎn):
在未調(diào)用getInstance() 之前,實(shí)例就已經(jīng)創(chuàng)建了,天生線程安全
缺點(diǎn):
如果一直沒有調(diào)用getInstance() , 但是已經(jīng)創(chuàng)建了實(shí)例,造成了資源浪費(fèi)。
2、懶漢式
package com;public class Person { private static Person person ; private Person(){ } public static Person get(){ if ( person == null ) { person = new Person() ; } return person ; }}
優(yōu)點(diǎn):
get() 方法被調(diào)用的時(shí)候,才創(chuàng)建實(shí)例,節(jié)省資源。
缺點(diǎn):
線程不安全。
這種模式,可以做到單例模式,但是只是在單線程中是單例的,如果在多線程中操作,可能出現(xiàn)多個(gè)實(shí)例。
測(cè)試:啟動(dòng)20個(gè)線程,然后在線程中打印 Person 實(shí)例的內(nèi)存地址
package com;public class A1 { public static void main(String[] args) { for ( int i = 0 ; i < 20 ; i ++ ) { new Thread( new Runnable() { @Override public void run() { System.out.println( Person.get().hashCode() ); } }).start(); ; } }}
結(jié)果:可以看到出現(xiàn)了兩個(gè) Person 實(shí)例,效果圖如下:
創(chuàng)建兩個(gè)實(shí)例原因分析:
線程A希望使用 Person ,調(diào)用 get()方法。因?yàn)槭堑谝淮握{(diào)用,A 就發(fā)現(xiàn) person 是 null 的,于是它開始創(chuàng)建實(shí)例,就在這個(gè)時(shí)候,CPU 發(fā)生時(shí)間片切換,線程B開始執(zhí)行,它要使用 Person ,調(diào)用get()方法,同樣檢測(cè)到 person 是null ——注意,這是在 A 檢測(cè)完之后切換的,也就是說 A 并沒有來得及創(chuàng)建對(duì)象——因此 B 開始創(chuàng)建。B創(chuàng)建完成后,切換到A繼續(xù)執(zhí)行,因?yàn)樗呀?jīng)檢測(cè)完了,所以A不會(huì)再檢測(cè)一遍,它會(huì)直接創(chuàng)建對(duì)象。這樣,線程 A 和 B 各自擁有一個(gè) Person 的對(duì)象——單例失敗!
總結(jié):
1、可以實(shí)現(xiàn)單線程單例
2、多線單例無法保證
改進(jìn):
1、加鎖
3、 用synchronized 加鎖同步
package com;public class Person { private static Person person ; private Person(){ } public synchronized static Person get(){ if ( person == null ) { person = new Person() ; } return person ; }}
經(jīng)過測(cè)試,已經(jīng)可以滿足多線程的安全問題了,synchronized 修飾的同步塊可是要比一般的代碼段慢上幾倍的!如果存在很多次 get() 的調(diào)用,那性能問題就不得不考慮了!
優(yōu)點(diǎn):
1、滿足單線程的單例
2、滿足多線程的單例
缺點(diǎn):
1、性能差
4、改進(jìn)性能 雙重校驗(yàn)
package com;public class Person { private static Person person ; private Person(){ } public synchronized static Person get(){ if ( person == null ) { synchronized ( Person.class ){ if (person == null) { person = new Person(); } } } return person ; }}
首先判斷 person 是不是為 null ,如果為 null ,加鎖初始化;如果不為 null ,直接返回 person 。整個(gè)設(shè)計(jì),進(jìn)行了雙重校驗(yàn)。
優(yōu)點(diǎn):
1、滿足單線程單例
2、滿足多線程單例
3、性能問題得以優(yōu)化
缺點(diǎn):
1、第一次加載時(shí)反應(yīng)不快,由于java內(nèi)存模型一些原因偶爾失敗
5、volatile 關(guān)鍵字,解決雙重校驗(yàn)帶來的弊端
package com;public class Person { private static volatile Person person = null ; private Person(){ } public static Person getInstance(){ if ( person == null ) { synchronized ( Person.class ){ if ( person == null ) { person = new Person() ; } } } return person ; }}
假設(shè)沒有關(guān)鍵字 volatile 的情況下,兩個(gè)線程 A、B,都是第一次調(diào)用該單例方法,線程A先執(zhí)行 person = new Person(),該構(gòu)造方法是一個(gè)非原子操作,編譯后生成多條字節(jié)碼指令,由于JAVA的指令重排序,可能會(huì)先執(zhí)行 person 的賦值操作,該操作實(shí)際只是在內(nèi)存中開辟一片存儲(chǔ)對(duì)象的區(qū)域后直接返回內(nèi)存的引用,之后 person 便不為空了,但是實(shí)際的初始化操作卻還沒有執(zhí)行,如果就在此時(shí)線程 B 進(jìn)入,就會(huì)看到一個(gè)不為空的但是不完整 (沒有完成初始化)的 Person 對(duì)象,所以需要加入 volatile 關(guān)鍵字,禁止指令重排序優(yōu)化,從而安全的實(shí)現(xiàn)單例。
補(bǔ)充:看了圖片加載框架 Glide (3.7.0版) 源碼,發(fā)現(xiàn) glide 也是使用 volatile 關(guān)鍵字的雙重校驗(yàn)實(shí)現(xiàn)的單例,可見這種方法是值得信賴的。
6、靜態(tài)內(nèi)部類
package com;public class Person { private Person(){ } private static class PersonHolder{ /** * 靜態(tài)初始化器,由JVM來保證線程安全 */ private static Person instance = new Person(); } public static Person getInstance() { return PersonHolder.instance; }}
優(yōu)點(diǎn):
1、資源利用率高,不執(zhí)行g(shù)etInstance()不被實(shí)例,可以執(zhí)行該類其他靜態(tài)方法
7、枚舉類實(shí)現(xiàn)單例
package com;public enum Singleton { INSTANCE ; public void show(){ // Do you need to do things }}
使用
獲取實(shí)例對(duì)象:Singleton.INSTANCE調(diào)用其他方法:Singleton.INSTANCE.show();
總結(jié):
1、上面的7中方法,都實(shí)現(xiàn)了某種程度的單例,各有利弊,根據(jù)使用的場景不同,需要滿足的特性不同,選取合適的單例方法才是正道。
2、對(duì)線程要求嚴(yán)格,對(duì)資源要求不嚴(yán)格的推薦使用:1 餓漢式
3、對(duì)線程要求不嚴(yán)格,對(duì)資源要求嚴(yán)格的推薦使用:2 懶漢式
4、對(duì)線程要求稍微嚴(yán)格,對(duì)資源要求嚴(yán)格的推薦使用:4 雙重加鎖
5、同時(shí)對(duì)線程、資源要求非常嚴(yán)格的推薦使用:5 、 6