简述
单例模式表示在内存中只有一个实例,多次使用该类的对象时,使用的都是同一个对象。单例模式可以避免一个全局使用的类被频繁地创建和销毁。
单例模式需要将构造函数私有化(避免外部使用构造函数创建对象),并为单例对象提供一个全局的访问点。
几种实现方式
以下只考虑线程安全的实现方式,线程不安全的不被认为是单例模式。
[1]懒汉式
懒汉式即指在第一次使用时才创建对象。
public class SingletonClass {
//组合一个自己的对象引用,private保护其不被直接访问修改,static保护本类唯一
private static SingletonClass singletonClass;
//构造函数私有化,使不能从外部创建本类对象
private SingletonClass() {
}
//获得单例实例的静态方法作外部访问点
//用synchronized修饰为同步,才能确保多线程环境下只创建这一个对象
public static synchronized SingletonClass getInstance() {
return null == singletonClass ? singletonClass = new SingletonClass() : singletonClass;
}
}
在这种方式下,因为将外部访问点整个设置为同步的,所以多线程环境下工作效率很低。当getInstance()
操作的同步对整个系统的性能不是很关键时,如需要避免饿汉创建该对象造成的内存浪费,不妨使用这种方式。
[2]饿汉式
饿汉式是在类加载时才创建这个对象,但应注意饿汉式不一定懒加载。可以将懒加载视为在第一次调用getInstance()
方法时创建这个对象,懒汉式是能保证懒加载的,饿汉式不能。
public class SingletonClass {
//在类加载时创建对象,饿汉式
private static SingletonClass singletonClass=new SingletonClass();
//构造函数私有化,使不能从外部创建本类对象
private SingletonClass() {
}
//调用到此方法时类一定已经加载过了,直接返回
//不需要synchronized同步
public static SingletonClass getInstance() {
return singletonClass;
}
}
在这种方式中,巧妙利用了Java的类装载过程来保证了线程安全,因为这个类只加载一次,所以这个对象一定是唯一的。并且因为没有synchronized
对访问点的限制,这种方式的访问效率比较高。
除了要注意内存浪费之外,还应注意到“类装载时”不一定是”第一次调用访问点时”,因为类中还可能存在其它的static方法在此前调用导致对象被创建,所以饿汉式不能保证懒加载,可能早在调用其它静态方法时就把这个对象创建好了。
[3]双检锁方式
使用双重校验锁(Double Checked Locking),可以结合懒汉式和饿汉式的优点。既不浪费内存(做到懒加载),又不至于让外部访问点性能下降太多。
public class SingletonClass {
//volatile保证有线程对该变量修改时,另一个线程中该变量的缓存行无效,读取时直接到内存读
//总之,若一个线程修改了某个变量的值,新值对其他线程来说是立即可见的,在访问点内检查时要用到
private volatile static SingletonClass singletonClass;
//构造函数私有化,使不能从外部创建本类对象
private SingletonClass() {
}
//使用DCL锁保证线程安全,不需要对整个方法synchronized同步
public static SingletonClass getInstance() {
//如果该线程发现该对象未创建
if (null == singletonClass) {
//那么首先要和其它线程竞争本类的锁
synchronized (SingletonClass.class) {
//获得锁以后,才能执行这部分代码
//这时再次检查是否为null
//如果还是null,说明自己是第一个竞争到锁的,本线程负责创建对象
if (null == singletonClass)
singletonClass = new SingletonClass();
//如果不是null了,说明自己这份锁已经是别人用过,创建好对象以后释放出来的
//这时对象已经被创建过了,本线程什么都不用做,直接释放锁即可
}
}
//至此,对象一定唯一地创建过了,直接返回
return singletonClass;
}
}
这种方式最复杂,性能损失较小但也并非最好,其优势在于在创建对象时能做的事情非常多。
[4]登记式
登记式是用一个线程安全的容器(网上很多登记式都用HashMap
,这是线程不安全的,用ConcurrentHashMap
才是正确的选择)来对要单例化的实例进行登记,当使用时直接从这个容器中取出即可。
可以单独设置一个类来管理要登记的单例对象,也可以为单例对象类自己设置登记容器(见线程安全的登记式单例)。下面演示一下前者,即设置一个单独的类来管理登记。
//单例管理类
public class SingletonManager {
//线程安全的容器,饿汉式保证容器对象本身为单例
private static Map map = new ConcurrentHashMap();
//外部访问点,传入类名,返回该类的单例对象.该类会被登记进入上面的容器进行单例管理
//在类中务必保证构造方法私有化,对这一点这个管理类是无法控制的,需要自己保证
public static Object getInstance(String className) {
//如果还没登记到容器
if (!map.containsKey(className)) {
//用反射的方式创建对象(因为已经构造函数私有化),并登记到容器中
try {
map.put(className, Class.forName(className).newInstance());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
//从容器中获取管理的单例对象并返回
return map.get(className);
}
}
可以看到,在外部管理类登记是需要用反射来创建对象的,因为构造方法已经私有化了,而在内部登记自己则不用。
[5]静态内部类
即使用静态内部类来实现,这种方式是对饿汉式的改进。既然饿汉式因为在类加载时就会创建对象,从而导致不能保证懒加载,那么不妨增加一个静态内部类,并且只在访问点方法中调用这个静态内部类,如此在这个静态内部类加载时创建对象就不会受到其它静态方法的影响了。
public class SingletonClass {
//私有的静态内部类,只有这个类内才能访问
private static class SingletonHolde {
//该对象不会在外部类加载时便创建,避免受外部类其它静态方法影响
private static SingletonClass singletonClass = new SingletonClass();
}
//构造函数私有化,使不能从外部创建本类对象
private SingletonClass() {
}
//外部访问点
public static SingletonClass getInstance() {
//第一次引用内部类时才会加载创建这个对象,而只要保证此必在这方法内,就做到了懒加载
return SingletonHolde.singletonClass;
}
}
一般只有在明确要求懒加载时,才使用这种方式。
[6]枚举
枚举的构造函数本身就是私有的,而且可以自由序列化、线程安全、保证单例。使用枚举是实现单例模式的最佳方式。以前对枚举不太了解,实际上枚举就是一个final类,也一样可以有其它属性和方法,当成普通类来用实现单例极为简便。
public enum SingletonEnum {
INSTANCE;//枚举对象天然就是单例
//枚举类也一样可以有其它属性
private int id = 2018;
//枚举类也一样可以有其它方法
public void sayId() {
System.out.println("id是" + id);
}
}
//测试一下
class Main {
public static void main(String[] args) {
SingletonEnum.INSTANCE.sayId();
System.out.println(SingletonEnum.INSTANCE == SingletonEnum.INSTANCE);
}
}
输出:
id是2018
true
参考阅读
[2] 单例模式以及双检锁DCL对DCL讲的比较清楚。
[4] 单例模式-runoob
[5] 深入理解Java枚举类型(enum)写得极好,有空好好学习一下。