源代碼:https://gitee.com/AgentXiao/SingletonPattern
一拆魏、核心作用
保證一個(gè)類只有一個(gè)實(shí)例,并且提供一個(gè)訪問該實(shí)例的全局訪問點(diǎn)邀泉。
二买窟、優(yōu)點(diǎn)
單例模式只生成一個(gè)實(shí)例,減少了系統(tǒng)性能開銷
三贾铝、各種版本的單例模式
實(shí)現(xiàn)單例模式的基本規(guī)則:
(1)構(gòu)造器私有化隙轻,不讓外部創(chuàng)建對象
(2)提供外部調(diào)用的對象的方法
1、餓漢式
餓漢式的特點(diǎn)是“餓”垢揩,即類加載時(shí)便創(chuàng)建對象玖绿。
//餓漢式:類初始化時(shí)立即加載
private static SingletonDome01 singleton = new SingletonDome01();
//構(gòu)造器私有化,外部用戶不能夠創(chuàng)建對象
private SingletonDome01(){
}
//提供訪問方法叁巨,每次訪問返回的都是同一個(gè)對象
public static SingletonDome01 getSingleton(){
return singleton;
}
特點(diǎn):
線程安全的:虛擬機(jī)保證只會(huì)裝載一次該類斑匪,肯定不會(huì)發(fā)生并發(fā)訪問的問題。因锋勺。
問題:
可能造成資源浪費(fèi):如果只是加載本類蚀瘸,而不是要調(diào)用getInstance(),甚至永遠(yuǎn)沒有調(diào)用庶橱,則會(huì)造成資源浪費(fèi)贮勃!
2、懶漢式
懶漢式的特點(diǎn)是“懶”苏章,即類加載的時(shí)候不創(chuàng)建對象寂嘉,在第一次調(diào)用訪問方法時(shí)創(chuàng)建,再次調(diào)用時(shí)直接調(diào)用枫绅。
//懶漢式:類初始化時(shí)不立即加載
private static SingletonDome02 singleton;
//構(gòu)造器私有化泉孩,外部用戶不能夠創(chuàng)建對象
private SingletonDome02(){
}
//提供訪問方法,第一次訪問時(shí)創(chuàng)建撑瞧,之后直接返回棵譬。但是需要設(shè)置同步
public static synchronized SingletonDome02 getSingleton(){
if(singleton == null){
singleton = new SingletonDome02();
}
return singleton;
}
問題:
每次調(diào)用getInstance()方法都要同步,并發(fā)效率較低预伺。在不加synchronized時(shí)订咸,假設(shè)A、B同時(shí)訪問酬诀,A創(chuàng)建了對象脏嚷,但是由于B也是null,也創(chuàng)建了一個(gè)對象瞒御,導(dǎo)致創(chuàng)建了多個(gè)對象父叙,不符合單例的原則。
3、雙重檢測鎖
因?yàn)轲I漢式會(huì)讓整個(gè)方法進(jìn)行等待趾唱,效率低涌乳。因此將同步內(nèi)容下方到if內(nèi)部,提高了執(zhí)行的效率甜癞。不必每次獲取對象時(shí)都進(jìn)行同步夕晓,只有第一次才同步,創(chuàng)建了以后就沒必要了悠咱。
//懶漢式:類初始化時(shí)不立即加載
private static SingletonDome03 singleton;
//構(gòu)造器私有化蒸辆,外部用戶不能夠創(chuàng)建對象
private SingletonDome03(){
}
//提供訪問方法,第一次訪問時(shí)創(chuàng)建析既,之后直接返回
public static SingletonDome03 getSingleton(){
if (singleton == null) {
SingletonDome03 sc;//第一次訪問時(shí)定義一個(gè)對象
synchronized (SingletonDome03.class) {
sc = singleton;
if (sc == null) {
synchronized (SingletonDome03.class) {
if(sc == null) {
sc = new SingletonDome03();
}
}
singleton = sc;
}
}
}
return singleton;
}
由于編譯器優(yōu)化原因和JVM底層內(nèi)部模型原因躬贡,偶爾會(huì)出問題。不建議使用眼坏。
4拂玻、靜態(tài)內(nèi)部類實(shí)現(xiàn)
//靜態(tài)內(nèi)部類不會(huì)立即加載,只有真正調(diào)用時(shí)才會(huì)加載
private static class SingletonClass{
private static final SingletonDome04 singleton = new SingletonDome04();
}
//構(gòu)造器私有化空骚,外部用戶不能夠創(chuàng)建對象
private SingletonDome04(){
}
//提供訪問方法纺讲,第一次訪問時(shí)創(chuàng)建,之后直接返回囤屹。但是需要設(shè)置同步
public static synchronized SingletonDome04 getSingleton(){
return SingletonClass.singleton;
}
說明:
(1)加載外部類時(shí)不會(huì)直接創(chuàng)建對象熬甚;
(2)只有在調(diào)用訪問方法時(shí)才會(huì)加載靜態(tài)內(nèi)部類去創(chuàng)建對象,同時(shí)加載一次類保證了線程安全肋坚。singleton是static final類型乡括,保證了內(nèi)存中只有這樣一個(gè)實(shí)例存在,而且只能被賦值一次智厌,從而保證了線程安全性诲泌。
(3)兼?zhèn)淞瞬l(fā)高效調(diào)用和延遲加載的優(yōu)勢!
5铣鹏、問題(針對以上四種方式)
(1)反射破解
SingletonDome02 s1 = SingletonDome02.getSingleton();
SingletonDome02 s2 = SingletonDome02.getSingleton();
System.out.println(s1);
System.out.println(s2);
System.out.println();
Class<SingletonDome02> clazz = (Class<SingletonDome02>) Class.forName("pri.xiaowd.singleton.SingletonDome02");
Constructor<SingletonDome02> c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);//忽略權(quán)限
SingletonDome02 s3 = c.newInstance();//構(gòu)造對象
SingletonDome02 s4 = c.newInstance();//構(gòu)造對象
System.out.println(s3);
System.out.println(s4);
由此可見敷扫,利用反射破解了單例模式。
通過在私有化的構(gòu)造方法中拋異常的方法可以解決诚卸。如果對象已經(jīng)創(chuàng)建了葵第,則報(bào)錯(cuò)。
private SingletonDome02(){
if(singleton != null){
throw new RuntimeException();
}
}
(2)反序列化破解
前提條件是SingletonDome02類實(shí)現(xiàn)了Serializable接口合溺。
//通過反序列化破解單例
FileOutputStream fos = new FileOutputStream("d:/a.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s1);
oos.close();
fos.close();
ObjectInputStream ios = new ObjectInputStream(new FileInputStream("d:/a.txt"));
SingletonDome02 s3 = (SingletonDome02) ios.readObject();
System.out.println(s3);
解決辦法:在SingletonDome02類中定義一個(gè)方法.反序列化時(shí)會(huì)自動(dòng)調(diào)用卒密。
//反序列化時(shí)如果定義了readResolve則直接返回此對象,不需要再創(chuàng)建
private Object readResolve(){
return singleton;
}
6棠赛、枚舉實(shí)現(xiàn)
//這個(gè)枚舉元素本身就是單例
SINGLETON;
//自己想要的操作
public static void getSingleton() {
}
優(yōu)點(diǎn):
– 實(shí)現(xiàn)簡單哮奇。
– 枚舉本身就是單例模式膛腐。由JVM從根本上提供保障!避免通過反射和反序列化的漏洞鼎俘。
缺點(diǎn):
– 無延遲加載哲身。
System.out.println(SingletonDome05.SINGLETON == SingletonDome05.SINGLETON);
返回true。
7贸伐、比較
常見的五種單例模式實(shí)現(xiàn)方式
– 主要:
? 餓漢式(線程安全律罢,調(diào)用效率高。 但是棍丐,不能延時(shí)加載。)
? 懶漢式(線程安全沧踏,調(diào)用效率不高歌逢。 但是,可以延時(shí)加載翘狱。)
– 其他:
? 雙重檢測鎖式(由于JVM底層內(nèi)部模型原因秘案,偶爾會(huì)出問題。不建議使用)
? 靜態(tài)內(nèi)部類式(線程安全潦匈,調(diào)用效率高阱高。 但是,可以延時(shí)加載)
? 枚舉式(線程安全茬缩,調(diào)用效率高赤惊,不能延時(shí)加載。并且可以天然的防止反射和反序列化漏洞凰锡!)
如何選用?
1未舟、單例對象占用資源 少,不需要延時(shí)加載:枚舉式 好于 餓漢式
2掂为、單例對象 占用 資源 大裕膀,需要延時(shí)加載:靜態(tài)內(nèi)部類式 好于 懶漢式
四、各種方式的效率(相對)
long start = System.currentTimeMillis();
int ThreadNum = 10;
final CountDownLatch countDownLatch = new CountDownLatch(ThreadNum);
for(int i=0;i<ThreadNum;i++){
new Thread(new Runnable(){
@Override
public void run() {
for(int i=0;i<1000000;i++){
//Object o = SingletonDome04.getSingleton();
Object o = SingletonDome05.SINGLETON;
}
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();//main線程阻塞勇哗,直到計(jì)數(shù)器變?yōu)?昼扛,才會(huì)繼續(xù)往下執(zhí)行
long end = System.currentTimeMillis();
System.out.println("耗時(shí):"+(end - start));
餓漢式SingletonDome01 : 86
懶漢式SingletonDome02 : 374
雙重檢測鎖SingletonDome03 : 143
靜態(tài)內(nèi)部類SingletonDome03 : 474
枚舉SingletonDome03 : 24