单例模式的七种写法,你都知道吗?

大家好,我是三乙己。考上大家一考:“单例模式的单例,怎样写的?”

“不就是构造方法私有化么?”

”对呀对呀!……单例模式有七种写法,你知道么?“


言归正传……

单例模式(Singleton Pattern)可以说是最简单的设计模式了。

用一个成语来形容单例模式——“天无二日,国无二主”。

你滴大王,无限猖狂

什么意思呢?就是当前进程确保一个类全局只有一个实例。

那单例模式有什么好处呢?[1]

  • 单例模式在内存中只有一个实例,减少了内存开支
  • 单例模式只生成一个实例,所以减少了系统的性能开销
  • 单例模式可以避免对资源的多重占用
  • 单例模式可以在系统设置全局的访问点

那单例模式是银弹吗?它有没有什么缺点?

  • 单例模式一般没有接口,扩展很困难
  • 单例模式不利于测试
  • 单例模式与单一职责原则有冲突

那什么情况下要用单例模式呢?

  • 要求生成唯一序列号的环境
  • 在整个项目中需要一个共享访问点或共享数据
  • 创建一个对象需要消耗的资源过多
  • 需要定义大量的静态常量和静态方法(如工具类)的环境

接下来,进入今天的主题,我们来看看单例模式的七种写法!

1、饿汉式(线程安全)⭐

public class Singleton_1 {

    private static Singleton_1 instance=new Singleton_1();

    private Singleton_1() {
    }

    public static Singleton_1 getInstance() {
        return instance;
    }

}

饿汉式,就像它的名字,饥不择食,定义的时候直接初始化。

因为instance是个静态变量,所以它会在类加载的时候完成实例化,不存在线程安全的问题。

这种方式不是懒加载,不管我们的程序会不会用到,它都会在程序启动之初进行初始化。

所以我们就有了下一种方式👇

2、懒汉式(线程不安全)⭐

public class Singleton_2 {

    private static Singleton_2 instance;

    private Singleton_2() {
    }

    public static Singleton_2 getInstance() {
        if (instance == null) {
            instance = new Singleton_2();
        }
        return instance;
    }

}

懒汉式 是什么呢?只有用到的时候才会加载,这就实现了我们心心念的懒加载。

但是!

它又引入了新的问题?什么问题呢?线程安全问题。

懒汉式线程不安全

图片也很清楚,多线程的情况下,可能存在这样的问题:

一个线程判断instance==null,开始初始化对象;

还没来得及初始化对象时候,另一个线程访问,判断instance==null,也创建对象。

最后的结果,就是实例化了两个Singleton对象。

这不符合我们单例的要求啊?怎么办呢?

3、懒汉式(加锁)

public class Singleton_3 {

    private static Singleton_3 instance;

    private Singleton_3() {
    }

    public synchronized static Singleton_3 getInstance() {
        if (instance == null) {
            instance = new Singleton_3();
        }
        return instance;
    }
}

最直接的办法,直接上锁呗!

但是这种把锁直接方法上的办法,所有的访问都需要获取锁,导致了资源的浪费。

那怎么办呢?

4、懒汉式(双重校验锁)⭐

public class Singleton_4 {
    //volatile修饰,防止指令重排
    private static volatile Singleton_4 instance;

    private Singleton_4() {
    }

    public static Singleton_4 getInstance() {
        //第一重校验,检查实例是否存在
        if (instance == null) {
            //同步块
            synchronized (Singleton_4.class) {
                //第二重校验,检查实例是否存在,如果不存在才真正创建实例
                if (instance == null) {
                    instance = new Singleton_4();
                }
            }
        }
        return instance;
    }

}

这是比较推荐的一种,双重校验锁。

它的进步在哪里呢?

我们把synchronized加在了方法的内部,一般的访问是不加锁的,只有在instance==null的时候才加锁。

同时我们来看一下一些关键问题。

  • 首先我们看第一个问题,为什么要双重校验?

大家想一下,如果不双重校验。

如果两个线程一起调用getInstance方法,并且都通过了第一次的判断instance==null,那么第一个线程获取了锁,然后实例化了instance,然后释放了锁,然后第二个线程得到了线程,然后马上也实例化了instance。这就不符合我们的单例要求了。

接着我们来看第二个问题,为什么要用volatile 修饰 instance?

我们可能知道答案是防止指令重排。

那这个重排指的是哪?指的是instance = new Singleton(),我们感觉是一步操作的实例化对象,实际上对于JVM指令,是分为三步的:

  1. 分配内存空间
  2. 初始化对象
  3. 将对象指向刚分配的内存空间

有些编译器为为了性能优化,可能会把第二步和第三步进行重排序,顺序就成了:

  1. 分配内存空间
  2. 将对象指向刚分配的内存空间
  3. 初始化对象

指令重排

所以呢,如果不使用volatile防止指令重排可能会发生什么情况呢?

访问到未初始化对象

在这种情况下,T7时刻线程B对instance的访问,访问的是一个初始化未完成的对象。

所以需要在instance前加入关键字volatile

  • 使用了volatile关键字后,可以保证有序性,指令重排序被禁止;
  • volatile还可以保证可见性,Java内存模型会确保所有线程看到的变量值是一致的。

5、单例模式(静态内部类)

public class Singleton_5 {

    private Singleton_5() {
    }

    private static class InnerSingleton {
        private static final Singleton_5 instance = new Singleton_5();
    }

    public static Singleton_5 getInstance() {
        return InnerSingleton.instance;
    }
}

静态内部类是更进一步的写法,不仅能实现懒加载、线程安全,而且JVM还保持了指令优化的能力。

Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会加载静态内部类InnerSingleton类,从而完成Singleton的实例化。

类的静态属性只会在第一次加载类的时候初始化,同时类加载的过程又是线程互斥的,JVM帮助我们保证了线程安全。

6、单例模式(CAS)

public class Singleton_6 {

    private static final AtomicReference<Singleton_6> INSTANCE = new AtomicReference<Singleton_6>();

    private Singleton_6() {
    }

    public static final Singleton_6 getInstance() {
        //等待
        while (true) {
            Singleton_6 instance = INSTANCE.get();
            if (null == instance) {
                INSTANCE.compareAndSet(null, new Singleton_6());
            }
            return INSTANCE.get();
        }
    }
}

这种CAS式的单例模式算是懒汉式直接加锁的一个变种,sychronized是一种悲观锁,而CAS是乐观锁,相比较,更轻量级。

当然,这种写法也比较罕见,CAS存在忙等的问题,可能会造成CPU资源的浪费。

7、单例模式(枚举)

public enum Singleton_7 {

    //定义一个枚举,代表了Singleton的一个实例
    INSTANCE;
    public void anyMethod(){
        System.out.println("do any thing");
    }
}

调用方式:

    @Test
    void anyMethod() {
        Singleton_7.INSTANCE.anyMethod();
    }

《Effective Java》作者推荐的一种方式,非常简练。

但是这种写法解决了最主要的问题:线程安全、⾃由串⾏化、单⼀实例。

总结

从使用的角度来讲,如果不需要懒加载的话,直接饿汉式就行了;如果需要懒加载,可以考虑静态内部类,或者尝试一下枚举的方式。

从面试的角度,懒汉式、饿汉式、双重校验锁饿汉式,这三种是重点。双重校验锁方式一定要知道指令重排是在哪,会导致什么问题。


简单的事情重复做,重复的事情认真做,认真的事情有创造性地做。

我是三分恶,一个努力学习中的程序员。

点赞关注不迷路,咱们下期见!



参考:

[1]. 《设计模式之禅》

[2]. 《重学设计模式》

[3]. 设计模式系列 - 单例模式

[4]. Java中的双重检查锁(double checked locking)

  • 76
    点赞
  • 110
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 31
    评论
Java8新特性及实战视频教程完整版Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。 Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。使用Lambda 表达式可以使代码变的更加简洁紧凑。Java8实战视频-01让方法参数具备行为能力Java8实战视频-02Lambda表达式初探Java8实战视频-03Lambda语法精讲Java8实战视频-04Lambda使用深入解析Java8实战视频-05Lambda方法推导详细解析-上.wmvJava8实战视频-06Lambda方法推导详细解析-下Java8实战视频-07Stream入门及Stream在JVM中的线程表现Java8实战视频-08Stream知识点总结Stream源码阅读Java8实战视频-09如何创建Stream上集Java8实战视频-10如何创建Stream下集.wmvJava8实战视频-11Stream之filter,distinct,skip,limit,map,flatmap详细介绍Java8实战视频-12Stream之Find,Match,Reduce详细介绍Java8实战视频-13NumericStream的详细介绍以及和Stream之间的相互转换Java8实战视频-14Stream综合练习,熟练掌握API的用法Java8实战视频-15在Optional出现之前经常遇到的空指针异常.wmvJava8实战视频-16Optional的介绍以及API的详解Java8实战视频-17Optional之flatMap,综合练习,Optional源码剖析Java8实战视频-18初识Collector体会Collector的强大Java8实战视频-19Collector使用方法深入详细介绍-01Java8实战视频-20Collector使用方法深入详细介绍-02Java8实战视频-21Collector使用方法深入详细介绍-03.wmvJava8实战视频-22Collector使用方法深入详细介绍-04Java8实战视频-23Collector原理讲解,JDK自带Collector源码深度剖析Java8实战视频-24自定义Collector,结合Stream的使用详细介绍Java8实战视频-25Parallel Stream编程体验,充分利用多核机器加快计算速度Java8实战视频-26Fork Join框架实例深入讲解Java8实战视频-27Spliterator接口源码剖析以及自定义Spliterator实现一个Stream.wmvJava8实战视频-28Default方法的介绍和简单的例子Java8实战视频-29Default方法解决多重继承冲突的三大原则详细介绍Java8实战视频-30多线程Future设计模式原理详细介绍,并且实现一个Future程序Java8实战视频-31JDK自带Future,Callable,ExecutorService介绍Java8实战视频-32实现一个异步基于事件回调的Future程序.wmvJava8实战视频-33CompletableFuture用法入门介绍Java8实战视频-34CompletableFuture之supplyAsync详细介绍Java8实战视频-35CompletableFuture流水线工作,join多个异步任务详细讲解Java8实战视频-36CompletableFuture常用API的重点详解-上Java8实战视频-37CompletableFuture常用API的重点详解-下Java8实战视频-38JDK老DateAPI存在的问题,新的DateAPI之LocalDate用法及其介绍.wmvJava8实战视频-39New Date API之LocalTime,LocalDateTime,Instant,Duration,Period详细介绍Java8实战视频-40New Date API之format和parse介绍

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 31
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三分恶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值