單例模式
We all want to as a 有追求的程序猿,這不將早已塵封的《劍指Offer》給拿出來(lái)重新拜讀一下缀台。該書(shū)中面試題2就是單例模式,可見(jiàn)其重要性(Maybe just for Interview).同時(shí)也為了這次更加系統(tǒng)地閱讀&總結(jié)哮奇。
PS :之前也是隨便翻了幾下就束之高閣啦膛腐。
前言
我們?cè)诿嬖囍薪?jīng)常遇到單例模式(However you as 面試者or面試官),關(guān)于單例模式的優(yōu)秀文章鼎俘,網(wǎng)上也是俯首皆是哲身。本文Just for me to 心得體會(huì)or筆記。如果有幸能幫助到其他人贸伐,那我將會(huì)更加高興...
1-餓漢式單例
這個(gè)寫(xiě)法就類(lèi)似于解決了單例模式中的“溫飽問(wèn)題”
public class Singleton {
// JVM加載該類(lèi)時(shí)勘天,單例對(duì)象就會(huì)自動(dòng)創(chuàng)建
private static Singleton instance = new Singleton();
private Singleton() {
System.out.println("構(gòu)造函數(shù)Running....");
}
public static Singleton getInstance(){
return instance;
}
/**
* 證明了沒(méi)有對(duì)instance做延時(shí)加載...
*/
public static void doSomething(){
System.out.println("Just for fun...");
}
public static void main(String[] args) {
/*
* 這里沒(méi)有用到該實(shí)例,But 照樣給我創(chuàng)建了其實(shí)例
*/
Singleton.doSomething();
}
}
"Talk is cheap,show me the code"
JVM類(lèi)的加載原理
- JVM在執(zhí)行類(lèi)的初始化期間捉邢,JVM會(huì)獲得一把鎖脯丝,該鎖可以同步多個(gè)線(xiàn)程對(duì)同一個(gè)類(lèi)的初始化
There is no doubt that 該種方案實(shí)現(xiàn)簡(jiǎn)單,且線(xiàn)程安全伏伐。但是其沒(méi)有對(duì)instance做相應(yīng)的延時(shí)加載宠进,只要初始化該類(lèi)就創(chuàng)建其實(shí)例,這樣就造成了資源浪費(fèi)藐翎。
2-懶漢式單例
“懶漢式”---顧名思義材蹬,就是你要我才給,按需分配
/**
* “懶漢式”吝镣,用到實(shí)例才加載堤器,否則不加載
*
* 缺點(diǎn):線(xiàn)程不安全...
*/
public class Singleton {
private static Singleton instance = null;
private Singleton() {
System.out.println("構(gòu)造函數(shù)Running...");
}
public static Singleton getInstance(){
/**
* 避免重復(fù)創(chuàng)建...
*/
if (instance == null) {
instance = new Singleton();
}
return instance;
}
/**
* HashCode相等說(shuō)明是同一個(gè)實(shí)例
* @return
*/
@Override
public int hashCode() {
return super.hashCode();
}
public static void main(String[] args) {
/**
* 模擬多線(xiàn)程環(huán)境,會(huì)發(fā)現(xiàn)不是同一個(gè)實(shí)例...
*/
for (int i = 0; i < 5; i++) {
new Thread(() -> System.out.println(getInstance().hashCode())).start();
}
}
}
Code 地址:該編輯器在多線(xiàn)程情況下測(cè)試單例不好使末贾,有興趣可以復(fù)制到本地去運(yùn)行測(cè)試
(PS:原圖片鏈接)
線(xiàn)程不安全闸溃,那我們只能去使用同步機(jī)制來(lái)保證線(xiàn)程安全
3-同步鎖的懶漢式
/**
*
* 保證線(xiàn)程安全的“餓漢式”單例
*
* 即:加入synchronized 同步關(guān)鍵字...
*
* 下面的格式將會(huì)造成:每次來(lái)調(diào)用getInstance()都要進(jìn)行線(xiàn)程同步(即調(diào)用synchronized鎖)
*
* 而實(shí)際上, 只需要在第一次調(diào)用的時(shí)候才需要進(jìn)行同步,只要單例存在圈暗,就沒(méi)必要進(jìn)行同步啦...
*
*/
public class Singleton {
private static Singleton instance = null;
private Singleton() {
System.out.println("構(gòu)造函數(shù)running...");
}
public static synchronized Singleton getInstance(){
if (instance == null) {
instance = new Singleton();
}
return instance;
}
/**
* 也可以寫(xiě)成這種格式
* */
// public static Singleton getInstance(){
// synchronized (Singleton.class){
// if (instance != null) {
// instance = new Singleton();
// }
// }
// return instance;
// }
@Override
public int hashCode() {
return super.hashCode();
}
public static void main(String[] args) {
for (int i = 0; i <5 ; i++) {
new Thread(() -> System.out.println(Singleton.getInstance().hashCode())).start();
}
}
}
代碼中的注釋已經(jīng)很清楚了掂为,這里就閑話(huà)不扯...
4-雙重校驗(yàn)鎖(Double-Check)單例
這個(gè)是重點(diǎn)...
/**
* "Double-Check"
* "雙重校驗(yàn)鎖"的單例...
*
*
* 其實(shí)就是在"同步鎖"的基礎(chǔ)上裕膀,外層 + if 判斷...
*
* 作用:若單例存在员串,就不需要進(jìn)行同步加鎖操作synchronized。直接返回實(shí)例昼扛。從而提高程序性能...
*
*
* PS: 到這里還沒(méi)有完工寸齐,主要原因在于 instance = new Singleton2();
* 這并非是個(gè)原子操作,該句事實(shí)上在JVM中大概有三個(gè)過(guò)程:
* 1. 給instance 分配內(nèi)存;
* 2. 調(diào)用Singleton2的構(gòu)造函數(shù)來(lái)初始化成員變量抄谐,生成實(shí)例;
* 3. 將singleton對(duì)象指向分配的內(nèi)存空間(此時(shí)渺鹦,instance 才是非null的)
*
*
* 但是在JVM的即時(shí)編譯器中存在指令重排的優(yōu)化,so 上述的2蛹含,3順序不能保證毅厚。
* 假如執(zhí)行序列為1-3-2.,當(dāng)3執(zhí)行完畢浦箱,而2未執(zhí)行之前吸耿,被其他線(xiàn)程搶占了,此時(shí)instance已經(jīng)是非null(但是沒(méi)有初始化)
* 線(xiàn)程直接返回了instance酷窥,然后使用就報(bào)錯(cuò)...
*
* 所以需要在instance聲明為volatile 就可以啦...
*
*
* volatile關(guān)鍵字的兩個(gè)功能:
* 1. 這個(gè)變量不會(huì)在多個(gè)線(xiàn)程中存在副本咽安,直接從內(nèi)存中讀取...
* 2. 禁止指令重排序優(yōu)化。
*
* 但是這個(gè)只在Java 1.5之后有效蓬推,因?yàn)橹暗腏ava內(nèi)存模型有缺陷...
*
* 總結(jié):
* 該單例版本有點(diǎn)復(fù)雜...
*
*/
public class Singleton {
/**
* 注意這里...volatile關(guān)鍵字
*/
private volatile static Singleton instance = null;
private Singleton() {
System.out.println("running...");
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
@Override
public int hashCode() {
return super.hashCode();
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> System.out.println(getInstance().hashCode())).start();
}
}
}
雙重校驗(yàn)鎖是滿(mǎn)足要求妆棒,But 有局限性... 別著急,還有更好的
5- 靜態(tài)內(nèi)部類(lèi)實(shí)現(xiàn)單例
package offer;
/**
* @author king
* @date 2018/5/6
* <p>
* 靜態(tài)內(nèi)部類(lèi)實(shí)現(xiàn)單例
*/
public class Singleton {
/**
* 創(chuàng)建靜態(tài)內(nèi)部類(lèi)
*/
private static class InnerSingleton {
/**
* 在靜態(tài)內(nèi)部類(lèi)里創(chuàng)建單例
*/
private static Singleton instance = new Singleton();
}
/**
* 私有化構(gòu)造函數(shù)
*/
private Singleton() {
System.out.println("構(gòu)造函數(shù)Running...");
}
public static Singleton getInstance() {
return InnerSingleton.instance;
}
@Override
public int hashCode() {
return super.hashCode();
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> System.out.println(getInstance().hashCode())).start();
}
}
}
靜態(tài)內(nèi)部類(lèi)單例Source Code
關(guān)于單例的小插曲
曾經(jīng)在面試過(guò)程中沸伏,問(wèn)到面試者餓漢式&懶漢式的區(qū)別:
曾有面試者告訴我糕珊,餓漢式每次加載類(lèi)都會(huì) new 一次對(duì)象,將造成資源浪費(fèi)毅糟。我當(dāng)時(shí)沒(méi)反應(yīng)過(guò)來(lái)红选,只注意到它回答的“資源浪費(fèi)”,后來(lái)我才明白過(guò)來(lái)留特,instance是static的纠脾,只會(huì)初始化一次,何來(lái)多次new 之談蜕青?苟蹈??
- 有一次讓面試者寫(xiě)個(gè)懶漢式單例(先不考慮多線(xiàn)程情況)右核,代碼大意如下:
public class Singleton {
public static Singleton instance = null;
private Singleton(){
}
// 注意...
public static Singleton getInstance(){
instance = new Singleton();
return instance;
}
}
少個(gè)判空慧脱,已非“單例”啊...
總結(jié)
無(wú)論是劍指offer這本書(shū),還是我們面試中高級(jí)崗位時(shí)贺喝,考察點(diǎn)基本都會(huì)設(shè)在雙重校驗(yàn)鎖上菱鸥,畢竟面試造核彈...