在上次的Android项目中,数据库对象以及netty连接的对象都使用过单例模式,但是对单例模式一直也是一知半解,借着刚在软件工程导论课上讲了单例模式,深入了解一下。
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
所以在单例模式中,最重要的一点就是保证其不会被实例化多次,保证其唯一性。
单例模式在Java中主要有以下几个实现方式:
懒汉式,线程不安全饿汉式,线程安全双重校验锁(DCL)静态内部类枚举此方式是懒加载的,也就是说,只有当调用getInstance()方法且当前类从未被实例化过,当前类才会被实例化,另外,此方法不适用于多线程,是线程不安全的。
将getInstance方法改为同步方法就可以保证线程安全了,但是由于同步代码的执行效率是很低的,所以,此方法不是很推荐。
此方法在类初始化的时候就完成了实例化,线程不安全的问题只会在首次实例化对象的时候才会发生,所以这种方式并不会产生线程不安全的问题。
JDK1.5以后可以使用
//DCL(线程安全) public class Singleton { private volatile static Singleton singleton; private Singleton() { } public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }此方法有两个需要注意的地方:
双重检查,相对于懒汉式(线程安全)方式,DCL方式在同步代码块外面又加了一个判断,这样既保证了线程安全,而且保证了只有在单例未被实例化的时候synchronized才会生效,保证了效率
volatile关键字的使用。可以看到上面基本上就可以保证线程安全了,但是为什么还要使用volatile关键字呢?在volatile关键字中,介绍过volatile关键字可以保证变量的有序性和可见性,在此处,使用volatile关键字就是为了保证有序性。
今天上软件工程导论的时候,老师讲到了单例模式,并且举到了DCL的例子,但是只字不提volatile关键字…
导致可能保证不了有序性的操作是下面的一个语句:
singleton = new Singleton();上面的一句话并不是一个原子操作,它可以被分解成以下几个语句:
memory = allocate(); //1:分配对象的内存空间 initInstance(memory); //2:初始化对象 singleton = memory; //3:设置singleton指向刚分配的内存地址可以看出上面的三个操作中,操作2是依赖于操作1的,而操作3并不依赖于操作2,因此,JVM就有可能对其进行重排序操作,重排序之后,上面的操作会变成下面的样子:
memory = allocate(); //1:分配对象的内存空间 singleton = memory; //3:设置singleton指向刚分配的内存地址(此时的对象还没有被初始化) initInstance(memory); //2:初始化对象可以看到,如果在线程A中经过重排序之后,操作3和操作2对调了,这样在初始化对象之前,instance首先指向了一个未被初始化的内存空间,如果这时有另一个线程B访问,此时的singleton已经不是null了,会返回一个单例,这样就有可能得到”半个”单例(未完成初始化)。如下表:
线程A线程B分配对象的内存空间singleton 指向一个未被初始化的内存空间判断 singleton 是否为null由于singleton不为null,访问引用对象初始化对象访问引用对象加上volatile关键字就可以防止JVM对其进行重排序了,就可以解决这种隐患。
关于内部类可以看此博客Java内部类。
通过静态内部类实现,既可以实现懒汉式的懒加载又可以实现饿汉式的线程安全。
此方法同样是使用了类加载机制保证初始化单例的只有一个线程,但是此方式在Singleton类被初始化之后,instance并不会被初始化,只有当显式地调用getInstance方法之后,才会实例化instance。所以此方法可以保证懒加载。
JDK1.5以后可以使用
public enum Singleton { INSTANCE; public void whateverMethod() { } }这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。 而且也不能通过反射来调用私有的构造方法。
在一般情况下,使用饿汉式就可以了,如果明确要求懒加载,可以使用静态内部类的方法,了解DCL可以帮助我们更好的理解synchronized关键字以及volatile关键字的作用,最后一种枚举类方式虽然用的少,但是还是很推荐使用该方式(毕竟是Effective Java的作者推荐的写法,听大神说的就对了?)。