每天一个设计模式(一)单例模式

1,236 阅读4分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

在设计模式中,单例模式往往被认为是最为简单及最好被理解的设计模式之一,但是要想深入理解以及能够正确的去使用还要有很多注意的点。

定义

单例模式提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。 所以当我们在代码中涉及到全局只允许存在一个实例并且频繁的被访问的时候就要考虑单例模式了。比如我们常常涉及到的用户信息管理,为了保证用户信息同步以及它频繁被调用的特点,就需要使用单例模式来创建。那么单例如何写呢?简单说就是:判断系统中是否已经存在这个实例,如果存在则直接返回,如果不存在则创建并返回,并将自己的构造方法进行私有化,保证外部无法直接实例化。

单例模式的几种写法

饿汉式

饿汉式效率比较高,但是由于classloader机制,UserManager类在进行类装载时就会被实例化,而如果不需要访问单例时就产生了内存浪费的问题。

public class UserManager {
    private UserManager(){}
    private static UserManager mInstance = new UserManager();
    public static UserManager getInstance() {
        return mInstance;
    }
}
懒汉式

所谓懒汉式就是在暴露对外获取单例的方法中进行实例的初始化,在懒汉式的写法中,又分为同步锁写法和非同步锁写法:

  1. 不加同步锁,这种写法由于没有加同步锁,所以在多线程下是存在线程安全问题的,可能会存在多个实例的情况,那么严格意义上是不能被称为单例的。
public class UserManager {
    private UserManager(){}
    private static UserManager mInstance;
    public static UserManager getInstance() {
        if (mInstance == null){
            mInstance = new UserManager();
        }
        return mInstance;
    }
}

2 加同步锁,这种方法虽然可以比较好的解决多线程多次创建实例的问题,但是效率很低,因为实际上只有第一次判空时需要加锁,只要被初始化一次后是不需要进行同步锁机制的。

public class UserManager {
    private UserManager(){}
    private static UserManager mInstance;
    public static synchronized UserManager getInstance() {
        if (mInstance == null){
            mInstance = new UserManager();
        }
        return mInstance;
    }
}
双重检验锁校验方式

这种方式是我们最为常用的单例写法,同时考虑了线程安全以及效率的问题。其实,称此方式为双重检验锁可能会容易产生误导,会认为是两个锁,个人感觉称之为“双重判空”或者“双重校验+锁”更为合适,总之理解其写法就好。在JVM中mInstance = new UserManager()会被拆分成3个单元操作:

  1. 为实力分配内存地址。
  2. 创建实例对象。
  3. 将创建的对象指定到刚刚分配的内存地址(进行到此操作后对象才不为空)。 在不加双重判空的情况下,如果只有一个判空操作,那么将会存在这样的场景:一个线程进入到方法体此时mInstance为空,那么对其进行实例化,当执行到上述1或2单元操作时,另外的线程也进入到方法体,但由于第一个线程的3操作还未完成,此时mInstance依然为空,所以后者的线程还会进行一次初始化,当两次线程全部完成访问后就会创建两个实例。
public class UserManager {
    private UserManager(){}
    private static volatile UserManager mInstance;
    public static UserManager getInstance() {
        if (mInstance == null){
            synchronized (UserManager.class){
                if (mInstance == null){
                    mInstance = new UserManager();
                }
            }
        }
        return mInstance;
    }
}
静态内部类方式

由于类加载机制中,即使UserManger类被装载,其内部类不会被初始化。只有在调用内部类方法时才会初始化,从而达到延迟加载(lazy load)的效果。很多人都推荐这种写法。

public class UserManager {
    private UserManager(){}
    static class User{
        private static final UserManager USER_MANAGER = new UserManager();
    }
    public static UserManager getInstance() {
        return User.USER_MANAGER;
    }
}
枚举

这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。不能通过 reflection attack 来调用私有构造方法。

public enum UserManager{
    USER_INSTANCE;
}

总结

  • 单例类全局中只能存在一个实例。
  • 单例类必须将构造方法私有化,保证无法被外界直接实例化。
  • 单例必须自己创造自己唯一的实例,并且对外提供获取单例的方法。