单例模式在我们日常开发中算是使用频率最高的设计模式了,为什么单例模式的使用频率会这么高呢?
有时我们的对象会占用一些系统资源,所以我们需要保证这些对象在整个系统中只有一个实例对象。甚至在我们自己设计类的时候,如果这个类的对象存在多个的时候可能会对我们的功能造成一定影响,我们都可以将其设计成一个单例的形式。就好比一个公司在一个时间只会存在一个董事长、一个国家最会存在一个总统或者主席是一样的道理。
单例类的特点就是只保证只会存在一个实例对象、它的构造方法必须私有化、它的对象由自己或者它的外部类来进行创建。这里我们主要说下饿汉式、懒汉式、双重检查锁形式、枚举形式、内部类形式、容器形式
1、饿汉式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
这种方式在类创建的时候就会将实例对象创建出来,所以也是线程安全的,但是它也达不到我们的lazy loading的要求。
2、懒汉式
public class Singleton {
private static volatile Singleton instance;
private Singleton(){
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种方式是在类加载的时候并不会去创建它的实例对象,而是在外部调用getInstance方法的时候创建,考虑到多线程问题,所以将方法同步。但是这样就会造成每次调用该方法的时候都会进行同步,造成资源的浪费。
3、DoubleCheckLock(DCL)
public class Singleton {
private static volatile Singleton instance;
private Singleton(){
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
考虑到懒汉式每次都会进行同步,所以这种DCL的方式只会在创建对象的时候进行同步,以后再调用的时候将不会进行同步了。因为加入的双重判断又有同步情况,所以在第一次创建的时候效率有点低。在大多数情况下这种方式能够保证对象唯一。
这里加上volatile的原因是为了在初始化单例对象的时候防止线程切换后使用单例对象出错。
在JVM处理instance = new Singleton()的时候有3个步骤,而且这个步骤顺序是不确定的。
①为Singleton对象开辟内存
②调用Singleton的构造函数初始化对象
③将instance指向分配的内存空间
如果线程A执行顺序是①③②,当执行到③的时候就将线程切换到其他线程B,在B线程中因为单例对象不为null(指向了开辟的内存),所以使用的时候就会出错,对象还没有初始化完成。
注意:在高并发的情况下,DCL这种方式还是有失效的情况。
4、枚举单例
public enum Singleton {
INSTANCE;
private Singleton() {
}
public void go() {
System.out.print("ENUM 单例");
}
}
枚举在初始化创建对象的时候是默认线程安全的,同时对象也是唯一的,它的写法也比较简单。它有一个优势就是在反序列化的时候对象同样是唯一的,但是其他几种方式的单例如果在反序列化的时候则对象不是唯一的。当然也有解决办法就是在类中加入readResolve方法,该方法返回单例对象即可。例如:
public class Singleton {
private static class SingletonHolder{
static Singleton instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
private Object readResolve() throws ObjectStreamException {
return SingletonHolder.instance;
}
}
5、静态内部类单例
public class Singleton {
private static class SingletonHolder{
static Singleton instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
它和饿汉式有点类似,但不同之处在于前者在加载类的时候就会初始化单例对象,而静态内部类方式只有在调用getInstance方法在加载内部类的时候才会初始化,而在jvm加载类的时候又保证了线程安全,所以这种既安全又延迟加载的方式是比较推荐使用的。
6、容器单例模式
容器类单例模式会使用一个map集合在装载单例对象。这种方式可以管理多个单例对象,但是实际上它还是需要保证它的各个单例对象的类不能被外部直接创建。
public class SingletonContainer {
private static final Map C = new HashMap<>();
private SingletonContainer() {
throw new RuntimeException("");
}
public static void putObject(String key,Object o) {
if (!C.containsKey(key)) {
C.put(key,o);
}
}
public static Object getObject(String key) {
if (C.containsKey(key)) {
return C.get(key);
}
return null;
}
}
这种方式很少用到,不过在Android中的Context里面使用到了这种方式。在android中获取系统服务调用getSystemService这个方法的时候就是使用的map形式获取。它的单例对象管理类是SystemServiceRegistry,它被加载时就会初始化很多系统服务相关的类对象。在ContextImpl这个类中有这么一行代码,它是Context的子实现类:
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}