注意
想法及时记录,实现可以待做。
简介
有时对于某些类来说,只有一个实例对某些类很重要。 我们只需要一个对象的一个实例,并且如果我们实例化超过一个,我们将遇到各种问题,例如不正确的程序行为、过度使用资源或不一致的结果。
我们只需要一个实例,比如:
- 应用上下文
- 缓存
- 线程池
- 输入输出的驱动
饿汉模式(线程安全)
public final class SingletonEager {
// 自行创建实例
private static final SingletonEager instance=new SingletonEager();
// 私有构造函数
private SingletonEager(){}
// 通过该函数向整个系统提供实例
public static SingletonEager getInstance(){
return instance;
}
}
该方式是以静态变量的形式提供的实例,它通过JVM来保证只会有一个实例被初始化。
这种方式的特点明显,在类的初始化阶段就进行了对象的生成,所以称之为饿汉模式。如果我们有大量的这种类的初始化,还未使用,就已经生成了对象,占用内存,这种是会占用资源的。
懒汉模式
public final class SingletonLazy {
// 与饿汉式相比,类初始化并不进行实例化
private static Singleton instance= null;
// 私有构造函数
private SingletonLazy(){}
// 通过该函数向整个系统提供实例
public static SingletonLazy getInstance(){
// 当instance为null时,则实例化对象,否则直接返回对象
if(null == instance){
// 真正进行实例化对象
instance = new SingletonLazy();
}
// 返回已存在的对象
return instance;
}
}
这种懒汉模式与上述饿汉模式相比,差异明显在于,只有调用的时候,才会进行初始化,否则不会提前进行实例化对象。这种有利于按需使用资源,不浪费资源。
但上述的方式,在单线程的情况下才是可行的,多线程是不安全的。试想多个线程同时在判断实例化,进而就会产生多个新的对象,导致实例化对象不是全局唯一。
synchronized的懒汉模式
public final class SingletonLazy {
// 与饿汉式相比,类初始化并不进行实例化
private static SingletonLazy instance= null;
// 私有构造函数
private SingletonLazy(){}
// 加同步锁,通过该函数向整个系统提供实例
public static synchronized SingletonLazy getInstance(){
// 当instance为null时,则实例化对象,否则直接返回对象
if(null == instance){
// 真正进行实例化对象
instance = new SingletonLazy();
}
// 返回已存在的对象
return instance;
}
}
这种加同步锁的方式确实可以保证线程安全,不过在方法上加锁,这种锁的粒度比较大,会造成锁竞争激烈,带来系统的性能问题。
Double-Check-Lock的懒汉模式
public final class SingletonLazy {
// 与饿汉式相比,类初始化并不进行实例化
private static SingletonLazy instance= null;
// 私有构造函数
private SingletonLazy(){}
// 通过该函数向整个系统提供实例,注意:这里没加同步锁
public static SingletonLazy getInstance(){
// 第一次判断,当instance为null时,则实例化对象,否则直接返回对象
if(null == instance){
// 加同步锁
synchronized (SingletonLazy.class){
// 第二次判断
if(null == instance){
// 真正进行实例化对象
instance = new SingletonLazy();
}
}
}
// 返回已存在的对象
return instance;
}
}
之所以要两次判断,是因为多线程环境中,第一次判断如果已经有多个线程进入,那么同步锁这一步,只是让多线程同步进入实例化的过程,最终还是会有多个实例。
第二次判断,就可以完全避免其他的线程获取到锁,从而进行实例化对象。
而且这种同步锁的粒度远低于方法上的同步,可以同时被大量的线程使用,而不会出现抢锁激烈的情况。
那这么看来,目前这这种方案是比较合适的吗?仔细看看,不一定,由于JVM会有指令重排发生,也就是说我们希望的代码执行顺序在JVM层面执行会存在变化,那么这种方式的实例化对象,也是不安全的。
于是,给实例变量使用关键字,保证线程间的可见性 volatile,也可以避免被指令重拍,让JVM按照我们希望的执行顺序去执行。
volatile+Double-Check-Lock的懒汉模式
public final class SingletonLazy {
// 与饿汉式相比,类初始化并不进行实例化
private volatile static SingletonLazy instance= null;
// 私有构造函数
private SingletonLazy(){}
// 通过该函数向整个系统提供实例,注意:这里没加同步锁
public static SingletonLazy getInstance(){
// 第一次判断,当instance为null时,则实例化对象,否则直接返回对象
if(null == instance){
// 加同步锁
synchronized (SingletonLazy.class){
// 第二次判断
if(null == instance){
// 真正进行实例化对象
instance = new SingletonLazy();
}
}
}
// 返回已存在的对象
return instance;
}
}
这种写法,相比较而言已经安全了许多,但是也不是完全可靠。还有一些方式可以破坏这种实例化对象。
- 序列化
- clone
- 反射
- 多个类加载器加载
不过有相应的方式可以一一解决这个问题,参考以下方式:
// 方式反射实例化对象,这里繁盛调用,会抛出异常,防止多次实例化对象
private Singleton() {
if (sc != null) {
throw new IllegalStateException("Already created.");
}
}
// 这里防止序列化对生成新的对象
private Object readResolve() throws ObjectStreamException {
return sc;
}
private Object writeReplace() throws ObjectStreamException {
return sc;
}
// 覆写clone方法,防止生成新的对象
public Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException("Singleton, cannot be clonned");
}
// 防止不同的类加载器导致生成新的对象
private static Class getClass(String classname) throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if (classLoader == null)
classLoader = Singleton.class.getClassLoader();
return (classLoader.loadClass(classname));
}
静态内部类的懒汉模式
当调用方法的时候才会创建静态内部类的实例,从而保证了只会实例化一次静态变量,达到全局唯一。
public final class SingletonLazy {
// 私有构造函数
private SingletonLazy(){}
// 静态内部类实现
public static class InnerSingleton {
// 自行创建实例
private static SingletonLazy instance=new SingletonLazy();
}
// 通过该函数向整个系统提供实例
public static SingletonLazy getInstance() {
// 返回内部类中的静态变量
return InnerSingleton.instance;
}
}
枚举类的单例模式(推荐使用)
这种实现方式借助于枚举类,实现方式简单可靠,它还有以下优势:
- Singleton实例是线程安全的,因为JVM保证以线程安全的方式创建枚举实例。
- JVM 还保证在 Singleton 类实现 Serializable 时保持 Singleton 状态,这在没有 Enum 的情况下仍然可以使用 readResolve() 方法,但繁琐而复杂。
public enum SingletonLazy{
SINLETON;
public void doSomething(){
}
}
总结
一般情况下,建议使用饿汉方式。只有在要明确实现懒加载效果时,才会使用静态内部类方式。如果涉及到反序列化创建对象时,可以尝试使用枚举方式。如果有其他特殊的需求,可以考虑使用双检锁方式。