设计模式——单例模式

单例模式

基本概念

  • 单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

  • 这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

  • 注意:

    • 1、单例类只能有一个实例。

    • 2、单例类必须自己创建自己的唯一实例。

    • 3、单例类必须给所有其他对象提供这一实例。

  • 优点

    • 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。

    • 2、避免对资源的多重占用(比如写文件操作)。

  • 缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

  • 使用场景:

    • 1、要求生产唯一序列号。

    • 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。

    • 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

  • 注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。

几种单例模式实现

  • 饿汉式

    • 单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。这种写法如果完美的话,就没必要在啰嗦那么多双检锁的问题了。缺点是它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    public class Hungry {
       private Hungry(){ //构造函数私有
       }
       private  static Hungry hungry = new Hungry();//饿汉式直接在本类中实例化对象
       public static Hungry getInstance(){
           return hungry;
       }
       
    //反射破解单例模式
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            Hungry instance1 = Hungry.getInstance();
            Constructor<Hungry> declaredConstructor = Hungry.class.getDeclaredConstructor(null);
            declaredConstructor.setAccessible(true);
            Hungry instance2 = declaredConstructor.newInstance();
            System.out.println(instance1);
            System.out.println(instance2);
        }
    }
  • 懒汉式

    • 线程不安全

    public class Singleton {
        private static Singleton instance;
        private Singleton (){}
    ​
        public static Singleton getInstance() {
         if (instance == null) {
             instance = new Singleton();
         }
         return instance;
        }
    }
    • 线程安全(同步方法)

    public class LazyManDemo {
        private LazyManDemo(){
            System.out.println(Thread.currentThread().getName());
        }
        private  static LazyManDemo instance;
        public static synchronized LazyManDemo getInstance(){
            if(instance==null){
                  instance = new LazyManDemo();
                }
            return instance;
            }
        //检查线程安全
            public static void main(String[] args) {
                for (int i = 0; i < 10; i++) {
                new Thread(()->{
                    LazyManDemo.getInstance();
                }).start();
            }
        }
    • 线程安全(双检锁)DCL懒汉式

      • instance=new LazyMan();并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

        1. 给 instance 分配内存

        2. 调用 Singleton 的构造函数来初始化成员变量

        3. 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

      • 三件事情的执行顺序不一定是预期中的,所以 instance 变量声明成 volatile 防止指令重排

    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    ​
    public class LazyMan {
        private LazyMan(){
            System.out.println(Thread.currentThread().getName());
        }
        private volatile static LazyMan instance;
        public static LazyMan getInstance(){
            if(instance==null){
                synchronized (LazyMan.class){
                    if(instance==null){
                        instance = new LazyMan();
                    }
                }
            }
            return instance;
        }
        //反射暴力破解
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            LazyMan instance1 = LazyMan.getInstance();
            Constructor<LazyMan> declaredConstructor = (Constructor<LazyMan>) instance1.getClass().getDeclaredConstructor(null);
            declaredConstructor.setAccessible(true);
            LazyMan lazyMan1 = declaredConstructor.newInstance();
            System.out.println(instance1);
            System.out.println(lazyMan1);
        }
    }
  • 静态内部类

    • 这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

    public class Singleton  {
        private Singleton (){
            System.out.println(Thread.currentThread().getName());
        };
        private static  class Inner{
        private static final Singleton INSTANCE = new Singleton();
    ​
        }
        public static final Singleton getInstance(){
            return Inner.INSTANCE;
        }
        //检查线程安全
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                new Thread(()->{
                   Singleton.getInstance();
                }).start();
            }
        }
    }
  • 枚举

    • 这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。 这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。

    public enum  EnumSingle{
        INSTANCE;
        public EnumSingle getInstance(){
            return INSTANCE;
        }
    }
    //实际是带有两个参数的构造函数,难以用反射破解
posted @ 2021-08-05 15:01  长安街ぷ  阅读(33)  评论(0)    收藏  举报