设计模式之单例模式

原创
2019/07/22 18:13
阅读数 36

定义

单例模式(Singleton Pattern)限制系统中某一个类只能有一个唯一的实例。很多时候系统对类的需求就只是一个全局对象,有些资源比较重,加载创建耗时,适用于单例模式;有些资源代表的是纯函数的操作,虽然可以使用new 来创建新对象,使用单例模式可以减少对象创建消耗,在手机等资源少的地方推荐使用。

使用场景

  • 资源加载创建耗时,而资源是可共享的,比如日志文件,应用配置,打印机、显示器等
  • 系统资源,如线程池
  • 可复用的类资源,如纯函数的 Service,工具类
  • 控制访问需求,如一个类要控制类实例的权限
  • 全局的状态,比如统计访问人数,系统的状态转换等

单例模式的创建方式

通常的单例获取是类提供一个 getInstance 的静态方法,外部对象使用静态方法获取到类实例。

预加载

在类加载后直接实例化单例,系统控制并发,缺点是如果这个类从始至终都没有被使用过,加载实例浪费了系统资源。

public final class Singleton {
    private static Singleton instance = new Singleton();
    
    private Singleton() {
        
    }
    
    public static Singleton getInstance() {
        return instance;
    }
}

懒加载(延迟加载)

针对预加载可能浪费资源的问题,可以使用懒加载来避免。懒加载只有在资源被使用到的时候才会去加载。

public final class Singleton {
    private static Singleton instance;
    
    private Singleton() {
        
    }
    
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

如果存在多线程并发的情况,上面的获取实例方法存在竞态条件,有两种解决方案。

  1. synchronized 同步方法
public final class Singleton {
    private static Singleton instance;
    
    private Singleton() {
        
    }
    
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

由于加了同步,因此多线程访问时只能串行访问,牺牲了效率保证了安全。

  1. double check lock
public final class Singleton {
    private static Singleton instance;

    private Singleton() {
        
    }
    
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

双重检查保证了只有实例还未初始化时才会竞争锁,当实例初始化后,与第一种懒加载方式执行路径是一样的,并且减少了锁争抢开销。(jdk1.5 之后才可以使用)

懒加载虽然保证了资源加载就一定是被使用的,但是如果资源加载时间较长,所以使用方都必须等待资源加载完成,譬如数据库的加载、连接建立,因此要看自己的使用场景来判断是否需要使用懒加载模式。

静态内部类

静态内部类严格说起来属于懒加载,使用了jvm 加载类的机制,避免了使用双重检测的方式来防止并发问题。

public final class Singleton {
    private static Singleton instance;

    private Singleton() {

    }

    public static synchronized Singleton getInstance() {
        return Holder.holder;
    }

    private static class Holder {
        private static Singleton holder = new Singleton();
    }
}

Singleton 类加载时内部类并没有被加载,因此内部类的 holder 未被实例化,调用 getInstance 方法时,才真正加载和实例化。

枚举

枚举天生自带的单一属性非常符合单例模式,使用枚举的解决方案也非常优雅,而且还适合与策略、命令等模式配合。

枚举相比于前面的实现有几个优点:

  1. 无需费力将构造函数设为 private,而且设为了 private 也无法阻止使用方使用反射来创建实例。
  2. 无需担心序列化与反序列化,上面的模式中都没有考虑序列化相关问题,如果考虑还需要加入 readObject writeObject 方法。
  3. 虽然模式叫单例模式,但有时我们可能需要 n 个实例,数量是需要控制的,增加了实现难度,枚举只需要加一个枚举值
  4. 可以与策略模式相配合,实现更优雅。

当然枚举也是一种预加载的实现方案,适合简单的单例,对于复杂的单例实现像数据库引擎等的还是前面的方式更合适一些。

枚举的实现

  1. 简单枚举
public enum  Singleton {
    /**
     * 实例
     */
    INSTANCE;

    /**
     *
     */
    public void doSomething() {

    }
}
  1. 与策略、命令等模式结合
    如果我们每个实例需要有不同的实现,那么可以在枚举实例中覆盖相关方法, 枚举也支持抽象方法。
public enum  SingletonEnum {
    /**
     * 实例
     */
    INSTANCE1 {
        @Override
        public void forOverride() {
            System.out.println("Instance1");
        }
    },
    INSTANCE2 {
        @Override
        public void forOverride() {
            System.out.println("Instance2");
        }
    };

    /**
     *
     */
    public void doSomething() {

    }

    public abstract void forOverride();
}

总结

单例模式在后端自己创建的场景已经不多了,Spring 框架完全代理了相关操作,为开发者提供了很大的遍历,但是 Spring 本质上是一个工厂,有些复杂场景还是需要自己来动手。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部