【设计模式】单例模式

单例模式

所谓的单例模式,就是在任何情形下一个类都只能获取到一个实例,并且提供一个全局的访问点。

饿汉式单例模式

饿汉式单例模式在类加载之初就会进行初始化,所以天生是线程安全的。
缺点 :一开始就初始化所有实例, 浪费内存

public class HungrySingleton {

    private static final HungrySingleton singleton = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance(){
        return singleton;
    }

}

还可以使用静态代码块来实现

懒汉式单例模式

优化了饿汉式单例模浪费内存的缺点,只有在使用到的时候才会进行初始化

synchroized线程安全

为了保证线程安全,对于getInstance方法加了synchronized 关键字

public class SimpleLazySingleton {

    private static SimpleLazySingleton singleton = null;

    private SimpleLazySingleton() {}
    
    public synchronized static SimpleLazySingleton getInstance() {
        if (singleton == null) {
            singleton = new SimpleLazySingleton();
        }
        return singleton;
    }
}

缺点: 由于对整个getInstance()方法都加了synchronized 关键字,当大量线程访问该方法的时候,会导致锁竞争,大量线程发生阻塞而影响性能

双重校验 double check
public class DoubleCheckSingleton {

    private static volatile DoubleCheckSingleton singleton = null;

    private DoubleCheckSingleton() {}

    public static DoubleCheckSingleton getInstance() {
        if (singleton == null) {
            synchronized (DoubleCheckSingleton.class) {
                if (singleton == null) {
                    //因为存在指令重排序的可能,需要加volatile关键字
                    //1) memory = allocate();   分配对象的内存空间指令
                    //2) ctorInstance(memory);   初始化对象
                    //3) instance = memory;     将已分配存地址赋值给对象引用
                    singleton = new DoubleCheckSingleton();
                }
            }
        }
        return singleton;
    }
}

缺点: 虽然发生锁竞争的可能性大大降低,但是还是会存在锁竞争

指令重排之后,instance指向分配好的内存(步骤3)放在了前面,而这段内存的初始化的指令被排在了后面,在线程 T1 初始化完成这段内存之前,线程T2 虽然进不去同步代码块,但是在同步代码块之前的判断就会发现 instance 不为空,此时线程T2 获得 instance 对象(此时这个对象是没有完成初始化的),如果直接使用就可能发生错误。

内部类

解决了懒汉式单例线程安全和饿汉式单例内存浪费的缺点

public class LazyInnerSingleton {

    private LazyInnerSingleton() {
    }

    public static final LazyInnerSingleton getInstance(){
        //在调用getInstance的时候才会加载LazyHolder
        return LazyHolder.singleton;
    }

    //在加载LazyInnerSingleton.class的时候 默认不加载
    private static class LazyHolder{
        private static final LazyInnerSingleton singleton = new LazyInnerSingleton();
    }
}

反射和序列化破坏单例

上述几种写法都是可以通过反射破坏单例,为了解决这个问题,在构造方法里加一个限制

public class LazyInnerSingleton {

    private LazyInnerSingleton() {
        if (LazyHolder.singleton != null) {
            throw new RuntimeException("不允许构建多个实例");
        }
    }

    public static final LazyInnerSingleton getInstance(){
        //在调用getInstance的时候才会加载LazyHolder
        return LazyHolder.singleton;
    }

    //在加载LazyInnerSingleton.class的时候 默认不加载
    private static class LazyHolder{
        private static LazyInnerSingleton singleton = new LazyInnerSingleton();
    }
}

除此之外,序列化也可以破坏单例,可以通过增加一个readResolve来解决这个问题(虽然解决了返回两个对象的问题,但是实际在代码执行过程中还是初始化了两次,只是最终返回的实例是一个)

public class SerializableSingleton implements Serializable {

    private static SerializableSingleton singleton = new SerializableSingleton();

    private SerializableSingleton() {}

    public SerializableSingleton getInstance() {
        return singleton;
    }

    public Object readResolve() {
        return singleton;
    }
}

可以看下OIS的源码

private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }

        //...
        Object obj;
        try {
            //判断是否有无参的构造方法,如果有则进行初始化
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
        }

        //...

        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {   
            //判断是否有无参的readResovle方法,有的话则通过反射调用
            Object rep = desc.invokeReadResolve(obj);
            //...
        }

        return obj;
    }

注册式单例

为了解决上面单例模式被序列化破坏的方式,出现了注册式单例

1. 枚举式
public enum EnumSingleton {
    SINGLETON;
    
    public static EnumSingleton getInstance() {
        return SINGLETON;
    }
}

通过java反编译工具JAD(http://www.javadecompilers.com/jad)反编译EnumSingleton, 查看EnumSingleton.jad可以看到有如下代码:

    static 
    {
        SINGLETON = new EnumSingleton("SINGLETON", 0);
        $VALUES = (new EnumSingleton[] {
            SINGLETON
        });
    }

枚举式单例是在静态代码块中对实例进行初始化,是饿汉式单例的实现。
不过枚举类天生不能被序列化和反射破坏,可以从源码看出

 /**
     * Reads in and returns enum constant, or null if enum type is
     * unresolvable.  Sets passHandle to enum constant's assigned handle.
     */
    private Enum<?> readEnum(boolean unshared) throws IOException {
        //...
        int enumHandle = handles.assign(unshared ? unsharedMarker : null);
        //...
        String name = readString(false);
        Enum<?> result = null;
        Class<?> cl = desc.forClass();
        if (cl != null) {
            try {
                @SuppressWarnings("unchecked")
                //通过类对象类和类名找到唯一的枚举实例
                Enum<?> en = Enum.valueOf((Class)cl, name);
                result = en;
            } catch (IllegalArgumentException ex) {
            }
            if (!unshared) {
                handles.setObject(enumHandle, result);
            }
        }

        //...
        return result;
    }

而Enum是没有无参构造方法的,只有一个protected 的有参构造方法

    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

可以看到源码里是禁止对修饰符为Enum的枚举类型进行实例化,会直接抛出Cannot reflectively create enum objects 异常

 public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        //对于enum类禁止通过反射实例化
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

优点: 不会被反射和序列化破坏,线程安全
缺点: 会在类加载之初就完成实例化,不适合大量使用

2.容器式

类似spring的容器写法

public class ContainerSingleton {

    //线程安全
    private static Map<String, Object> beanMap = new ConcurrentHashMap<String, Object>();

    private ContainerSingleton() {}
    
    public Object getBean(String name) {
        synchronized (beanMap) {
            if (!beanMap.containsKey(name)) {
                try {
                    Object object = Class.forName(name).newInstance();
                    beanMap.put(name, object);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return beanMap.get(name);
        }
    }
}

容器式单例适合需要大量创建单例的场景,使用了synchronized 保证线程安全,但是多个线程访问同一个对象的时候,会出现锁竞争,影响性能

还可以使用ThreadLocal来实现一个线程内的单例(保证其在单个线程中唯一,天生线程安全). synchronized 通过加锁保证线程安全,以时间换空间,而ThreadLocal将所有对象存储在ThreadLocalMap中,给每个线程提供一个对象,以空间换时间来实现线程隔离

总结:

个人更倾向于内部类实现方式的懒汉式单例模式,虽然可以被反射和序列化破坏单例,但实际工作中也没人故意这么操作去破坏单例

实际使用过程中饿汉式单例也没有什么不好的,它的缺点在于不能延迟加载,但是对于一个肯定要使用的到的资源(没有程序使用我们也不会去创建这个单例),在启动的时候初始化反而比在程序运行过程中去初始化更好(如果资源的创建耗时,反而会影响接口响应)。

单例模式虽然使用起来很方便,不过也存在一些问题:

  • 单例会隐藏类之间的依赖关系
  • 对代码的扩展性不友好
  • 对代码的可测试性不友好
    是否要使用单例模式要看你的使用场景,也可以通过工厂模式,spring ioc容器等来保证全局唯一
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值