单例模式

介绍

  单例模式(Singleton Pattern)可能是设计模式中用的最多的模式,单例模式非常简单同时代码也比较短方便手写代码,所以也是面试中经常会问到的设计模式。单例模式它是一种对象创建模式,它用于确保系统中一个类只产生一个实例。例如:一个系统使用AppConfig对象读取诸如xml和properties配置文件,而当系统运行时,能够存在多个AppConfig对象,而每一个AppConfig对象中都封装着配置文件,这样非常浪费内存资源,因此系统内部只需一个该配置对象,这里就需要使用单例模式了。

  本文针结合网上的资料对单例模式的常有的写法进行整理,希望对你有一定帮助。

最直接的单例模式

  当面试题被问到这一题的时候,很多人的根据直觉可能写出下面的代码。

public class Singleton01 {
    private static Singleton01 instance;
    private Singleton01() {}
    public static Singleton01 getInstance() {
        if (instance == null) {
            instance = new Singleton01();
        }
        return instance;
    }
}

  上面的代码简单明了,是很多人的最终答案,上面的代码还使用懒加载模式(就是需要使用实例对象时,才创建对象)。但有个明显的缺陷,就是只能在单线程中使用,在多线程时,会创建多个实例,造成内存泄漏。

线程安全的单例模式

  遇到多线程的问题,自然而然地想到加锁,对获取实例方法加锁后,多线程情况下就可以保证线程安全。

public class Singleton02 {
    private static Singleton02 instance;
    private Singleton02() { }
    public static synchronized Singleton02 getInstance() {
        if (instance == null) {
            instance = new Singleton02();
        }
        return instance;
    }
}

  但上面的代码,虽然是线程安全,但是每一次使用getInstance()获取实例时,都必须加同步锁,加锁是很耗时的操作,在没有必要的时候我们应该尽量避免。

双重检验锁

  对于上面的效率不高的问题,我们可以使用双重检验锁(Double-checked locking)来进行解决,我们应该在实例还没有创建之前需要加锁操作,以保证只有一个实例,当实例创建之后,就不再需要做加锁操作,在此过程中,会两次检查instance==null,称为double checked,其中一次在同步块外,一次在同步块内,至于为什么在同步块内还要检验一次,是因为可能多个线程都进行if代码中,如不进行检验,就可能创建多个实例。

public class Singleton03 {
    private static Singleton03 instance;
    private Singleton03() { }
    public static synchronized Singleton03 getInstance() {
        if (instance == null) {         // Single check
            synchronized (Singleton03.class) {
                if (instance == null) {      // double check
                    instance = new Singleton03();
                }
            }
        }
        return instance;
    }
}

  上面的代码看上去非常完美,但是还存在问题,主要在于instance = new Singleton03();这一句,这个语句并不是一个原子操作,它可以分解为下面三步:

  1. 给对象引用instance分配内存 ;

  2. 调用Singleton03的构造函数来初始化对象成员变量;

    3. 将生成的实例对象的内存地址赋值给对象引用instance。

   然而上面的3个步骤并不是最终的执行顺序,JVM中即时编译器中存在指令重排序的优化,最后的执行顺序,可能是1-2-3,也可能是1-3-2,当为第二种情况时,线程一刚执行第3步,线程二直接返回instance,然后instance这个对象引用已经指向堆内存,但堆内存中却没有初始化,于是报错,对于这个问题将instance声明为volatile即可。

public class Singleton03 {
    private volatile static Singleton03 instance;
    private Singleton03() { }
    public static synchronized Singleton03 getInstance() {
        if (instance == null) {         // Single check
            synchronized (Singleton03.class) {
                if (instance == null) {      // double check
                    instance = new Singleton03();
                }
            }
        }
        return instance;
    }
}

饿汉式

  上面的单例模式都是使用懒加载的方式,可以称为懒汉式,这里介绍的是饿汉式,顾名思义,实例对象在使用之间就创建好了,直接用就可以了。

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

    public static Singleton04 getInstance() {
        return INSTANCE;
    }
}

  上面的代码中当classloader加载Singleton04类时就创建了该实例,之后可以直接使用,这样一定是线程安全的,但这样的话,我们将实例的创建委托给类加载器,可能与我们要求的行为可能不太一致,我们可能希望在使用getInstance方法才创建单例对象。

静态内部类

  为了解决上面的问题,我们可以使用静态内部类的方法,既可以在第一次使用getInstance时创建单例对象,又可以保证同步安全。

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

    public static Singleton05 getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

  上面的代码,SingletonHolder为私有静态类,只能通过getInstance访问,所以是懒加载的,同时获取实例时无需同步,没有性能上的损失。

枚举

  最简单的方式居然是用枚举,让人意想不到。

public enum Singleton06 {
    INSTANCE;
    private Singleton06() {
    }
    public static Singleton06 getInstance() {
        return INSTANCE;
    }
}

  使用枚举创建实例默认是线程安全的,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。上面的getInstance方法可以不要,可以直接使用Singleton06.INSTANCE进行使用。

总结

  单例模式的特点

    1、类的实例对象只有一个;

      2、可以通过类自身的方法创建实例对象 ;

      3、系统使用的对象为同一个对象。

  单例模式的使用方法小小的单例模式同样有很多种不同的写法,但根据我的经验推荐大家使用双重检验锁的方式,或直接使用饿汉式的方式,使用枚举的方式实际工作中感觉很少使用。

  单例模式的注意事项:单例模式它是有范围的,它只局限于类加载器(ClassLoader)中能保证实例唯一,当有不同的类加载器时,会出现不同的类加载器装载同一个类,从而产生多个实例,所以,应该尽量保证多个类加载器不会装载同一个单例类。

posted @ 2016-08-22 20:03  苍穹2018  阅读(464)  评论(6编辑  收藏  举报