设计模式——单例模式(Singleton Pattern)

确保一个类在任何情况下都只有一个实例被返回,并提供一个全局访问点。

隐藏其所有构造方法,所有构造方法私有。属于创建型模式。

ServletContext、ServletConfig、ApplicationContext、DBPool

优点:

  • 在内存中只有一个实例,减少内存开销。
  • 避免对资源的多重占用。
  • 设置了全局访问点,控制了访问。

缺点:没有接口,扩展困难,违背了开闭原则。

注意事项:

  • 私有化构造器
  • 保证线程安全
  • 延迟加载
  • 防止序列化和反序列化破坏单例
  • 防御反射攻击破坏单例

1. 饿汉式单例

类加载时就已经被初始化。

优点:执行效率高,性能高,没有任何的锁。

缺点:在不使用的情况下会造成内存的浪费。

public class HungrySingleton {
    // 类加载的时候已经初始化,会占用内存空间,如果不使用,就会浪费内存
    private static HungrySingleton hungrySingleton = new HungrySingleton();
    private HungrySingleton() {}
    public HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

2. 懒汉式单例

第一次调用时才初始化。

2.1 简单懒汉式 + synchronized

public class LazySingleton {
    // 加volatile避免指令重排序问题
    private volatile static LazySingleton lazySingleton;
    private LazySingleton() {}
    // synchronized加锁避免第一次访问时两个线程同时来创建实例
    // 但是方法锁会影响性能,后期每次获取实例都只能一次一个线程访问
    public synchronized LazySingleton getInstance() {
        if (null == lazySingleton) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

优点:节省内存资源

缺点:加锁,会有性能问题

注意:如果不加synchronized关键字,会有线程安全问题,多个线程同时访问有两种运行结果:

a. 同一个实例:正常顺序执行,但是后者结果覆盖了前者,从而打印。所以看着打印结果实例相同,实际上是两个实例。

b. 不同实例:同时进入条件,按顺序执行返回

2.2 DCL懒汉式 + volatile

对懒汉式单例进行优化:Double Check Lock

优点:调用的时候才初始化,如果不使用不占用内存空间,避免造成浪费。性能提升了,线程安全了。

缺点:可读性差了,两个if判断,代码不够优雅。

public class LazyDoubleCheckSingleton {
    // 加volatile避免指令重排序问题
    private volatile static LazyDoubleCheckSingleton lazySingleton;
    private LazyDoubleCheckSingleton() {}
    // synchronized加锁避免第一次访问时两个线程同时来创建实例
    public static LazyDoubleCheckSingleton getInstance() {
        if (null == lazySingleton) {//检查是否需要阻塞进入锁
            synchronized(LazyDoubleCheckSingleton.class) {
                if (null == lazySingleton) {//检查是否已经创建实例
                    // 这里可能出现指令重排序问题,所以在声明地方加上volatile关键字
                    lazySingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazySingleton;
    }
}

2.3 懒汉静态内部类

通过内部类创建实例,然后调用内部类的静态变量获取实例。静态成员变量:在类加载的时候就会分配内存空间;静态内部类:在使用的时候才会分配空间。

优点:利用Java本身语法,通过内部类,性能高,避免内存浪费。

缺点:不优雅。

public class LazyStaticInnerClassSingleton {
    private LazyStaticInnerClassSingleton() {
        // 避免通过反射暴力获取对象实例
        if (null != LazyHelper.INSTANCE) {
            throw new RuntimeException("不允许通过反射获取实例");
        }
    }
    public LazyStaticInnerClassSingleton getInstance() {
        return LazyHelper.INSTANCE;
    }
    private static class LazyHelper {
        private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
    }
}

3. 注册式单例

将每一个实例都缓存到统一的容器中,然后通过唯一的标识来获取实例。

3.1 枚举式单例

枚举式单例在类加载时就已经将实例放在了一个Map类型的枚举常量字典enumConstantDirectory

private volatile transient Map<String, T> enumConstantDirectory = null;

优点:简单优雅,线程安全,不会被反射破坏(JDK底层已经针对反射做了处理),

缺点:某些情况下可能造成内存浪费,声明的时候已经将实例放在了内存中。不适合在大量使用单例的情况下使用此方式。

public enum EnumSingleton {
    INSTANCE;
    public static final EnumSingleton getInstance(){
        return INSTANCE;
    }
}

3.2 容器式单例

针对以上枚举式单例的问题, Spring做出了改良,提出了容器式单例。

public class ContainerSingleton {
    private ContainerSingleton(){}
    private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();
    public static Object getInstance(String className) {
        Object instance = null;
        if (!ioc.containsKey(className)) {
            // 通过DCL双重检查锁解决线程安全问题
            synchronized (ioc) {
                if (!ioc.containsKey(className)) {
                    try {
                        instance = Class.forName(className);
                        ioc.put(className, instance);
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return instance = ioc.get(className);
    }
}

3.3 序列化问题

在序列化、反序列化过程中,也会破坏单例。

序列化、反序列化问题:首先得到一个类的instance实例,然后对其进行序列化存储到磁盘,然后再反序列化得到对象,比较反序列化后的对象地址和原始实例地址,发现内存地址不同,这就是序列化、反序列化破坏单例的问题。

解决:在单例中添加一个私有readResolve方法即可解决序列化反序列化问题。

private Object readResolve() {return INSTANCE;}

public class SerializableSingleton implements Serializable {
    private static final SerializableSingleton INSTANCE = new SerializableSingleton();
    private SerializableSingleton() {}
    public static SerializableSingleton getInstance() {
        return INSTANCE;
    }
    // 解决序列化、反序列化问题
    private Object readResolve() {return INSTANCE;}
}

4. ThreadLocal单例

保证同一个线程内部全局唯一,且天生线程安全。不同线程中实例不同。

public class ThreadLocalSingleton {
    private static final ThreadLocal<ThreadLocalSingleton> threadLocalSingleton = new ThreadLocal<ThreadLocalSingleton>() {
        @Override
        protected ThreadLocalSingleton initialValue() {
            return new ThreadLocalSingleton();
        }
    };
    private ThreadLocalSingleton(){}
    public static ThreadLocalSingleton getInstance() {
        return threadLocalSingleton.get();
    }
}

面试题

如何选择用哪种单例?

如果程序不复杂,单例对象不多,推荐饿汉式单例

如果经常发生多线程并发情况,推荐静态内部类和枚举式单例

哪些情况会破坏单例?

  • 多线程情况下线程安全问题
    解决方案:DCL双重检查锁,静态内部类单例
  • JVM指令重排序
    解决方案:在单例对象上添加voilate关键字
  • 深克隆破坏单例
    解决方案:重写克隆方法,只返回当前实例
  • 反射攻击破坏单例
    解决方案:在构造方法检查单例对象,如果已创建则抛异常
    将单例的实现方式改为枚举式单例
  • 反序列化破坏单例
    解决方案:重写readResolve方法,将返回值设置为单例

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值