设计模式之单例Singleton

内容目标

        了解单例的各种实现方式


目录


单例模式的定义

        官方定义:单例是指确保一个类在任何情况下都绝对只有一个实例并提供一个全局访问点。

        补充:设计单例时,需要隐藏其所有的构造方法。并且单例是属于创建型模式。(后续会补充一篇文章专门描述创建型、行为型。)

单例模式的使用场景

        在开发过程,我们会经常使用到一些单例的类,比如

ServletContext、ServletConfig、ApplicationContext、DBPool等等,

这些类都确保了在任何情况下都绝对只有一个实例

 单例模式的常见写法

饿汉式单例

        饿汉式单例是在类首次加载的时候就会创建单例了

优点:

  1. 执行效率高。
  2. 线程安全,不需要加锁。

缺点:

  1. 资源浪费,单个情况下还好,如果存在成千上万个单例对象,那么全部使用饿汉式就非常浪费资源。
// # v1
public class Singleton
{
    private static final Singleton singleton = new Singleton();

    private Singleton(){}

    public static Singleton getInstance(){return singleton ;}
}

// # v2
public class Singleton
{
    //类加载顺序
    //先静态后动态
    //先上,后下
    //先属性后方法
    private static final Singleton singleton;

    //与构造方法没什么区别,只能装逼
    static {
        singleton= new Singleton();
    }

    private Singleton(){}

    public static Singleton getInstance(){return singleton;}
}

懒汉式单例

        懒汉式是在被外部类调用时才创建实例

 简单懒汉式

  • 优点:节省了内存
  • 缺点:线程不安全,原因是并发情况下,在第一个线程判断完,进去初始化的时候,第二个线程进来了,因为第一个线程还没开始初始化,所有instance还是空的,所以也会进去初始化,那么就会存在多个对象了。(破坏单例了
  • 解决方法:在方法上加上关键字synchronized,当出现多个线程并发的时候,需要第一个线程返回值后,第二个线程才能进行。即线程阻塞了Monitor状态
  • synchronized关键字控制资源不被同时占用,保证资源只能被一个线程占用。线程安全了
  • 随之问题又出现了,加了synchronized关键字,带来了新的缺点,性能较差。
public class Singleton
{
    private static Singleton instance;

    private Singleton(){}

    // 关键字synchronized 
    public static synchronized Singleton getInstance(){
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

双重检查懒汉式

  •  优点:线程安全、性能提高。只有在第一次为空的时候才会阻塞,后续都不阻塞。
  • 变量必须加上volatile关键字,保证代码的执行顺序。原因是在多线程环境下,代码执行会存在一定的随机性,会不断的去抢CPU的时间片的。
  • 补充:在执行的时候,首先会分配对象的内存地址,还会分配变量的内存地址,在赋值的时候,需要将变量的指针指向对象的内存地址。这个过程会存在创建两个内存地址,那么就会存在先后顺序问题,会出现给变量instance分配地址、后在给对象再地址,那么就会存在线程混乱的问题,所以必须要加上volatile关键字,保证代码的执行顺序
  • 缺点:代码可读性差,难度大。不够优雅
public class LazyDuobleCheckSingleton
{
    private static volatile LazyDuobleCheckSingleton instance;

    private LazyDuobleCheckSingleton(){}

    public static LazyDuobleCheckSingleton getInstance(){
        //检查是否需要阻塞
        if (instance == null) {
            synchronized (LazyDuobleCheckSingleton.class){
                //检查是否要重新创建实例
                if (instance == null){
                    instance = new LazyDuobleCheckSingleton();
                }
            }

        }
        return instance;
    }
}

静态内部类方式

  • 执行原理:假设类名为LazyStaticInnerClassSingleton,java编译后的class文件是 LazyStaticInnerClassSingleton.class。内部类是  LazyStaticInnerClassSingleton$LazyHolder.class,当程序调用LazyHolder.INSTANCE的时候,才会去调用加载内部类LazyHolder
  • 优点:写法优雅、很好的利用的Java本身的语法特点,性能高、避免内存浪费。
  • 缺点:能够被反射破坏。
  • 解决方法在父类上加上LazyHolder.INSTANCE != null的时候就抛出异常,防止反射破坏,但是加了判断后又带来了新的问题,代码又变成不过优雅了,可读性差。
public class LazyStaticInnerClassSingleton {

    private LazyStaticInnerClassSingleton(){

        //解决反射破坏单例的问题,但是代码随之又变成了不优雅了
        if (LazyHolder.INSTANCE != null){
            throw new RuntimeException("不允许非法访问");
        }
    }

    private static LazyStaticInnerClassSingleton getInstance(){
        return LazyHolder.INSTANCE;
    }

    //静态内部类只有在调用的时候,才会进行初始化。所以是懒汉式
    private static class LazyHolder{
        private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
    }
}

注册式单例

        将每一个实例都缓存到统一的容器中,使用唯一标识获取实例。

枚举类单例

  • enum类相当于继承 Enum抽象类.
  • 优点:代码优雅、线程安全、性能较高。还可以防止反射破坏,(为什么可以防止,大家可以私聊我,我专门为你解答)。
  • 缺点:会存在资源浪费,单个对象情况下还好,如果存在成千上万个对象。那么全部使用枚举也会造成浪费资源,以及反序列化破坏单例的问题
public enum EnumSingleton
{
    INSTANCE;

    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

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

 容器类单例

  • 优点:性能较好、避免内存浪费
  • 缺点:线程安全问题,以及反序列化破坏单例的问题,反序列化破坏单例的解决方法:在类中实现readResolve方法,改方法名必须为readResolve
  • 原理:原因是反序列化的时候,会调用ObjectInputStream类,而且在ObjectInputStream类中,做了判断,如果类里面存在readResolve方法,那么就会调用该方法,直接将方法内的返回值进行返回。INSTANCE
  • private Object readResolve(){return INSTANCE;}
public class ContainerSingleton
{
    private ContainerSingleton(){}

    private static Map ioc = new ConcurrentHashMap();

    public static Object getInstance(String className){
        if (ioc.containsKey(className)){
            try {
                Object instance = Class.forName(className).newInstance();
                ioc.put(className, instance);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return ioc.get(className);
    }
}

 

ThreadLocal单例

        保证线程内部的全局唯一,且天生线程安全。

  • ThreadLocal 比较特殊,在同一线程内,获得的对象是相同的。
  • 对于不同的线程来说,ThreadLocal获得的对象都是不一样的。

总结

单例模式的优点

1. 在内存中只有一个实例、减少内存开销
2. 可以避免对资源的多重占用
3. 设置全局访问点,严格控制访问。

单例模式的缺点

1. 没有接口,扩展困难
2. 如果要扩展单例对象,只有修改代码,没有其他途径。

单例模式的知识重点

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

 彩蛋

  1. 学习反射
  2. 学习序列化
  3. 学习volatile
  4. 下次再见,欢迎各位看官点赞、评论、收藏,谢谢大家。

你可能感兴趣的:(单例模式,设计模式,java)