你真的会写单例模式吗 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
deppwxq
V2EX    Java

你真的会写单例模式吗

  •  
  •   deppwxq
    deppwang 2020-04-12 15:16:17 +08:00 5522 次点击
    这是一个创建于 2011 天前的主题,其中的信息可能已经有所发展或是发生改变。

    作者:DeppWang原文地址

    又一篇一抓一大把的博文,可是你真的的搞懂了吗?点开看看,事后,你也来一篇。。。

    人生在世,谁不面试。单例模式:一个搞懂不加分,不搞懂减分的知识点

    img

    单例模式是面试中非常喜欢问的了,我们往往自认为已经完全理解了,没什么问题了。但要把它手写出来的时候,可能出现各种小错误,下面是我总结的快速准确的写出单例模式的方法。

    单例模式有各种写法,什么「双重检锁法」、什么「饿汉式」、什么「饱汉式」,总是记不住、分不清。这就对了,人的记忆力是有限的,我们应该记的是最基本的单例模式怎么写。

    单例模式:一个类有且只能有一个对象(实例)。单例模式的 3 个要点:

    1. 外部不能通过 new 关键字(构造函数)的方式新建实例,所以构造函数为私有:private Singleton(){}
    2. 只能通过类方法获取实例,所以获取实例的方法为公有、且为静态:public static Singleton getInstance()
    3. 实例只能有一个,那只能作为类变量的「数据」,类变量为静态 (另一种记忆:静态方法只能使用静态变量):private static Singleton instance

    一、最基础、最简单的写法

    类加载的时候就新建实例

    public class Singleton { private static Singleton instance = new Singleton(); private Singleton() { } public static Singleton getInstance() { return instance; } public void show(){ System.out.println("Singleon using static initialization in Java"); } } // Here is how to access this Singleton class Singleton.getInstance().show(); 

    当执行 Singleton.getInstance() 时,类加载器加载 Singleton.class 进虚拟机,虚拟机在方法区(元数据区)为类变量分配一块内存,并赋值为空。再执行 <client>() 方法,新建实例指向类变量 instance 。这个过程在类加载阶段执行,并由虚拟机保证线程安全。所以执行 getInstance() 前,实例就已经存在,所以 getInstance() 是线程安全的。

    很多博文说 instance 还需要声明为 final,其实不用。final 的作用在于不可变,使引用 instance 不能指向另一个实例,这里用不上。当然,加上也没问题。

    看到这里,单例模式的写法你已经学到了。后面的是加餐,可以选择不看了。

    这个写法有一个不足之处,就是如果需要通过参数设置实例,则无法做到。举个栗子:

    public class Singleton { private static Singleton instance = new Singleton(); private Singleton() { } // 不能设置 name ! public static Singleton getInstance(String name) { return instance; } public void show(){ System.out.println("Singleon using static initialization in Java"); } } // Here is how to access this Singleton class Singleton.getInstance("test").show(); 

    二、可通过参数设置实例的写法

    考虑到这种情况,就在调用 getInstance() 方法时,再新建实例。

    public class Singleton { private static Singleton instance; private String name; private Singleton(String name) { this.name = name; } public static synchronized Singleton getInstance(String name) { if (instance == null) { instance = new Singleton(name); } return instance; } public String show() { return name + ",hashcode: " + instance.hashCode(); } } Singleton.getInstance("test").show(); 

    这里加了 synchronized 关键字,能保证线程安全(只会生成一个实例),但效率不高。因为实例创建成功后,再获取实例时就不用加锁了。

    当不加 synchronized 时,会发生什么:

    instance 是类的变量,类存放在方法区(元数据区),元数据区线程共享,所以类变量 instance 线程共享,类变量也是在主内存中。线程执行 getInstance() 时,在自己工作内存新建一个栈帧,将主内存的 instance 拷贝到工作内存。多个线程并发访问时,都认为 instance == null,就将新建多个实例,那单例模式就不是单例模式了。

    测试:

    public class Test { public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> { Singleton instance = Singleton.getInstance("test"); System.out.println(instance.show()); }).start(); } } } 

    三、改良版加锁的写法

    实现只在创建的时候加锁,获取时不加锁。

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

    为什么要判断两次:

    多个线程将 instance 拷贝进工作内存,即多个线程读取到 instance == null,虽然每次只有一个线程进入 synchronized 方法,当进入线程成功新建了实例,synchronized 保证了可见性(在 unlock 操作前将变量写回了主内存),此时 instance 不等于 null 了,但其他线程已经执行到 synchronized 这里了,某个线程就又会进入 synchronized 方法,如果不判断一次,又会再次新建一个实例。

    为什么要用 volatile 修饰 instance:

    synchronized 已经可以实现原子性、可见性、有序性,其中实现原子性:一次只有一个线程执行同步块的代码。但计算机为了提升运行效率,会指令重排序。

    代码 instance = new Singleton(); 会被计算机拆为 3 步执行。

    • A:在堆中分配一块内存空间
    • B:在内存空间位置新建一个实例
    • C:将引用指向实例,即,引用存放实例的内存空间地址

    线程可能按 ACB 执行,如果 instance 都在 synchronized 里面,怎么重排序都没啥问题,问题出现在还有 instance 在 synchronized 外边,因为此时外边一群饿狼(线程),就在等待一个 instance 这块肉不为 null 。

    模拟一下指令重排序的出错场景:多线程环境下,正好一个线程,在同步块中按 ACB 执行,执行到 AC 时(并将 instance 写回了主内存),另一个线程执行第一个判断时,从主内存拷贝了最新的 instance,认为 instance 不为空,返回 instance,但此时 instance 还没被正确初始化,所以出错。

    volatile 修饰 instance 时,虚拟机在 ACB 后添加一个 lock 指令,lock 指令之前的操作执行完成后,后面的操作才能执行。只有当 ACB 都执行完了之后,其他线程才能读取 instance 的值,即:只有当写操作完成之后,读操作才能开始。这也是 Java 虚拟机规范的其中一条先行发生原则:对 volatile 修饰的变量,读操作,必须等写操作完成。

    所以用 volatile 修饰 instance,是使用它的禁止指令重排序特性:禁止读指令重排序到写指令之前。(它禁止不了 lock 指令前的指令重排序。)

    你可能认为上面的解释太复杂,不好理解。对,确实比较复杂,看不懂,下次问到再看吧。

    四、其他非主流写法

    枚举写法:

    public enum EasySingleton{ INSTANCE; } 

    当面试官让我写一个单例模式,我总是觉得写这个好像有点另类。

    静态内部类写法:

    public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } } 

    这个写法还是比较有逼格的,但稍不注意就容易出错。

    五、小结

    单例模式主要为了节省内存开销,Spring 容器的 Bean 就是通过单例模式创建出来的。

    单例模式没写出来,那也没啥事,因为那下一个问题你也不一定能答出来 :)。

    单例模式不会写,也不影响你称为大佬,哈哈。

    六、延伸阅读

    24 条回复    2020-04-13 19:44:07 +08:00
    xcstream
        1
    xcstream  
       2020-04-12 15:52:28 +08:00   2
    老是会想到 孔乙己中,茴香豆的“茴”字有几种写法
    hantsy
        2
    hantsy  
       2020-04-12 15:54:07 +08:00   1
    enum 才是 Java 5 后的正解。最经典还是双检模式( Lazy 方式)。Spring 以 Singleton 为主,也有很多 Prototype 的使用场景。Bean 状态这东西还是 CDI 规范定义的比较好。
    lhx2008
        3
    lhx2008  
       2020-04-12 15:58:08 +08:00 via Android
    第二种写法是楼主发明的吗
    momocraft
        4
    momocraft  
       2020-04-12 16:00:01 +08:00
    根 java 存模型 3 是不安全的: 同步指令跨程看到一非 null 的引用不保任何事, 包括象正初始化完成

    2 不自作明反而安全

    “我们往往自认为已经完全理解了”
    momocraft
        5
    momocraft  
       2020-04-12 16:05:37 +08:00
    >4

    我看了 有 volatile 是有保的

    再次 “我们往往自认为已经完全理解了”
    deppwxq
        6
    deppwxq  
    OP
       2020-04-12 16:40:36 +08:00
    @xcstream 大哥,这是讽刺么 [Facepalm]
    dr1q65MfKFKHnJr6
        7
    dr1q65MfKFKHnJr6  
       2020-04-12 16:45:25 +08:00
    二次加锁 这种方式是理论上的, 如果有安全测试的话,有一些公认的安全规范是过不了的, 不允许你整这样的东西的, 要求你简单点 类加载的时候 new 出来。
    putaozhenhaochi
        8
    putaozhenhaochi  
       2020-04-12 16:57:10 +08:00 via Android   4
    一般的套路是文章结尾有个公众号广告。竟然没有
    deppwxq
        9
    deppwxq  
    OP
       2020-04-12 16:59:10 +08:00
    @hantsy tks,Spring 还需要深入研究研究
    softtwilight
        10
    softtwilight  
       2020-04-12 17:12:31 +08:00
    枚举不是非主流,是比 double check 更好的实践
    deppwxq
        11
    deppwxq  
    OP
       2020-04-12 17:43:48 +08:00
    @softtwilight 是的,写得不太严谨,
    jin7
        12
    jin7  
       2020-04-12 17:46:48 +08:00
    枚举和饿汉没啥区别
    yazoox
        13
    yazoox  
       2020-04-12 18:17:43 +08:00
    @deppwxq
    为啥 enum 也算是单例模式,貌似大家都还认为这种方法不错。
    没看太懂......
    sagaxu
        14
    sagaxu  
       2020-04-12 18:25:03 +08:00 via Android
    object SingletonObj {
    ...
    }

    线程安全和懒加载的单例,一行搞定不香吗
    AmmeLid
        15
    AmmeLid  
       2020-04-12 18:28:58 +08:00
    @yazoox
    effactive java 推荐写法,解决多线程环境下不完全构造问题和序列化破坏单例问题。
    deppwxq
        16
    deppwxq  
    OP
       2020-04-12 19:27:59 +08:00
    @yazoox 因为「枚举」,你可以看看枚举方面的知识点
    fish47
        17
    fish47  
       2020-04-12 23:24:53 +08:00
    有状态的,和上下文强关联的,这些对象不应该做成单例。太容易滥用成为全局变量了。
    cwjokaka
        18
    cwjokaka  
       2020-04-13 09:28:11 +08:00
    谢谢,复习了
    liangdu
        19
    liangdu  
       2020-04-13 10:43:41 +08:00
    用 final static 代替 volatile 更好
    bHvFB8c1VUQyGiNT
        20
    bHvFB8c1VUQyGiNT  
       2020-04-13 12:29:37 +08:00
    为什么我看其他地方讲解 volatile 是说 abc 不会被线程优化城 acb
    deppwxq
        21
    deppwxq  
    OP
       2020-04-13 13:43:32 +08:00
    @bHvFB8c1VUQyGiNT 你可以看看《深入理解 Java 虚拟机》最后一部分「高效并发」,里面有清晰的讲解。使用 volatile 后,赋值操作后面会加一个内存屏障 lock,重排序时,不能将后面的指令重排序到内存屏障的前面。可能还存在 ACB,只是要等 ACB 执行完了,读操作才能执行。
    cyd
        22
    cyd  
       2020-04-13 16:57:13 +08:00
    我来指出 /探讨一个问题。
    volatile 已经不需要加了,在高版本的 jvm,new 的指令重排问题已经被解决了。
    我是在某网站上看的,真假未知。
    deppwxq
        23
    deppwxq  
    OP
       2020-04-13 18:08:13 +08:00
    @cyd 新知识点,
        24
    laozhang  
       2020-04-13 19:44:07 +08:00
    枚举啊。必须枚举。不信你看看 Effective Java
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     922 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 26ms UTC 20:54 PVG 04:54 LAX 13:54 JFK 16:54
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86