學(xué)習(xí)資料:
- Java 的 23 種設(shè)計(jì)模式全解析
- 《Java程序性能優(yōu)化》
《Java程序性能優(yōu)化》,這本書蠻不錯(cuò)的,豆瓣評(píng)分挺高7.9。本篇就是第2章第一章節(jié)的讀書筆記
最近項(xiàng)目中經(jīng)常用到單例模式,雖然能手寫出來,但了解的東西并不多,并不確定為何要這樣寫,以及這樣寫的好處,書上正好看到,就學(xué)習(xí)了解
1. 單例模式
總體來說設(shè)計(jì)模式分為三大類:<p>
創(chuàng)建型模式,共五種:工廠方法模式、抽象工廠模式、單例模式、建造者模式、原型模式。<p>
結(jié)構(gòu)型模式,共七種:適配器模式、裝飾器模式、代理模式、外觀模式、橋接模式、組合模式、享元模式。<p>
行為型模式,共十一種:策略模式、模板方法模式、觀察者模式、迭代子模式、責(zé)任鏈模式、命令模式、備忘錄模式、狀態(tài)模式、訪問者模式、中介者模式、解釋器模式
單例模式是一種對(duì)象創(chuàng)建模式, 可以用來確保一個(gè)類只產(chǎn)生一個(gè)對(duì)象的具體實(shí)例。
適用場(chǎng)景:
- 系統(tǒng)的關(guān)鍵組件
- 被頻繁使用的對(duì)象
好處:
- 對(duì)于頻繁使用的對(duì)象,可以省略創(chuàng)建對(duì)象所花費(fèi)的時(shí)間。尤其是一些重量級(jí)的對(duì)象,可以省下一些系統(tǒng)開銷
- 由于
new次數(shù)減少,對(duì)系統(tǒng)內(nèi)存的使用頻率也會(huì)降低,從而減輕GC壓力,縮短GC停頓時(shí)間
1.1 簡(jiǎn)單實(shí)例
單例模式的核心在于通過一個(gè)方法返回唯一的對(duì)象實(shí)例
代碼
public class Singleton {
private Singleton() {
System.out.println("Singleton is created");// 創(chuàng)建單例的過程可能會(huì)比較慢
}
private static Singleton instance = new Singleton();
public Singleton getInstance() {
return instance;
}
}
代碼的核心在于:
一個(gè)private訪問級(jí)的構(gòu)造函數(shù);private static的instance成員變量;public static的getInstance()方法
這種寫法簡(jiǎn)單可靠,缺點(diǎn)就是 無法對(duì)instance實(shí)例進(jìn)行延遲加載
1.1.1 缺點(diǎn)
當(dāng)采用簡(jiǎn)單實(shí)例形式時(shí),若此時(shí)的單例類還扮演著其他的角色,由于instance成員是static的,當(dāng)JVM加載單例類時(shí),單例對(duì)象就會(huì)被創(chuàng)建,導(dǎo)致在任何時(shí)候想要試用這個(gè)單例類,都會(huì)進(jìn)行初始化這個(gè)單例變量
public class Singleton {
private Singleton() {
System.out.println("Singleton is created");// 創(chuàng)建單例的過程可能會(huì)比較慢
}
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
public static void createString(){ //模擬單例類扮演其他角色
System.out.println("CreateString in Singleton");
}
}
使用Singleton.createString(),輸出結(jié)果:
Singleton is created
CreateString in Singleton
當(dāng)使用Singleton.createString()方法時(shí),總會(huì)先創(chuàng)建出一個(gè)Singleton對(duì)象實(shí)例,這就是所謂的不滿足實(shí)例延遲加載
1.2 實(shí)例延遲加載
代碼:
public class LazySingleton {
private LazySingleton() {
System.out.println("LazySingleton is created");
}
private static LazySingleton instance = null;
public static synchronized LazySingleton getInstance() {
if (null == instance) {
instance = new LazySingleton();
}
return instance;
}
}
首先,將靜態(tài)成員變量的初始值賦予null,確保啟動(dòng)時(shí)沒有額外的負(fù)載
其次,在getInstance()方法中,判斷當(dāng)前的單例是否已存在,若存在則返回;不存在則建立單例實(shí)例
需要注意的是在getInstance()前加了synchronized,不加的話當(dāng)在多線程時(shí),線程1正在創(chuàng)建單例過程中,完成賦值前,線程2可能判斷instance為null,線程2就會(huì)啟動(dòng)創(chuàng)建單例的語句,導(dǎo)致多個(gè)實(shí)例被創(chuàng)建
這種延遲加載的缺點(diǎn)在于性能消耗
1.2.1 驗(yàn)證synchronized作用
將getInstance()前的synchronized去掉,修改代碼
public static LazySingleton getInstance() {
if (null == instance) {
try {
TimeUnit.MILLISECONDS.sleep(10);//模擬耗時(shí)操作,線程sleep
instance = new LazySingleton();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return instance;
}
測(cè)試:
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
LazySingleton instance = LazySingleton.getInstance();
System.out.println(instance);
}
}
public static void main(String[] args) {
MyThread[] myThreads = new MyThread[5];
for (int i = 0; i < 5; i++) {
myThreads[i] = new MyThread();
}
beginTime = System.currentTimeMillis();
for (MyThread myThread : myThreads) {
myThread.start();
}
}
}
部分輸出結(jié)果:
LazySingleton is created
l.single.LazySingleton@71053f05
l.single.LazySingleton@71053f05
l.single.LazySingleton@71053f05
LazySingleton is created
l.single.LazySingleton@6c749686
l.single.LazySingleton@6c749686
l.single.LazySingleton@6c749686
LazySingleton is created
l.single.LazySingleton@3a9ea3d2
l.single.LazySingleton@3a9ea3d2
l.single.LazySingleton@3a9ea3d2
由結(jié)果看出,LazySingleton被多次創(chuàng)建,說明synchronized同步后可以避免實(shí)例被多次創(chuàng)建
在其他的博客看到,還用volatile來修飾instance
private volatile static LazySingleton instance = null
暫時(shí)對(duì)volatile不了解,以后再學(xué)習(xí)
1.2.2 延遲加載帶來的性能消耗
使用了synchronized的LazySingleton在多線程下會(huì)有更多的性能消耗
測(cè)試:
@Override
public void run() {
long beginTime = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
// Singleton.getInstance();
LazySingleton.getInstance();
}
System.out.println("spend: " + (System.currentTimeMillis() - beginTime));
}
當(dāng)使用Singleton.getInstance()平均耗時(shí)25 milliseconds
使用LazySingleton.getInstance()平均耗時(shí)200 milliseconds
實(shí)際耗時(shí)需要根據(jù)自己電腦來確定
1.3 改進(jìn)型延遲加載
上面的LazySingleton雖然支持了延遲加載并防止了多次創(chuàng)建實(shí)例,卻導(dǎo)致了額外的性能消耗,推薦兩種改進(jìn)形式:雙檢查鎖與內(nèi)部類
兩種方式在使用時(shí)根據(jù)場(chǎng)景選擇,若需要通過構(gòu)造方法傳遞參數(shù),則選擇雙檢查鎖形式;若不需要?jiǎng)t都可以
1.3.1 雙檢查鎖形式
public class LazySingleton {
private LazySingleton() {
System.out.println("LazySingleton is created");
}
private static volatile LazySingleton instance = null;
public static LazySingleton getInstance() {
if (null == instance) {
synchronized (LazySingleton.class) {
if (null == instance) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
在getInstance()方法內(nèi),先對(duì)instance進(jìn)行判斷,若不為null則不需要進(jìn)行同步鎖操作,從而避免了同步鎖在多線程下帶來的性能消耗,而且由于有同步鎖,也能避免多次創(chuàng)建實(shí)例
經(jīng)測(cè)試,這種方式使用1.2.2的方式測(cè)試,平均耗時(shí)15 milliseconds
經(jīng)1.2.1的方式測(cè)試,也沒有出現(xiàn)多處創(chuàng)建實(shí)例
1.3.1 內(nèi)部類方式
代碼:
public class InclassSingleton {
private InclassSingleton() {
System.out.println("InclassSingleton is created");
}
private static class SingletonHolder {
private static InclassSingleton instance = new InclassSingleton();
}
public static InclassSingleton getInstance() {
return SingletonHolder.instance;
}
}
當(dāng)InclassSingletong被加載時(shí),內(nèi)部類并不會(huì)加載,而當(dāng)getInstance()調(diào)用時(shí),才會(huì)初始化instance
由于實(shí)例的建立是在類加載時(shí)完成,天生多線程友好,所有不需要同步synchronized關(guān)鍵字
這種兩種形式基本可以保證不會(huì)創(chuàng)建多個(gè)實(shí)例,只有極特殊的情景,例如通過反射強(qiáng)行調(diào)用單例類的私有構(gòu)造函數(shù)會(huì)造成多次創(chuàng)建實(shí)例
1.4 單例模式的序列化
public class Singleton implements Serializable {
private Singleton() {
System.out.println("Singleton is created");// 創(chuàng)建單例的過程可能會(huì)比較慢
}
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
public static void createString() { //模擬單例類扮演其他角色
System.out.println("CreateString in Singleton");
}
protected Object readResolve() {
return instance;
}
}
進(jìn)行單元測(cè)試:
@Test
public void test() throws Exception {
Singleton s1 = null;
Singleton s0 = Singleton.getInstance();
String fileName ="Singleton.txt";
//將實(shí)例串行化到文件
FileOutputStream fos = new FileOutputStream(fileName);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s0);
oos.flush();
oos.close();
//從文件讀出所有的單例類
FileInputStream fis = new FileInputStream(fileName);
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (Singleton) ois.readObject();
//進(jìn)行比較
assertEquals(s0,s1);
}
若將readResolve()方法去掉,則會(huì)報(bào)異常,說s1和s0指向不同的實(shí)例
需要序列化單例類的場(chǎng)景很少見,這里了解下
2. 最后
學(xué)習(xí)了解常見的設(shè)計(jì)模式,以后閱讀一些庫的源碼或者自己寫代碼時(shí)也有些幫助
共勉 :)