Java创建型设计模式 —— 单例模式有这么多种写法你都知道吗?

一、引言

还记得老师当初给我们讲单例模式吗? 小编还清楚记得老师讲了一个是饿汉式一个是懒汉式,也讲了两者的实现方式。

那个时候不理解设计模式是做什么的,就死记硬背记住了,应付一下面试什么的。

如果你只知道两种写法看完文本肯定会有所收获,如果你是大牛,那就可以点点赞什么的哈哈哈哈哈

单例模式使用场景:

如果系统中有比较重量级的对象,并且只需要实例化一个的时候,就考虑使用单例模式。举个实际例子,在实际业务中难免会把数据存储到Elasticsearch(搜索引擎),那么对于操作Elasticsearch来说,只需要实例化一个对象即可,这个对象负责与Elasticsearch进行数据交互,看下实际代码如下:

一个很简单的饿汉式单例模式,把实例化的过程写在静态块当中,根据不同的启动环境连接不同的地址的Elasticsearch,最后创建对象赋值给esOperateService。

/**
 * @Auther: IT 贱男
 * @Date: 2018/11/1 16:15
 * @Description: Elasticsearch 代理对象,使用单例模式饿汉式 - 静态块实现
 */
public class EsServiceProxy {

    private static EsOperateService esOperateService;

    // 私有构造,防止外部new
    private EsServiceProxy() {
    }

    // 提供获取实例的静态方法
    public static EsOperateService getEsOperateService() {
        return esOperateService;
    }

    static {
        String env = Foundation.server().getEnvType();
        if (StringUtils.isEmpty(env)) {
            throw new RuntimeException("环境变量env未配置,请检查配置!");
        }

        env = env.toLowerCase();
        String baseurl = "http://192.168.188.21:8080/es/esOperateService";
        if (!env.equals("dev") && !env.equals("fat")) {
            if (env.equals("uat")) {
                baseurl = "http://192.168.188.22:8081/es/esOperateService";
            } else if (env.equals("pro")) {
                baseurl = "http://192.168.13.10:9082/es/esOperateService";
            }
        }

        // 通过代理工厂创建对象并且赋值给常量
        HessianProxyFactory hessianProxyFactory = new HessianProxyFactory();
        hessianProxyFactory.setOverloadEnabled(true);
        try {
            esOperateService = (EsOperateService) hessianProxyFactory.create(EsOperateService.class, baseurl);
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
    }
}

 

二、单例模式饿汉式 - 静态常量

 饿汉式在对象实例化的时候,就会创建好对象,没有达到懒加载的效果(懒加载:就是说这个对象我不要一开始就创建,等到我需要用的时候在创建),但是这样就不会因为多线程的问题导致创建多个实例。

如果保证这个对象,在系统中一定会有用到,那么这种方式也是推荐使用的。

/**
 * @Auther: IT贱男
 * @Date: 2019/7/25 15:22
 * @Description: 单例模式 饿汉式 - 静态常量
 *
 * 优点:写法比较简单,在类装载的时候就完成实例化,避免了多线程的问题
 * 缺点:这种方式没有达到懒加载的效果,可能会造成内存浪费 (如果系统中一定会用到这个对象,则就避免了内存浪费)
 *
 */
public class SingletonCase1 {

    // 私有构造方法,避免外部new
    private SingletonCase1() {
    }

    // 创建静态常量
    private static final SingletonCase1 singleton = new SingletonCase1();

    // 给外部提供实例获取方法
    public static SingletonCase1 getSingleton() {
        return singleton;
    }

}

三、单例模式饿汉式 - 静态代码块

 如果保证这个对象,在系统中一定会有用到,那么这种方式也是推荐使用的。

/**
 * @Auther: IT贱男
 * @Date: 2019/7/25 15:22
 * @Description: 单例模式 饿汉式 - 静态代码
 *
 * 优缺点和饿汉式静态常量一致
 *
 */
public class SingletonCase2 {

    // 私有构造方法,避免外部new
    private SingletonCase2() {
    }

    // 创建静态常量
    private static SingletonCase2 singleton;

    static {
        // 将对象的实例化放在静态块当中,则可以编写一些逻辑代码,如文章一开始举的实战例子
        singleton = new SingletonCase2();
    }

    // 给外部提供实例获取方法
    public static SingletonCase2 getSingleton() {
        return singleton;
    }

}

四、单例模式懒汉式 - 线程不安全写法

懒汉式:可以这样去记忆理解,既然是懒汉式就是在类加载的时候,懒得去创建对象,等到要用的时候在创建,这样也就实现了懒加载的效果。 但是这样实现也会存在一个很严重的问题,那就是在多线程的情况下,会存在创建多个实例的现象,具体看如下实现代码。

这种方式不推荐使用

/**
 * @Auther: IT贱男
 * @Date: 2019/7/25 15:22
 * @Description: 单例模式 懒汉式 - 线程不安全
 *
 * 优点:可以实现懒加载的效果
 * 缺点:在多线程的情况,可能会创建多个实例的情况
 *
 */
public class SingletonCase3 {

    // 私有构造方法,避免外部new
    private SingletonCase3() {
    }

    // 创建静态常量
    private static SingletonCase3 singleton;

    // 给外部提供实例获取方法
    public static SingletonCase3 getSingleton() {
        // 这里会存在多线程的问题,假设线程一正在执行new SingletonCase3()的操作,此时的singleton还是为null
        // 线程二进行 singleton == null 判断,这个时候等式还是成立的,所以线程二也会执行创建对象的操作。
        if (singleton == null) {
            singleton = new SingletonCase3();
        }
        return singleton;
    }

}

五、单例模式懒汉式 - 线程安全、同步方法

这个是针对上面的懒汉式进行了改进,给方法加上了synchronized关键字,能给有效的解决多线程的问题,但是会影响执行效率,因为所有的线程都要排队执行,所以这种方式也不推荐使用

/**
 * @Auther: IT贱男
 * @Date: 2019/7/25 15:22
 * @Description: 单例模式 懒汉式 - 线程不安全
 *
 * 优点:可以解决多线程的问题
 * 缺点:执行效率太慢,因为加上synchrionzed所有线程将会排队等待
 *
 */
public class SingletonCase4 {

    // 私有构造方法,避免外部new
    private SingletonCase4() {
    }

    // 创建静态常量
    private static SingletonCase4 singleton;

    // 给外部提供实例获取方法
    // 这里给方法上加了synchronized关键字,能够保证只有一个线程执行,其他线程排队等待
    public static synchronized SingletonCase4 getSingleton() {
        if (singleton == null) {
            singleton = new SingletonCase4();
        }
        return singleton;
    }

}

六、单例模式 - 双重检查

经过分析懒汉式,一共存在两个问题:1 多线程会创建多个、2、执行效率的问题

针对以上两个问题,所以就有了双重检查,这种方式不仅仅避免多线程的问题,还不会影响效率,也实现了懒加载,在公司中也比较常用。

这种方式推荐使用

/**
 * @Auther: IT贱男
 * @Date: 2019/7/25 15:22
 * @Description: 单例模式 双重检查
 *
 * 优点:推荐使用,能够解决懒加载、多线程的问题
 *
 * volatile :
 * 1、保证此变量对所有线程的可见性,“可见性”指当一条线程修改了这个变量的值,新的值对与其他线程来说是立即得知的。
 * 2、禁止指令重排序优化。
 *
 */
public class SingletonCase5 {

    // 私有构造方法,避免外部new
    private SingletonCase5() {
    }

     /**
      *
     *  加了volatile关键字,能够解决指令重排的问题,这里简单的解释一下指令重排的问题,需要一点点java内存模型的基础
     *
     *  执行这句代码时 singleton = new SingletonCase5();
     *
     *  正常情况下是这样的指令顺序
     *  1、memory = allocate() 分配对象的内存空间
     *  2、ctorInstance() 初始化对象
     *  3、instance = memory 设置instance指向刚分配的内存
     *
     *  在多线程的情况下,JVM 和 CPU优化会发生指令重排,变成这样了
     *  1、memory = allocate() 分配对象的内存空间
     *  3、instance = memory 设置instance指向刚分配的内存
     *  2、ctorInstance() 初始化对象
     *
     *  A线程在new SingletonCase5()的时候,如果在指令重排以后的情况下,执行到以上的步骤三时,这个时候对象还未初始化
     *  B线程进执行第一个if判断的时候,则会直接返回对象,这个时候对象还是未初始化的,如果直接使用则会出现问题
     *
     */


    // 创建静态常量
    // 这里给常量加了volatile关键字,能够保证此变量对所有线程对可见性
    // 当只要有一个线程修改了这个变量的值,那么其他线程也就可以立马获取到改变之后的值。

    private static volatile SingletonCase5 singleton;

    // 给外部提供实例获取方法
    public static SingletonCase5 getSingleton() {

        // 有些小伙伴在这里有点疑问,这里也加了synchronized关键字呀,为什么不会影响效率呢?
        // 小伙伴可以仔细看下代码,假设线程一进来之后,通过了第一个if判断,然后进入下面的代码,并且锁住了,然后创建了对象
        // 然后线程二进来,即使通过了第一个if判断,等线程一执行完,此时的singleton已不再为空,所以避免了创建多个对象
        // 那之后的线程如果再进来,直接在第一个if判断就不通过了,不会有线程等待的现象,也不影响效率。
        if (singleton == null) {
            synchronized (SingletonCase5.class) {
                if (singleton == null) {
                    singleton = new SingletonCase5();
                }
            }
        }
        return singleton;
    }

}

七、单例模式 - 静态内部类

这种方式也是可以推荐使用的,也避免了之前懒汉式的问题,编码也比较简单。

利用了jvm在装载类的时候,线程是安全的,也利用了内部类的在加载类时,不会加载内部类,所以这样写也是一种方式。

/**
 * @Auther: IT贱男
 * @Date: 2019/7/25 15:22
 * @Description: 单例模式 懒汉式 - 静态内部类
 *
 * 优点:
 * 1、静态内部类在类加载的时候,是不会被加载的,实现了懒加载的特性
 * 2、在调用获取实例的方法是,会去装载内部类,在jvm装载类的时候线程是安全的,静态属性只会在装载类初始化一次
 *
 */
public class SingletonCase6 {

    // 私有构造方法,避免外部new
    private SingletonCase6() {
    }

    // 创建静态内部类,提供常量
    private static class SingletonInstance {
        private static final SingletonCase6 SINGLETON = new SingletonCase6();
    }

    // 给外部提供实例获取方法
    public static SingletonCase6 getSingleton() {
        return SingletonInstance.SINGLETON;
    }

}

八、单例模式 - 枚举实现

枚举的实现方式确实能够达到单例模式所期待的效果,但小编在工作当中也没有遇到过实际的使用场景。

这种实现方式也不存在有什么问题,所以也是值得被推荐使用

/**
 * @Auther: IT贱男
 * @Date: 2019/7/25 15:22
 * @Description: 单例模式 懒汉式 - 枚举实现
 * <p>
 * 优点:借助JDK枚举来实现单例模式,不仅能避免多线程同步的问题,而且还能防止反序列化重新创建新的对象
 */
public class SingletonCase8 {

    // 私有构造方法,避免外部new
    private SingletonCase8() {
    }

    // 给外部提供实例获取方法
    public static SingletonCase8 getSingleton() {
        return Singleton.INSTANCE.getInstance();
    }

    private enum Singleton {
        INSTANCE;

        private SingletonCase8 singletonCase8;
        
        // JVM 保证了这个方法绝对只调用一次
        Singleton() {
            singletonCase8 = new SingletonCase8();
        }

        public SingletonCase8 getInstance() {
            return singletonCase8;
        }

    }
}

九、总的来说

值得推荐使用的几种方式有:

1、如果保证这个对象在项目中一定有使用到,那么饿汉式是值得推荐使用的,在项目中比较常用。

2、双重检查方式推荐使用,在项目中比较常用。

3、静态内部类、枚举实现方式推荐使用

 

那么这几种应该是市面上所有的常见的单例模式实现的方式,小编对每一种方式进行分析,也说明了优缺点。小伙伴可以根据不同的业务场景来选择不同的实现方式。

小编通过简单文字说明和注释来讲解的单例模式的,如果有小伙伴哪里有疑惑的可以评论留言,小编看到了会及时回复的。

 

发布了152 篇原创文章 · 获赞 422 · 访问量 43万+

猜你喜欢

转载自blog.csdn.net/weixin_38111957/article/details/99703470