TA的每日心情 | 开心 2021-3-12 23:18 |
---|
签到天数: 2 天 [LV.1]初来乍到
|
本文将探讨单例模式的各种情况,并给出相应的建议。单例模式应该是设计模式中比较简单的一个,但是在多线程并发的环境下使用却是不那么简单了。
首先看最原始的单例模式。
1
package
xylz.study.singleton;
2
3
public
class
Singleton {
4
5
private
static
Singleton instance
=
null
;
6
7
private
Singleton() {
8
}
9
10
public
static
Singleton getInstance() {
11
if
(instance
==
null
) {
12
instance
=
new
Singleton();
13
}
14
return
instance;
15
}
16
}
17
显然这个写法在单线程环境下非常好,但是多线程会导致多个实例出现,这个大家都能理解。
最简单的改造方式是添加一个同步锁。
1
package
xylz.study.singleton;
2
3
public
class
SynchronizedSingleton {
4
5
private
static
SynchronizedSingleton instance
=
null
;
6
7
private
SynchronizedSingleton() {
8
}
9
10
public
static
synchronized
SynchronizedSingleton getInstance() {
11
if
(instance
==
null
) {
12
instance
=
new
SynchronizedSingleton();
13
}
14
return
instance;
15
}
16
}
17
显然上面的方法避免了并发的问题,但是由于我们只是在第一次构造对象的时候才需要同步,以后就不再需要同步,所以这里不可避免的有性能开销。于是将锁去掉采用静态的属性来解决同步锁的问题。
1
package
xylz.study.singleton;
2
3
public
class
StaticSingleton {
4
5
private
static
StaticSingleton instance
=
new
StaticSingleton();
6
7
private
StaticSingleton() {
8
}
9
10
public
static
StaticSingleton getInstance() {
11
return
instance;
12
}
13
}
14
上面的方法既没有锁又解决了性能问题,看起来已经满足需求了。但是追求“完美”的程序员想延时加载对象,希望在第一次获取的时候才构造对象,于是大家非常聪明的进行改造,也即非常出名的双重检查锁机制出来了。
1
package
xylz.study.singleton;
2
3
public
class
DoubleLockSingleton {
4
5
private
static
DoubleLockSingleton instance
=
null
;
6
7
private
DoubleLockSingleton() {
8
}
9
10
public
static
DoubleLockSingleton getInstance() {
11
if
(instance
==
null
) {
12
synchronized
(DoubleLockSingleton.
class
) {
13
if
(instance
==
null
) {
14
instance
=
new
DoubleLockSingleton();
15
}
16
}
17
}
18
return
instance;
19
}
20
}
21
双重锁机制看起来非常巧妙的避免了上面的问题。但是真的是这样的吗?文章《双重检查锁定及单例模式》中谈到了非常多演变的双重锁机制带来的问题,包括比较难以理解的指令重排序机制等。总之就是双重检查锁机制仍然对导致错误问题而不是性能问题。
于是继续改造,某个牛人利用JVM的特性来解决上述问题,具体哪个牛人我忘记了,但是不是下面文章的作者。
(1)《Java theory and practice: Fixing the Java Memory Model, Part 2》
(2)《Initialize-On-Demand Holder Class and Singletons》
1
package
xylz.study.singleton;
2
3
public
class
HolderSingleton {
4
5
private
static
class
HolderSingletonHolder {
6
7
static
HolderSingleton instance
=
new
HolderSingleton();
8
}
9
10
private
HolderSingleton() {
11
//
maybe throw an Exception when doing something
12
}
13
14
public
static
HolderSingleton getInstance() {
15
return
HolderSingletonHolder.instance;
16
}
17
}
18
上述代码看起来解决了上面单例模式遇到的所有问题,而且实际上工作的很好,没有什么问题。但是却有一个致命的问题,如果第11行抛出了一个异常,也就是第一次构造函数失败将导致永远无法再次得到构建对象的机会。
使用下面的代码测试下。
1
package
xylz.study.singleton;
2
3
public
class
HolderSingletonTest {
4
5
private
static
class
HolderSingletonHolder {
6
7
static
HolderSingletonTest instance
=
new
HolderSingletonTest();
8
}
9
10
private
static
boolean
init
=
false
;
11
12
private
HolderSingletonTest() {
13
//
maybe throw an Exception when doing something
14
if
(
!
init) {
15
init
=
true
;
16
throw
new
RuntimeException(
"
fail
"
);
17
}
18
}
19
20
public
static
HolderSingletonTest getInstance() {
21
return
HolderSingletonHolder.instance;
22
}
23
public
static
void
main(String[] args) {
24
for
(
int
i
=
0
;i
<
3
;i
++
) {
25
try
{
26
System.out.println(HolderSingletonTest.getInstance());
27
}
catch
(Exception e) {
28
System.err.println(
"
one->
"
+
i);
29
e.printStackTrace();
30
}
catch
(ExceptionInInitializerError err) {
31
System.err.println(
"
two->
"
+
i);
32
err.printStackTrace();
33
}
catch
(Throwable t) {
34
System.err.println(
"
three->
"
+
i);
35
t.printStackTrace();
36
}
37
}
38
}
39
}
40
很不幸将得到以下输出:
1
two
->
0
2
java.lang.ExceptionInInitializerError
3
at xylz.study.singleton.HolderSingletonTest.getInstance(HolderSingletonTest.java:
21
)
4
at xylz.study.singleton.HolderSingletonTest.main(HolderSingletonTest.java:
26
)
5
Caused by: java.lang.RuntimeException: fail
6
at xylz.study.singleton.HolderSingletonTest.
<
init
>
(HolderSingletonTest.java:
16
)
7
at xylz.study.singleton.HolderSingletonTest.
<
init
>
(HolderSingletonTest.java:
12
)
8
at xylz.study.singleton.HolderSingletonTest$HolderSingletonHolder.
<
clinit
>
(HolderSingletonTest.java:
7
)
9
2
more
10
three
->
1
11
java.lang.NoClassDefFoundError: Could not initialize
class
xylz.study.singleton.HolderSingletonTest$HolderSingletonHolder
12
at xylz.study.singleton.HolderSingletonTest.getInstance(HolderSingletonTest.java:
21
)
13
at xylz.study.singleton.HolderSingletonTest.main(HolderSingletonTest.java:
26
)
14
three
->
2
15
java.lang.NoClassDefFoundError: Could not initialize
class
xylz.study.singleton.HolderSingletonTest$HolderSingletonHolder
16
at xylz.study.singleton.HolderSingletonTest.getInstance(HolderSingletonTest.java:
21
)
17
at xylz.study.singleton.HolderSingletonTest.main(HolderSingletonTest.java:
26
)
18
很显然我们想着第一次加载失败第二次能够加载成功,非常不幸,JVM一旦加载某个类失败将认为此类的定义有问题,将来不再加载,这样就导致我们没有机会再加载。目前看起来没有办法避免此问题。如果要使用JVM的类加载特性就必须保证类加载一定正确,否则此问题将比并发和性能更严重。如果我们的类需要初始话那么就需要想其它办法避免在构造函数中完成。看起来像是又回到了老地方,难道不是么?
总之,结论是目前没有一个十全十美的单例模式,而大多数情况下我们只需要满足我们的需求就行,没必有特意追求最“完美”解决方案。
|
|