Java-单例模式实现

Java单例模式实现

学习枚举类的时候看到了一篇文章, 其中大致总结了几种单例模式的写法, 正好我也抽空自己整理一下

简介

什么是单例模式? 其实是一种最常使用的设计模式, 也就是确保某个类只有一个实例, 而且这个类能自行实例化并向整个系统提供这个实例.

应用在: 线程池, 缓存, 日志对象, 对话框对象等.

实现

饿汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SingletonHungry {

private static SingletonHungry instance = new SingletonHungry();

private SingletonHungry() {

}

public static SingletonHungry getInstance() {
return instance;
}

}

比较简单的一种写法

在类加载的时候就会创建对象, 但是有一个问题就是如果这个类依赖于很多资源, 那么创建必定比较耗时. 所以我们希望他能够延迟加载, 减少初始化负载, 从而就有了懒汉式单例实现

懒汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SingletonLazy {

private static volatile SingletonLazy singletonLazy;

private SingletonLazy() {
}

public static synchronized SingletonLazy getInstance() {
if (singletonLazy == null) {
singletonLazy = new SingletonLazy();
}
return singletonLazy;
}

}

这样写, 就具备了懒加载的特点. 并且为了能够在多线程中更好的工作, 加入了 synchronized 关键字. 优点自然是可以更好的同步, 缺点就是因为 synchronized, 效率会变低.

volatile 关键字打算再另一篇里面写, 简单描述一下就是:

第一层语义是可见性,可见性是指在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以其它线程会马上读取到已修改的值,关于工作内存和主内存可简单理解为高速缓存(直接与CPU打交道)和主存(日常所说的内存条),注意工作内存是线程独享的,主存是线程共享的。volatile的第二层语义是禁止指令重排序优化,我们写的代码(特别是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同,这在单线程并没什么问题,然而一旦引入多线程环境,这种乱序就可能导致严重问题。volatile关键字就可以从语义上解决这个问题,值得关注的是volatile的禁止指令重排序优化功能在Java 1.5后才得以实现,因此1.5前的版本仍然是不安全的,即使使用了volatile关键字。

为了优化这个缺点, 在单线程的情况下, 去掉 synchronized 关键字.

双重检查锁

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

private static volatile Singleton singleton = null;

private Singleton() {
}

public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

双重检查锁进行了两次 null 检查. 因为在 getSinleton 调用的时候并没有进入同步代码, 外面的检查是并发的, 并且过滤了绝大多数的 null 检查. 当出现 null 的时候进入同步代码再进行一次检查.

这样极大提升了并发度, 也提升了性能.

但是有一个问题, 就是 volatile 关键字是 1.5 才实现的禁止指令重排, 所以可以使用静态内部类

静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
public class SingletonInner {

private static class Holder{
private static SingletonInner singleton = new SingletonInner();
}

private SingletonInner(){}

public static SingletonInner getInstance(){
return Holder.singleton;
}
}

我们把Singleton实例放到一个静态内部类中,这样可以避免了静态实例在Singleton类的加载阶段(类加载过程的其中一个阶段的,此时只创建了Class对象)就创建对象,毕竟静态变量初始化是在SingletonInner类初始化时触发的,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的

共同缺点

  1. 序列化可能会破坏单例模式. 因为每次反序列化一个序列化的对象会创建一个新的实例, 解决方案:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Singleton implements java.io.Serializable {     
    public static Singleton INSTANCE = new Singleton();

    protected Singleton() {
    }

    //反序列时直接返回当前INSTANCE
    private Object readResolve() {
    return INSTANCE;
    }
    }
  2. 使用反射强行调用私有构造器. 解决方案: 修改构造器, 让他在创建第二个实例的时候抛异常

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public static Singleton INSTANCE = new Singleton();     
    private static volatile boolean flag = true;
    private Singleton(){
    if(flag){
    flag = false;
    }else{
    throw new RuntimeException("The instance already exists !");
    }
    }

优化

上面的四类模式有了, 共同的缺点也能解决了, 但是代码复杂度也上去了, 更高效的方法就是枚举单例.

1
2
3
4
5
6
7
8
9
10
public enum SingletonEnum {
INSTANCE;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

无比简洁!

使用枚举单例我们还不需要考虑序列化和反射的问题, 因为枚举序列化是由jvm保证的,每一个枚举类型和定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定:在序列化时Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的并禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,从而保证了枚举实例的唯一性

这里要深入了解可以查看 Enum 类的 valueOf 方法.

总结就是: 创建枚举实例只有编译器能做到

单例模式的几个重点需要我们一直注意:

  • 线程安全
  • 延迟加载
  • 序列化与反序列化安全
  • 反射安全