學習資料:
- Java 的 23 種設(shè)計模式全解析
- 《Java程序性能優(yōu)化》
《Java程序性能優(yōu)化》秩冈,這本書蠻不錯的,豆瓣評分挺高7.9
。本篇就是第2章第一章節(jié)的讀書筆記
最近項目中經(jīng)常用到單例模式咖祭,雖然能手寫出來,但了解的東西并不多蔫骂,并不確定為何要這樣寫么翰,以及這樣寫的好處,書上正好看到辽旋,就學習了解
1. 單例模式
總體來說設(shè)計模式分為三大類:<p>
創(chuàng)建型模式浩嫌,共五種:工廠方法模式、抽象工廠模式补胚、單例模式码耐、建造者模式、原型模式溶其。<p>
結(jié)構(gòu)型模式骚腥,共七種:適配器模式、裝飾器模式瓶逃、代理模式束铭、外觀模式、橋接模式厢绝、組合模式契沫、享元模式。<p>
行為型模式昔汉,共十一種:策略模式懈万、模板方法模式、觀察者模式、迭代子模式会通、責任鏈模式口予、命令模式、備忘錄模式涕侈、狀態(tài)模式苹威、訪問者模式、中介者模式驾凶、解釋器模式
單例模式
是一種對象創(chuàng)建模式, 可以用來確保一個類只產(chǎn)生一個對象的具體實例掷酗。
適用場景:
- 系統(tǒng)的關(guān)鍵組件
- 被頻繁使用的對象
好處:
- 對于頻繁使用的對象调违,可以省略創(chuàng)建對象所花費的時間。尤其是一些重量級的對象泻轰,可以省下一些系統(tǒng)開銷
- 由于
new
次數(shù)減少技肩,對系統(tǒng)內(nèi)存的使用頻率也會降低,從而減輕GC
壓力浮声,縮短GC
停頓時間
1.1 簡單實例
單例模式的核心在于通過一個方法返回唯一的對象實例
代碼
public class Singleton {
private Singleton() {
System.out.println("Singleton is created");// 創(chuàng)建單例的過程可能會比較慢
}
private static Singleton instance = new Singleton();
public Singleton getInstance() {
return instance;
}
}
代碼的核心在于:
一個private
訪問級的構(gòu)造函數(shù)虚婿;private static
的instance
成員變量;public static
的getInstance()
方法
這種寫法簡單可靠泳挥,缺點就是 無法對instance
實例進行延遲加載
1.1.1 缺點
當采用簡單實例形式時然痊,若此時的單例類還扮演著其他的角色,由于instance
成員是static
的屉符,當JVM
加載單例類時剧浸,單例對象就會被創(chuàng)建,導致在任何時候想要試用這個單例類矗钟,都會進行初始化這個單例變量
public class Singleton {
private Singleton() {
System.out.println("Singleton is created");// 創(chuàng)建單例的過程可能會比較慢
}
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
當使用Singleton.createString()
方法時,總會先創(chuàng)建出一個Singleton
對象實例吨艇,這就是所謂的不滿足實例延遲加載
1.2 實例延遲加載
代碼:
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
,確保啟動時沒有額外的負載
其次东涡,在getInstance()
方法中冯吓,判斷當前的單例是否已存在,若存在則返回软啼;不存在則建立單例實例
需要注意的是在getInstance()
前加了synchronized
桑谍,不加的話當在多線程時,線程1正在創(chuàng)建單例過程中祸挪,完成賦值前锣披,線程2可能判斷instance
為null
,線程2就會啟動創(chuàng)建單例的語句,導致多個實例被創(chuàng)建
這種延遲加載的缺點在于性能消耗
1.2.1 驗證synchronized作用
將getInstance()
前的synchronized
去掉雹仿,修改代碼
public static LazySingleton getInstance() {
if (null == instance) {
try {
TimeUnit.MILLISECONDS.sleep(10);//模擬耗時操作增热,線程sleep
instance = new LazySingleton();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return instance;
}
測試:
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
同步后可以避免實例被多次創(chuàng)建
在其他的博客看到峻仇,還用volatile
來修飾instance
private volatile static LazySingleton instance = null
暫時對volatile
不了解,以后再學習
1.2.2 延遲加載帶來的性能消耗
使用了synchronized
的LazySingleton
在多線程下會有更多的性能消耗
測試:
@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));
}
當使用Singleton.getInstance()
平均耗時25 milliseconds
使用LazySingleton.getInstance()
平均耗時200 milliseconds
實際耗時需要根據(jù)自己電腦來確定
1.3 改進型延遲加載
上面的LazySingleton
雖然支持了延遲加載并防止了多次創(chuàng)建實例邑商,卻導致了額外的性能消耗摄咆,推薦兩種改進形式:雙檢查鎖與內(nèi)部類
兩種方式在使用時根據(jù)場景選擇,若需要通過構(gòu)造方法傳遞參數(shù)人断,則選擇雙檢查鎖形式吭从;若不需要則都可以
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),先對instance
進行判斷恶迈,若不為null
則不需要進行同步鎖操作涩金,從而避免了同步鎖在多線程下帶來的性能消耗,而且由于有同步鎖暇仲,也能避免多次創(chuàng)建實例
經(jīng)測試步做,這種方式使用1.2.2
的方式測試,平均耗時15 milliseconds
經(jīng)1.2.1
的方式測試奈附,也沒有出現(xiàn)多處創(chuàng)建實例
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;
}
}
當InclassSingletong
被加載時全度,內(nèi)部類并不會加載,而當getInstance()
調(diào)用時斥滤,才會初始化instance
由于實例的建立是在類加載時完成讼载,天生多線程友好,所有不需要同步synchronized
關(guān)鍵字
這種兩種形式基本可以保證不會創(chuàng)建多個實例中跌,只有極特殊的情景咨堤,例如通過反射強行調(diào)用單例類的私有構(gòu)造函數(shù)會造成多次創(chuàng)建實例
1.4 單例模式的序列化
public class Singleton implements Serializable {
private Singleton() {
System.out.println("Singleton is created");// 創(chuàng)建單例的過程可能會比較慢
}
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;
}
}
進行單元測試:
@Test
public void test() throws Exception {
Singleton s1 = null;
Singleton s0 = Singleton.getInstance();
String fileName ="Singleton.txt";
//將實例串行化到文件
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();
//進行比較
assertEquals(s0,s1);
}
若將readResolve()
方法去掉,則會報異常漩符,說s1
和s0
指向不同的實例
需要序列化單例類的場景很少見一喘,這里了解下
2. 最后
學習了解常見的設(shè)計模式,以后閱讀一些庫的源碼或者自己寫代碼時也有些幫助
共勉 :)