单例模式

单例模式是代码里只有一个实例,因此需要考虑如何防止别人再new一个实例。

思路一(饿汉式)

很简单,把构造函数写成private就可以,然后在类里直接构造一个private实例,只public一个get接口(getInstance)这样就只能获得实例,不能改写,也不能new一个新实例。

另外需要注意的是,创建实例和获得实例的函数都是static类型的,也就是只属于类本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package singleton;

public class Mgr01 {
private static final Mgr01 INSTANCE = new Mgr01();
private Mgr01() {};

public static Mgr01 getInstance() {return INSTANCE;}

public static void main(String[] args){
Mgr01 m1 = Mgr01.getInstance();
Mgr01 m2 = Mgr01.getInstance();
System.out.println(m1 == m2);
}
}

另外这是线程安全的,因为jvm保证每一个class只漏到内存一次,因此只初始化一次(多线程访问也没关系)——推荐使用!

思路二

和饿汉式一样,只不过把构造函数改成静态语句块,本质上是一样的。

1
2
3
4
private static final Mgr02 INSTANCE;
static {
INSTANCE = new Merge02{};
}

思路三(懒汉式)

和饿汉式的区别是什么时候用什么时候初始化。这种方法是调用getInstance的时候才进行初始化(需要加个判断操作),而饿汉式是只要类加载那么就自动初始化。但是也带来了线程不安全的问题,因为有可能多个线程同时调用getInstance函数,在进行if判断的时候为null,然后建立了多个实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package singleton;

public class Mgr03 {
private static Mgr03 INSTANCE; //这里没有final啦!
private Mgr03() {};

public static Mgr03 getInstance() {
if(INSTANCE == null){
INSTANCE = new Mgr03();
}
return INSTANCE;
}

public static void main(String[] args){
Mgr03 m1 = Mgr03.getInstance();
Mgr03 m2 = Mgr03.getInstance();
System.out.println(m1 == m2);
}
}

思路四

对于懒汉式如何解决线程安全问题,那么就加个锁,但是相应的也会导致性能的下降。

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
package singleton;

public class Mgr04 {
private static Mgr04 INSTANCE;
private Mgr04() {};

public static synchronized Mgr04 getInstance() {
if(INSTANCE == null){
try{
Thread.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
INSTANCE = new Mgr04();
}
return INSTANCE;
}

public static void main(String[] args){
for(int i=0;i<100;i++){
new Thread(()->{
System.out.println(Mgr04.getInstance().hashCode());
}).start();
}
}
}

思路五:静态内部类方式

jvm保证单例,因此class只会加载一次,MgrHolder也只加载一次,因此保证了线程安全。

加载外部类的时候不会加载静态内部类,保证了懒加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package singleton;

public class Mrg05 {
private Mrg05() {
}

private static class Mrg05Holder {
private final static Mrg05 Instance = new Mrg05();
}

public static Mrg05 getInstance() {
return Mrg05Holder.Instance;
}

public static void main(String[] args) {
for(int i=0;i<100;i++){
new Thread(()->{
System.out.println(Mgr04.getInstance().hashCode());
}).start();
}
}
}

思路六

枚举单例,非常鬼才的一种方式,不仅可以解决线程同步,同时可以防止反序列化,因为枚举类没有构造方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
package singleton;

public class Mrg06 {
INSTANCE;

public static void main(String[] args) {
for(int i=0;i<100;i++){
new Thread(()->{
System.out.println(Mgr06.INSTANCE.hashCode());
}).start();
}
}
}

除了以上六种,还有在第四种的基础上扩展的两种方法,为了增加效率,减少同步代码块的大小,但是需要额外判断两次instance == null,代码略。这里有一个重点就是需要加上volatile,如果不加volatile可能因为指令重排导致一些问题。

总结:比较常用且完美的方法就是饿汉式(常用简洁)、懒汉式两次判断+静态内部类。枚举单例很完美但是不常用。