文章目錄
1. 單例模式
1.1 核心作用
保證一個類只有一個實例,並且提供一個訪問該實例的全局訪問點
1.2 常見應用場景
- Windows的Task Manager(任務管理器)就是很典型的單例模式
- Windows的Recycle Bin(回收站)也是典型的單例應用,在整個系統運行過程中,回收站一直維護着僅有的一個實例
- 項目中,讀取配置文件的類,一般也只有一個對象,沒有必要每次使用配置文件數據時都new一個對象去讀取
- 網站的計數器一般也採用單例模式實現,否則難以同步
- 應用程序的日誌應用,一般都採用單例模式實現,這一般是由於共享的日誌一直處於打開狀態,因爲只能有一個實例去操作,否則不好追加
- 數據庫連接池的設計一般也是採用單例模式,因爲數據庫連接是一種數據庫資源
- 操作系統的文件系統,也是大的單例模式實現的具體例子,一個操作系統只能有一個文件系統
- Application也是單例的典型應用(Servlet編程中會涉及到)
- 在Spring中,每個Bean默認都是單例的
- 在Servlet編程中,每個Servlet也是單例的
- 在SpringMVC框架/Struts1框架中,控制器對象也是單例
1.3 優點
- 只生成一個實例,減少了系統性能開銷:當一個對象的產生需要比較多的資源時,如讀取配置,產生其他依賴對象時,則可以通過在應用啓動時直接產生一個單例對象,然後永久駐留內存的方式解決。
- 單例模式可以再系統設置全局的訪問點,優化共享資源訪問,例如可以設計一個單例類,負責所有數據表的映射處理。
1.4 常見的五種單例模式實現方法
-
主要
- 餓漢式(線程安全,調用效率高,但是,不能延遲加載)
- 懶漢式(線程安全,調用效率不高,但是,可以延遲加載)
-
其他
- 雙重檢測瑣式(由於JVM底層內部模型原因,偶爾會出問題,不建議使用)
- 靜態內部式(線程安全,調用效率高,但是,可以延遲加載)
- 枚舉單例(線程安全,調用效率高,但是,不能延遲加載)
如何選用:
- 單例對象 佔用 資源少,不需要 延遲加載:枚舉式好於餓漢式
- 單例對象 佔用 資源大,需要 延遲加載:靜態內部類式好於懶漢式
2. 單例模式的5種實現
2.1 餓漢式(單例對象立即加載)
餓漢式單例模式代碼中,static變量會在類裝載時初始化,此時也不會涉及多個線程對象訪問該對象問題
。虛擬機保證只會裝載一次該類,肯定不會發生併發訪問的問題。因此,可以省略synchronized關鍵字。
/**
* 測試餓漢式單例模式
* @author kevin
*
*/
public class SingletonDemo1 {
//類初始化時,立即加載這個對象(沒有延時加載的優勢)。加載類時,天然的是線程安全的!
private static SingletonDemo1 instance = new SingletonDemo1();
private SingletonDemo1() {
}
//方法沒有同步,調用效率高!
public static SingletonDemo1 getInstance() {
return instance;
}
}
測試類:
public class Client1 {
public static void main(String[] args) {
SingletonDemo1 s1 = SingletonDemo1.getInstance();
SingletonDemo1 s2 = SingletonDemo1.getInstance();
System.out.println(s1 == s2);//true
System.out.println(s1.hashCode()==s2.hashCode());//true
}
}
問題:如果
只是加載本類,而不是要調用getInstance(),甚至永遠沒有調用,則會造成資源浪費
。
2.2 懶漢式(單例對象延遲加載)
lazy load 延遲加載,懶加載! 真正用的時候才加載!
/**
* 測試懶漢式單例模式
* @author kevin
*
*/
public class SingletonDemo2 {
//類初始化時,不初始化這個對象(延時加載,真正用的時候再創建)。
private static SingletonDemo2 instance;
private SingletonDemo2(){
}
//方法同步,調用效率低!
public static synchronized SingletonDemo2 getInstance() {
if(instance == null) {
instance = new SingletonDemo2();
}
return instance;
}
}
測試類:
public class Client1 {
public static void main(String[] args) {
SingletonDemo2 s3 = SingletonDemo2.getInstance();
SingletonDemo2 s4 = SingletonDemo2.getInstance();
System.out.println(s3 == s4);//true
}
}
問題:
資源利用率高了,但是,每次調用getInstance()方法都要同步,併發效率較低。
2.3 雙重檢測鎖實現
將同步內容放到if內部,提高了執行的效率,不必每次獲取對象時都進行同步,只有第一次才同步,創建了以後就沒必要了。
/**
* 雙重檢索實現單例模式
* @author kevin
*
*/
public class SingletonDemo3 {
private static SingletonDemo3 instance = null;
private SingletonDemo3() {
}
public static SingletonDemo3 getInstance() {
if(instance == null) {
SingletonDemo3 sc;
synchronized (SingletonDemo3.class) {
sc = instance;
if(sc == null) {
synchronized (SingletonDemo3.class) {
if(sc == null) {
sc = new SingletonDemo3();
}
}
instance = sc;
}
}
}
return instance;
}
}
測試類:
public class Client1 {
public static void main(String[] args) {
SingletonDemo3 s5 = SingletonDemo3.getInstance();
SingletonDemo3 s6 = SingletonDemo3.getInstance();
System.out.println(s5 == s6);//true
}
}
問題:由於編譯器優化原因和JVM底層內部模型原因,偶爾會出問題,不建議使用
2.4 靜態內部類實現方式(單例對象延遲加載)
/**
* 測試靜態內部類實現單例模式
* @author kevin
*
*/
public class SingletonDemo4 {
private static class SingletonClassInstance {
private static final SingletonDemo4 instance = new SingletonDemo4();
}
private SingletonDemo4() {
}
//方法沒有同步,調用效率高
public static SingletonDemo4 getInstance() {
return SingletonClassInstance.instance;
}
}
測試類:
public class Client1 {
public static void main(String[] args) {
SingletonDemo4 s7 = SingletonDemo4.getInstance();
SingletonDemo4 s8 = SingletonDemo4.getInstance();
System.out.println(s7 == s8);//true
}
}
要點:
- 外部類沒有static屬性,則不會像餓漢式那樣立即加載對象。
- 只有真正調用getInstance()纔會加載靜態內部類,加載類時線程是安全的,instance是static final類型,保證了內存中只有一個這樣實例存在,而且只能被賦值一次,從而保證了線程安全性。
- 兼備了併發高效調用和延遲加載的優勢!
2.5 枚舉實現方式
/**
* 測試枚舉實現單例模式
* @author kevin
*
*/
public enum SingletonDemo5 {
//這個枚舉元素 ,代表了Singleton的一個實例
INSTANCE;
//添加自己需要的操作
public void singletonOperation() {
}
}
測試類:
public class Client1 {
public static void main(String[] args) {
SingletonDemo5 s9 = SingletonDemo5.INSTANCE;
SingletonDemo5 s10 = SingletonDemo5.INSTANCE;
System.out.println(s9 == s10);//true
}
}
優點:
- 實現簡單
- 枚舉本身就是單例模式。由jvm從根本上提供保障!避免通過反射和反序列化的漏洞。
缺點:
- 無延遲加載
3. 反射和反序列化破解單例模式
1) 反射可以破解上面幾種單例模式(非枚舉式)的實現方式。
可以在構造方法中手動拋出異常控制
2) 反序列化可以破解上面幾種單例模式(非枚舉式)實現方式。
可以通過定義readResolve()防止獲得不同對象
3.1 反射破解單例模式
定義個懶漢式實現的單例模式:
public class SingletonDemo6 {
//類初始化時,不初始化這個對象(延時加載,真正用的時候再創建)。
private static SingletonDemo6 instance;
private SingletonDemo6(){
}
//方法同步,調用效率低!
public static synchronized SingletonDemo6 getInstance() {
if(instance == null) {
instance = new SingletonDemo6();
}
return instance;
}
}
使用反射破解單例模式測試類:Client2.java:
public class Client2 {
public static void main(String[] args) throws Exception {
SingletonDemo6 s1 = SingletonDemo6.getInstance();
SingletonDemo6 s2 = SingletonDemo6.getInstance();
System.out.println(s1);
System.out.println(s2);
//通過反射的方式直接調用私有的構造器
Class clazz = Class.forName("com.stormkai.singleton.demo.SingletonDemo6");
Constructor<SingletonDemo6> c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
SingletonDemo6 s3 = c.newInstance();
SingletonDemo6 s4 = c.newInstance();
System.out.println(s3);
System.out.println(s4);
}
}
測試結果爲:
com.stormkai.singleton.demo.SingletonDemo6@15db9742
com.stormkai.singleton.demo.SingletonDemo6@15db9742
com.stormkai.singleton.demo.SingletonDemo6@6d06d69c
com.stormkai.singleton.demo.SingletonDemo6@7852e922
3.1.1 防止反射破解單例模式
只需要在單例的私有構造器中添加如下代碼:
if(instance != null) {
throw new RuntimeException();
}
則反射破解單例模式時候就會報java.lang.reflect.InvocationTargetException
.
代碼如下:
public class SingletonDemo6 {
//類初始化時,不初始化這個對象(延時加載,真正用的時候再創建)。
private static SingletonDemo6 instance;
private SingletonDemo6(){
//添加如下代碼,反射破解單例時候會報java.lang.reflect.InvocationTargetException
if(instance != null) {
throw new RuntimeException();
}
}
//方法同步,調用效率低!
public static synchronized SingletonDemo6 getInstance() {
if(instance == null) {
instance = new SingletonDemo6();
}
return instance;
}
}
再次執行Client2.java,測試結果爲:
com.stormkai.singleton.demo.SingletonDemo6@15db9742
com.stormkai.singleton.demo.SingletonDemo6@15db9742
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
at java.lang.reflect.Constructor.newInstance(Unknown Source)
at com.stormkai.singleton.demo.Client2.main(Client2.java:30)
Caused by: java.lang.RuntimeException
at com.stormkai.singleton.demo.SingletonDemo6.<init>(SingletonDemo6.java:20)
... 5 more
3.2 反序列化破解單例模式
給單例的實現類SingletonDemo6.java添加序列化implements Serializable
public class SingletonDemo6 implements Serializable {
//類初始化時,不初始化這個對象(延時加載,真正用的時候再創建)。
private static SingletonDemo6 instance;
private SingletonDemo6(){
//添加如下代碼,反射破解單例時候會報java.lang.reflect.InvocationTargetException
if(instance != null) {
throw new RuntimeException();
}
}
//方法同步,調用效率低!
public static synchronized SingletonDemo6 getInstance() {
if(instance == null) {
instance = new SingletonDemo6();
}
return instance;
}
}
使用反序列化破解單例模式測試類:Client2.java:
public class Client2 {
public static void main(String[] args) throws Exception {
SingletonDemo6 s1 = SingletonDemo6.getInstance();
SingletonDemo6 s2 = SingletonDemo6.getInstance();
System.out.println(s1);
System.out.println(s2);
//通過反序列化的方式構造多個對象
FileOutputStream fos = new FileOutputStream("d:/aaa.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s1);
oos.close();
fos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/aaa.txt"));
SingletonDemo6 s3 = (SingletonDemo6)ois.readObject();
System.out.println(s3);
}
}
測試結果爲:
com.stormkai.singleton.demo.SingletonDemo6@15db9742
com.stormkai.singleton.demo.SingletonDemo6@15db9742
com.stormkai.singleton.demo.SingletonDemo6@776ec8df
3.2.1 防止反序列化破解單例模式
反序列化時,如果定義了readResolve()則直接返回此方法指定的對象。而不需要單獨再創建新對象!
修改單例實現類SingletonDemo6.java
public class SingletonDemo6 implements Serializable {
//類初始化時,不初始化這個對象(延時加載,真正用的時候再創建)。
private static SingletonDemo6 instance;
private SingletonDemo6(){
//添加如下代碼,反射破解單例時候會報java.lang.reflect.InvocationTargetException
if(instance != null) {
throw new RuntimeException();
}
}
//方法同步,調用效率低!
public static synchronized SingletonDemo6 getInstance() {
if(instance == null) {
instance = new SingletonDemo6();
}
return instance;
}
//反序列化時,如果定義了readResolve()則直接返回此方法指定的對象。而不需要單獨再創建新對象!
private Object readResolve() throws ObjectStreamException {
return instance;
}
}
再次執行Client2.java,測試結果爲:
com.stormkai.singleton.demo.SingletonDemo6@15db9742
com.stormkai.singleton.demo.SingletonDemo6@15db9742
com.stormkai.singleton.demo.SingletonDemo6@15db9742
4. 常見的五種模式在多線程環境下的效率測試
測試類:
public class Client3 {
public static void main(String[] args) throws Exception {
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 = SingletonDemo2.getInstance();
//Object o = SingletonDemo5.INSTANCE;
}
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await(); //main線程阻塞,直到計數器變爲0,纔會繼續往下執行!
long end = System.currentTimeMillis();
System.out.println("總耗時:"+(end-start));
}
}
CountDownLatch
同步輔助類,在完成一組正在其他線程中執行的操作之前,它允許一個或者多個線程一直等待。
- countDown():當前線程調此方法,則計數減一(建議放在finally裏執行)
- await():調用此方法會一直阻塞當前線程,直到計時器值爲0