23 种设计模式之单例模式

发布于:2023-01-04 ⋅ 阅读:(525) ⋅ 点赞:(0)

23 种设计模式之单例模式



一、认识

①一句话来说就是,某个类只能有一个实例,提供一个全局的访问点。

②单例模式的要点有三个:

  • 一是某个类只能有一个实例;
  • 二是它必须自行创建这个实例;
  • 三是它必须自行向整个系统提供这个实例。

③使用Singleton的好处还在于可以节省内存,因为它限制了实例的个数,有利于Java垃圾回收(garbage collection),而且确保所有对象都访问唯一实例。但是不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。用单例模式,就是在适用其优点的状态下使用

回到顶部


二、UML类图

在这里插入图片描述
UML说明:

  • 1.构造方法私有化:可以使得该类不被实例化即不能被new
  • 2.在类本身里创建自己的对象
  • 3.提供一个公共的方法供其他对象访问

回到顶部


三、代码实现

1. 饿汉式

第一种:

public class Singleton {

    /**
     * static:
     * ①表示共享变量,语意符合
     * ②使得该变量能在getInstance()静态方法中使用
     * final:
     * ①final修饰的变量值不会改变即常量,语意也符合,当然不加final也是可以的
     * ②保证修饰的变量必须在类加载完成时就已经进行赋值。
     * final修饰的变量,前面一般加static
     */
    private static final Singleton singleton = new Singleton();

    /**
     * 私有化构造方法,使外部无法通过构造方法构造除singleton外的类实例
     * 从而达到单例模式控制类实例数目的目的
     */
    private Singleton(){}

    /**
     * 类实例的全局访问方法
     * 因为构造方法以及被私有化,外部不可能通过new对象来调用其中的方法
     * 加上static关键词使得外部可以通过类名直接调用该方法获取类实例
     * @return
     */
    public static Singleton getSingleton() {
        return singleton;
    }
}

第二种

public class SingletonStatic {

    private static final SingletonStatic singletonStatic;

    /**
     * 和第一种没有什么区别,这种看起来高大上面试装逼使用
     */
    static {
        singletonStatic = new SingletonStatic();
    }

    private SingletonStatic() {}

    public static SingletonStatic getSingletonStatic(){
        return singletonStatic;
    }
}

说明:

  • 优点: 一般使用static和final修饰变量(具体作用已经在代码里描述了),只在类加载时才会初始化,以后都不会,线程绝对安全,无锁,效率高。
  • 缺点: 类加载的时候就初始化,不管用不用,都占用空间,会消耗一定的性能(当然很小很小,几乎可以忽略不计,所以这种模式在很多场合十分常用而且十分简单)

注: 这里有两个小知识点:

  • 如果是final非static成员,必须在构造器、代码块、或者直接定义赋值
  • 如果是final static 成员变量,必须直接赋值 或者在静态代码块中赋值

2. 懒汉式

public class Singleton {

    private static Singleton singleton = null;

    private Singleton(){}

    public static Singleton getSingleton() {
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

说明:

  • 优点: 在外部需要使用的时候才进行实例化,不使用的时候不会占用空间。
  • 缺点: 线程不安全。看上去,这段代码没什么明显问题,但它不是线程安全的。假设当前有N个线程同时调用getInstance()方法,由于当前还没有对象生成,所以一部分同时都进入if语句new
    Singleton(),那么就会由多个线程创建多个user对象。

3. 线程安全的懒汉式

public class Singleton {

    private static Singleton singleton;

    private Singleton(){};

    private static synchronized Singleton getSingleton(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

说明:

  • 优点: 解决了懒汉式线程不安全的问题
  • 缺点: 线程阻塞,影响性能。

4. DCL单例 - 高性能的懒汉式

public class Singleton {
 /*volatile在这里发挥的作用是:禁止指令重排序(编译器和处理器为了优化程序性能
    * 而对指令序列进行排序的一种手段。)
    * singleton = new Singleton();这句代码是非原子性操作可分为三行伪代码
    * a:memory = allocate() //分配内存,在jvm堆中分配一段区域
    * b:ctorInstanc(memory) //初始化对象,在jvm堆中的内存中实例化对象
    * c:instance = memory //赋值,设置instance指向刚分配的内存地址
    * 上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。
    * 重排序是为了优化性能,但是不管怎么重排序,在单线程下程序的执行结果不能被改变
    * 保证最终一致性。而在多线程环境下,可能发生重排序,会影响结果。
    * ①若A线程执行到代码singleton = new Singleton()时;
    * ②同时若B线程进来执行到代码到第一层检查if (singleton == null)
    * ③当cpu切换到A线程执行代码singleton = new Singleton();时发生了指令重排序,
    * 执行了a-b,没有执行c,此时的singleton对象只有地址,没有内容。然后cpu又切换到了B线程,
    * 这时singleton == null为false(==比较的是内存地址),
    * 则代码会直接执行到了return,返回一个未初始化的对象(只有地址,没有内容)。
    * */
    private volatile static Singleton singleton;

    private Singleton() {
    }

    public static Singleton getSingleton() {
        /*第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入
        * ①当多个线程第一次进入,所有线程都进入if语句
        * ②当多个线程第二次进入,因为singleton已经不为null,因此所有线程都不会进入if语句,
        * 即不会执行锁,从而也就不会因为锁而阻塞,避免锁竞争*/
        if (singleton == null) {
            /*第一层锁,保证只有一个线程进入,
            * ①多个线程第一次进入的时候,只有一个线程会进入,其他线程处于阻塞状态
            * 当进入的线程创建完对象出去之后,其他线程又会进入创建对象,所以有了第二次if检查
            * ②多个线程第二次是进入不到这里的,因为已被第一次if检查拦截*/
            synchronized (Singleton.class) {
                /*第二层检查,防止除了进入的第一个线程的其他线程重复创建对象*/
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

说明: 代码注释已详细讲解volatile在该单例模式的作用,已经双重锁的作用。

  • 优点: 解决了线程阻塞的问题
  • 缺点: 多个线程第一次进入的时候会造成大量的线程阻塞,代码不够优雅。

5. 静态内部类的方式

public class Singleton {

    private Singleton(){}

    private static class LayzInner{
        private static Singleton singleton = new Singleton();
    }
    
    public static Singleton getSingleton(){
        return LayzInner.singleton;
    }
}

说明:

  • 优点: 第一次类创建的时候加载,避免了内存浪费,不存在阻塞问题,线程安全,唯一性
  • 缺点: 序列化-漏洞:反射,会破坏内部类单例模式

6. 枚举单例模式

public enum EnumSingleton {
    INSTANCE;
    private Singleton singleton;
    EnumSingleton(){
        singleton = new Singleton();
    }
    public Singleton getSingleton(){
        return singleton;
    }
}

说明: 单元素的枚举类型已经成为实现Singleton的最佳方法,无法反射创建对象,是特殊的饿汉式。

7. 静态内部类升级版

借鉴枚举单例的内部实现的方式

public class Singleton {
    private Singleton(){
        if(LayzInner.singleton != null){
            throw new RuntimeException("不能够进行反射!");
        }
    }

    private static class LayzInner{
        private static Singleton singleton = new Singleton();
    }

    public static Singleton getSingleton (){
        return LayzInner.singleton;
    }
}

说明:

  • 优点:第一次类创建的时候加载,避免了内存浪费,不存在阻塞问题,线程安全,唯一性,解决了反射会破坏内部类单例模式的问题
  • 缺点:不是官方的

8. 容器式单例

public class Singleton {
    private Singleton() {
    }

    private static Map<String, Object> ioc = new ConcurrentHashMap<>();

    public static Object getBean(String className) {
        synchronized (ioc) {
            if (ioc.containsKey(className)) {
                Object o = null;
                try {
                    o = Class.forName(className).newInstance();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return o;
            } else {
                return ioc.get(className);
            }
        }
    }
}

说明: Spring ioc 单例 是懒汉式 枚举上的升级

9. ThreadLocal单例

package com.rf.designPatterns.singleton.threadLocalSingleton;

/**
 * @description: ThreadLocal线程单例,为每一个线程提供一个对象,在访问的时候相互不影响
 */
public class ThreadLocalSingleton {

    //创建ThreadLocal实例对象,并且重写initialValue方法
    private static final ThreadLocal<ThreadLocalSingleton>  threadLocalSingleton =
            new ThreadLocal<ThreadLocalSingleton>(){
        @Override
        protected ThreadLocalSingleton initialValue() {
            return new ThreadLocalSingleton();
        }
    };

    //构造方法
    private ThreadLocalSingleton(){

    }

    //提供对外暴露的方法,获取ThreadLocalSingleton对象
    public static ThreadLocalSingleton getInstance(){
        return threadLocalSingleton.get();
    }
}

说明:

局部单例模式:某一个线程里唯一

①ThreadLocal的作用呢,是提供线程内的局部变量,在多线程环境访问时,能保证各个线程内的ThreadLocal变量各自独立。也就是说每个线程的ThreadLocal变量是自己专用的,其他线程是访问不到的。

②ThreadLocal最常用于在多线程环境下存在对非线程安全对象的并发访问,而且该对象不需要在线程内共享,如果对该对象加锁,会造成大量线程阻塞影响程序性能,这时候就可以使用ThreadLocal来使每个线程都持有该对象的副本,这是典型的空间换取时间从而提高执行效率的方式。例如项目里经常使用的SimpleDateFormat日期格式化对象,该对象是线程不安全的,而且不需要在线程内共享,因此可以使用ThreadLocal保证其线程安全。

回到顶部


本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到