1、單例模式介紹html
單例模式:保證一個類只有一個實例,而且提供一個訪問該實例的全局訪問點。java
單例模式優勢:數據庫
1.只生成一個實例,系統開銷比較小windows
2.單例模式能夠在系統設置全局的訪問點,優化共享資源的訪問。設計模式
常見單例模式分類:安全
主要:併發
餓漢式(線程安全,調用效率高,可是不能延時加載)ide
懶漢式(線程安全,調用效率不高,可是能夠延時加載)高併發
其餘:學習
雙重檢測鎖式(因爲JVM底層內部模型緣由,偶爾會出問題。不建議使用)
靜態內部類式(線程安全,調用效率高。可是能夠延時加載)
枚舉單例(線程安全,調用效率高,不能延時加載)
2、單例模式實例代碼
一、懶漢式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package
com.fz.singleton;
/**
* 餓漢式單例:所謂餓漢式,就是比較餓。當類一加載的時候就直接new了一個靜態實例。無論後面有沒有用到該實例
*/
public
class
Singleton1 {
/**
* 一、提供一個靜態變量。
* 當類加載器加載該類時,就new一個實例出來。從屬於這個類。無論後面用不用這個類。因此沒有延時加載功能
*/
private
static
Singleton1 instance =
new
Singleton1();
/**
* 二、私有化構造器:外部是不能直接new該對象的
*/
private
Singleton1(){}
/**
* 三、對外提供一個公共方法來獲取這個惟一對象(方法沒有使用synchronized則調用效率高)
* @return
*/
public
static
Singleton1 getInstance(){
return
instance;
}
}
|
二、餓漢式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package
com.fz.singleton;
/**
* 懶漢式單例:比較懶,一開始不初始化實例。等何時用就何時初始化.避免資源浪費
*/
public
class
Singleton2 {
/**
* 一、聲明一個靜態實例,不給它初始化。等何時用就何時初始化。節省資源
*/
private
static
Singleton2 instance;
/**
* 二、依然私有化構造器,對外不讓new
*/
private
Singleton2(){}
/**
* 三、對外提供一個獲取實例的方法,由於靜態屬性沒有實例化。
* 假如高併發的時候,有可能會同時調用該方法。形成new出多個實例。因此須要加上同步synchronized,所以調用效率不高
* 在方法上加同步,是整個方法都同步。效率不高
* @return
*/
public
synchronized
static
Singleton2 getInstance(){
if
(instance ==
null
) {
//第一次調用時爲空,則直接new一個
instance =
new
Singleton2();
}
//以後第二次再調用的時候就已經初始化了,不用再new。直接返回
return
instance;
}
}
|
三、雙重檢索方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
package
com.fz.singleton;
/**
* 雙重檢索單例模式
* 將鎖加在判斷實例爲空的地方,不加在方法上
*/
public
class
Singleton3 {
/**
* 一、提供未實例化的靜態實例
*/
private
static
Singleton3 instance =
null
;
/**
* 二、私有化構造器
*/
private
Singleton3(){}
/**
* 三、對外提供獲取實例的方法
* 可是同步的時候將鎖放到第一次獲取實例的時候,這樣的好處就是隻有第一次會同步。效率高
* @return
*/
public
static
Singleton3 getInstance(){
if
(instance ==
null
) {
Singleton3 s3;
synchronized
(Singleton3.
class
) {
s3 = instance;
if
(s3 ==
null
) {
synchronized
(Singleton3.
class
) {
if
(s3 ==
null
) {
s3 =
new
Singleton3();
}
}
instance = s3;
}
}
}
return
instance;
}
}
|
四、靜態內部類方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package
com.fz.singleton;
/**
* 靜態內部類單例實現
*/
public
class
Singleton4 {
/**
* 一、私有化構造器
*/
private
Singleton4(){}
/**
* 二、聲明一個靜態內部類,在靜態內部類內部提供一個外部類的實例(常量,不可改變)
* 初始化Singleton4 的時候不會初始化SingletonClassInstance,實現了延時加載。而且線程安全
*/
private
static
class
SingletonClassInstance{
//該實例只讀,無論誰都不能修改
private
static
final
Singleton4 instance =
new
Singleton4();
}
/**
* 三、對外提供一個獲取實例的方法:直接返回靜態內部類中的那個常量實例
* 調用的時候沒有同步等待,因此效率也高
* @return
*/
public
static
Singleton4 getInstance(){
return
SingletonClassInstance.instance;
}
}
|
五、枚舉單例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package
com.fz.singleton;
/**
* 枚舉實現單例模式(枚舉自己就是單例)
*/
public
enum
Singleton5 {
/**
* 定義一個枚舉元素,它就是一個單例的實例了。
*/
INSTANCE;
/**
* 對枚舉的一些操做
*/
public
void
singletonOperation(){
}
}
|
3、如何破解單例模式?
a、經過反射破解(不包括枚舉,由於枚舉自己是單例,是由JVM管理的)
b、經過反序列化
一、經過反射破解單例實例代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package
com.fz.singleton;
import
java.lang.reflect.Constructor;
/**
* 經過反射破解單例模式
*/
public
class
TestReflect {
public
static
void
main(String[] args)
throws
Exception {
Singleton6 s1 = Singleton6.getInstance();
Singleton6 s2 = Singleton6.getInstance();
System.out.println(s1 == s2);
//true
//經過反射破解
Class<Singleton6> clazz = (Class<Singleton6>) Class.forName(Singleton6.
class
.getName());
Constructor<Singleton6> c = clazz.getDeclaredConstructor(
null
);
//得到無參構造器
c.setAccessible(
true
);
//跳過檢查:能夠訪問private構造器
Singleton6 s3 = c.newInstance();
//此時會報錯:沒有權限訪問私有構造器
Singleton6 s4 = c.newInstance();
System.out.println(s3==s4);
//不加c.setAccessible(true)則會報錯。此時的結果就是false,得到的就是兩個對象
}
}
|
如何防止反射破解單例模式呢?
在Singleton6構造的時候,假如不是第一次就直接拋出異常。不讓建立。這樣第二次構建的話就直接拋出異常了。
1
2
3
4
5
6
|
private
Singleton6(){
if
(instance !=
null
) {
//若是不是第一次構建,則直接拋出異常。不讓建立
throw
new
RuntimeException();
}
}
|
二、經過序列化和反序列化構建對象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
package
com.fz.singleton;
import
java.io.FileInputStream;
import
java.io.FileOutputStream;
import
java.io.ObjectInputStream;
import
java.io.ObjectOutputStream;
import
java.lang.reflect.Constructor;
/**
* 經過反射破解單例模式
*/
public
class
TestReflect {
public
static
void
main(String[] args)
throws
Exception {
Singleton6 s1 = Singleton6.getInstance();
Singleton6 s2 = Singleton6.getInstance();
//經過反序列化構建對象:經過序列化將s1存儲到硬盤上,而後再經過反序列化把s1再構建出來
FileOutputStream fos =
new
FileOutputStream(
"e:/a.txt"
);
ObjectOutputStream oos =
new
ObjectOutputStream(fos);
oos.writeObject(s1);
oos.close();
fos.close();
//經過反序列化將s1對象再構建出來
ObjectInputStream ois =
new
ObjectInputStream(
new
FileInputStream(
"e:/a.txt"
));
Singleton6 s5 = (Singleton6) ois.readObject();
System.out.println(s5);
//此時打印出一個新對象
System.out.println(s1==s5);
//false
}
}
|
防止反序列化構建對象
在Singleton6中定義一個方法,此時結果就會同樣了。System.out.println(s1==s5);結果就是true了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
package
com.fz.singleton;
import
java.io.ObjectStreamException;
import
java.io.Serializable;
/**
* 用於測試反射破解的單例類
*/
public
class
Singleton6
implements
Serializable {
/**
* 一、提供一個靜態變量。
* 當類加載器加載該類時,就new一個實例出來。從屬於這個類。無論後面用不用這個類。因此沒有延時加載功能
*/
private
static
Singleton6 instance =
new
Singleton6();
/**
* 二、私有化構造器:外部是不能直接new該對象的
*/
private
Singleton6(){
if
(instance !=
null
) {
//若是不是第一次構建,則直接拋出異常。不讓建立
throw
new
RuntimeException();
}
}
/**
* 三、對外提供一個公共方法來獲取這個惟一對象(方法沒有使用synchronized則調用效率高)
* @return
*/
public
static
Singleton6 getInstance(){
return
instance;
}
/**
* 反序列化時,若是定義了readResolve()則直接返回該方法指定的實例。不會再單首創建新對象!
* @return
* @throws ObjectStreamException
*/
private
Object readResolve()
throws
ObjectStreamException{
return
instance;
}
}
|
測試幾種單例的速度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
package
com.fz.singleton;
import
java.util.concurrent.CountDownLatch;
/**
* 測試幾種單例模式的速度
*/
public
class
TestSingleton {
public
static
void
main(String[] args)
throws
InterruptedException {
long
start = System.currentTimeMillis();
int
threadNum =
10
;
//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 <
100000
; i++) {
Object o = Singleton5.INSTANCE;
}
countDownLatch.countDown();
//計數器-1
}
}).start();
}
countDownLatch.await();
//main線程阻塞
long
end = System.currentTimeMillis();
System.out.println(
"耗時:"
+(end-start));
/**
* 結果(毫秒):
* Singleton1(餓漢式)耗時:5
* Singleton2(懶漢式)耗時:227
* Singleton3(雙重檢索式)耗時:7
* Singleton4(靜態內部類式)耗時:40
* Singleton5(枚舉式)耗時:5
*/
}
}
|
4、總結
如何選用?
枚舉式 好於 餓漢式
靜態內部類式 好於 懶漢式
常見應用場景
windows的任務管理器
網站的計數器
數據庫的鏈接池
Application容器也是單例
Spring中每一個bean默認也是單例
Servlet中,每一個servlet也是單例
參考資料:
大話設計模式(帶目錄完整版).pdf
HEAD_FIRST設計模式(中文版).pdf
尚學堂_高淇_java300集最全視頻教程_【GOF23設計模式】