设计模式之单例模式!

单例模式就是在程序运行中只实例化一次,创建一个全局唯一对象。

饿汉模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快
public class SingletonObject {
// 利用静态变量来存储唯一实例
private static final SingletonObject instance = new SingletonObject();

// 私有化构造函数
private SingletonObject(){
// 里面可能有很多操作
}

// 提供公开获取实例接口
public static SingletonObject getInstance(){
return instance;
}
}

饿汉模式优缺点:

缺点:

  • 不能实现懒加载,造成空间浪费。
  • 如果一个类比较大,在初始化的时就加载了这个类,但是长时间没有使用这个类,这就导致了内存空间的浪费。

懒汉模式

在程序初始化时不会创建实例,只有在使用实例的时候才会创建实例。

  • 所以懒汉模式解决了饿汉模式带来的空间浪费问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SingletonObject {
// 定义静态变量时,未初始化实例
private static SingletonObject instance;

// 私有化构造函数
private SingletonObject(){

}

public static SingletonObject getInstance(){
// 使用时,先判断实例是否为空,如果实例为空,则实例化对象
// 这段代码在多线程的情况下是不安全的
if (instance == null)
instance = new SingletonObject();
return instance;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SingletonObject {
private static SingletonObject instance;

private SingletonObject(){

}

public synchronized static SingletonObject getInstance(){
/**
* 添加class类锁,影响了性能,加锁之后将代码进行了串行化
* 我们的代码块绝大部分是读操作,在读操作的情况下,代码线程是安全的
*/
if (instance == null)
instance = new SingletonObject();
return instance;
}
}

懒汉模式的优缺点:

优点:

  • 实现了懒加载,节约了内存空间。

缺点:

  • 在不加锁的情况下,线程不安全,可能出现多份实例。
  • 在加锁的情况下,会是程序串行化,使系统有严重的性能问题。

双重检查锁模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class SingletonObject {
private static SingletonObject instance;

private SingletonObject(){

}

public static SingletonObject getInstance(){

// 第一次判断,如果这里为空,不进入抢锁阶段,直接返回实例
if (instance == null)
synchronized (SingletonObject.class){
// 抢到锁之后再次判断是否为空
if (instance == null){
instance = new SingletonObject();
}
}

return instance;
}
}

在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。

1
2
3
4
5
6
private SingletonObject(){
1 int x = 10;
2 int y = 30;
3 Object o = new Object();

}

我们编写的顺序是1、2、3,JVM 会对它进行指令重排序。

  • 所以执行顺序可能是3、1、2,也可能是2、3、1,不管是那种执行顺序,JVM 最后都会保证所以实例都完成实例化。

如果构造函数中操作比较多时,为了提升效率,JVM 会在构造函数里面的属性未全部完成实例化时,就返回对象。

双重检测锁出现空指针问题的原因就是出现在这里。

  • 当某个线程获取锁进行实例化时,其他线程就直接获取实例使用。
  • 由于JVM指令重排序的原因,其他线程获取的对象也许不是一个完整的对象,所以在使用实例的时候就会出现空指针异常问题。

要解决双重检查锁模式带来空指针异常的问题,只需要使用volatile关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    // 添加volatile关键字
private static volatile SingletonObject instance;

private SingletonObject(){

}

public static SingletonObject getInstance(){

if (instance == null)
synchronized (SingletonObject.class){
if (instance == null){
instance = new SingletonObject();
}
}

return instance;
}
}

静态内部类单例模式

静态内部类单例模式实例由内部类创建。

由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。

静态属性由static修饰,保证只被实例化一次,并且严格保证实例化顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SingletonObject {


private SingletonObject(){

}
// 单例持有者
private static class InstanceHolder{
private final static SingletonObject instance = new SingletonObject();

}

//
public static SingletonObject getInstance(){
// 调用内部类属性
return InstanceHolder.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
public class SingletonObject {


private SingletonObject(){

}

/**
* 枚举类型是线程安全的,并且只会装载一次
*/
private enum Singleton{
INSTANCE;

private final SingletonObject instance;

Singleton(){
instance = new SingletonObject();
}

private SingletonObject getInstance(){
return instance;
}
}

public static SingletonObject getInstance(){

return Singleton.INSTANCE.getInstance();
}
}

破坏单例模式的方法及解决办法

除枚举方式外,其他方法都会通过反射的方式破坏单例。

反射是通过调用构造方法生成新的对象。

  • 所以如果想要阻止单例破坏,可以在构造方法中进行判断,若已有实例, 则阻止生成新的实例。
1
2
3
4
5
6
7
8
9
Class objClass = SingletonObject.class;
//获取类的构造器
Constructor constructor = objClass.getDeclaredConstructor();
//把构造器私有权限放开
constructor.setAccessible(true);
//正常的获取实例方式
SingletonObject staticInnerClass = SingletonObject.getInstance();
//反射创建实例
SingletonObject newStaticInnerClass = (SingletonObject) constructor.newInstance();
1
2
3
4
5
private SingletonObject(){
if (instance !=null){
throw new RuntimeException("实例已经存在,请通过 getInstance()方法获取");
}
}

如果单例类实现了序列化接口Serializable ,就可以通过反序列化破坏单例。

所以可以不实现序列化接口。

  • 如果非得实现序列化接口,可以重写反序列化方法readResolve(), 反序列化时直接返回相关单例对象。
1
2
3
public Object readResolve() throws ObjectStreamException {
return instance;
}