专栏电商日志财经减肥爱情
投稿投诉
爱情常识
搭配分娩
减肥两性
孕期塑形
财经教案
论文美文
日志体育
养生学堂
电商科学
头戴业界
专栏星座
用品音乐

没那么简单的单例模式

  大有学问
  大家好,我是呼噜噜,单例(Singleton)可以说是最简单的设计模式之一,而且基本上哪怕你没特别了解过,也能够随手写出,但是单例真有这么简单吗?
  什么是单例
  单例对象的类必须保证只有一个实例存在,自行提供这个实例,并向整个系统提供这个实例。
  上述定义总结以下特点大致有3点:
  单例类只有一个实例对象;该单例对象必须由单例类自行创建;单例类对外提供一个访问该单例的全局访问点。单例的应用场景
  单例模式的核心精髓其实是避免创建不必要的对象
  不必要的对象一般是:
  频繁创建的一些类,又频繁被销毁昂贵的对象,有些对象创建的成本比其他对象要高得多,比如占用资源较多,或实例化耗时较长系统要求单一控制逻辑的操作,或者对象需要被共享的情况。。。。。。
  常见的使用场合:数据库的连接池、Spring中的全局访问点BeanFactory,Spring下的Bean、多线程的线程池、网络连接池等等
  单例模式的优点:
  1。不仅可以减少每次创建对象的时间开销,还可以节约内存空间;
  2。能够避免由于操作多个实例导致的逻辑错误;
  3。如果一个对象有可能贯穿整个应用程序,能够起到了全局统一管理控制的作用。
  缺点:单例模式一般没有接口,没有抽象层,扩展困难。如果要扩展,得修改原来的代码
  单例模式的功能代码通常写在一个类中,其职责过重,如果功能设计不合理,则很容易违背单一职责原则
  不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。比如单例模式下去将对象转成json会出现互相引用的问题。单例的实现方式
  对单例的实现一般可以分为两大类懒汉式和饿汉式
  他们的区别在于:
  懒汉式:全局的单例实例,默认不会实例化,直到首次使用时才实例化,通俗点讲一个懒汉,不愿意动弹。等到饭点了,他才开始想办法搞食物
  饿汉式:全局的单例实例在类装载时就实例化,并且创建单例对象。通俗点讲一个饿汉,很勤快就怕自己饿着。总是先把食物准备好,等啥时候到饭点了,他随时拿来吃1。懒汉式单例简单版本
  我们首先来写一个最简单的懒汉实现单例的方式:懒汉最简单的版本publicclassSingletonEasy{privatestaticSingletonEasyinstance;privateSingletonEasy(){}将构造器私有化,防止外部调用publicstaticSingletonEasygetInstance(){if(instancenull){instancenewSingletonEasy();}returninstance;}}
  使用方式:SingletonEasysingletonEasySingletonEasy。getInstance();
  SingletonEasy的instance默认为空,直到程序获取instance时,先进行判断instance是否为空,如果instance为空就new一个,反之直接返回已存在的instance
  我们以这种方式实现的单例是线程不安全的,在大部分情况下是没问题的,但是当突然有一天有多个访问者(线程)同时去获取对象实例时,if(instancenull){instancenewSingletonEasy();}
  他们发现都不存在instance,然后就会导致创建多个同样的实例的问题。那怎么解决这种问题呢?2。懒汉式单例synchronized版
  其实遇到上面的问题,我们很容易想到一个解决方案加锁synchronized懒汉加锁synchronizedpublicclassSingleSyn{privatestaticSingleSyninstance;privateSingleSyn(){将构造器私有化,防止外部调用}publicstaticsynchronizedSingleSyngetInstance(){if(instancenull){instancenewSingleSyn();}returninstance;}}
  加锁之后,如果有多个访问者(线程)访问getInstance()方法,当一个线程获得锁之后,进行判空、对象创建、获得返回值的操作,其他的线程必须等待其完成,才能继续执行
  这样加锁之后懒汉模式虽然解决了线程并发问题(线程安全的),但由于把锁加到方法上后,所有的访问都因需要锁占用导致资源的浪费,这其实非常影响程序的性能,效率很低。那我们可以怎样优化呢?3。懒汉式单例双重校验锁synchronized版懒汉双层校验锁publicclassSingleDoubleCheck{privatestaticSingleDoubleCheckinstancenull;privateSingleDoubleCheck(){}将构造器私有化,防止外部调用publicstaticSingleDoubleCheckgetInstance(){if(instancenull){part1synchronized(SingleDoubleCheck。class){if(instancenull){part2instancenewSingleDoubleCheck();part3}}}returninstance;}}
  我们来仔细看下它的妙处:在多线程的环境下,当一个线程执行getInstance()时先判断单例对象是否已经初始化,如果已经初始化,就直接返回单例对象,如果未初始化,就在同步代码块中先进行初始化,然后返回,效率很高。在多线程的环境下,当一个线程执行getInstance()时程序到达part1处的if(instancenull)先判断单例对象是否已经初始化,如果已经初始化,就直接返回单例对象,如果未初始化,则进入后续同步块逻辑;
  此处解决了懒汉式单例synchronized版的缺陷,不会影响到其他线程的getInstance()方法。程序进入同步块,当一个线程获得锁之后,进行判空(part2处的instancenull)、对象创建、获得返回值的操作,其他的线程必须等待其完成,才能继续执行。
  此处实现了懒汉式单例synchronized版的功能,保证了线程安全。
  这种写法,理论上既线程安全又效率高,可惜事实并非如此。
  问题出现在了part3处instancenewSingleDoubleCheck();我们来看下整个类的字节码(JVM指令集):javapcSingleDoubleCheck。classCompiledfromSingleDoubleCheck。javapublicclasscom。zj。ideaprojects。test。SingleDoubleCheck{publicstaticcom。zj。ideaprojects。test。SingleDoubleCheckgetInstance();Code:0:getstatic2Fieldinstance:LcomzjideaprojectstestSingleDoubleCheck;3:ifnonnull376:ldc3classcomzjideaprojectstestSingleDoubleCheck8:dup9:astore010:monitorenter11:getstatic2Fieldinstance:LcomzjideaprojectstestSingleDoubleCheck;14:ifnonnull2717:new3classcomzjideaprojectstestSingleDoubleCheck20:dup21:invokespecial4Methodinit:()V24:putstatic2Fieldinstance:LcomzjideaprojectstestSingleDoubleCheck;27:aload028:monitorexit29:goto3732:astore133:aload034:monitorexit35:aload136:athrow37:getstatic2Fieldinstance:LcomzjideaprojectstestSingleDoubleCheck;40:areturnExceptiontable:fromtotargettype112932any323532anystatic{};Code:0:aconstnull1:putstatic2Fieldinstance:LcomzjideaprojectstestSingleDoubleCheck;4:return}
  内容比较多,我们直接看instancenewSingleDoubleCheck()相关的部分,
  可以发现在JVM字节码中instancenewSingleDoubleCheck()是有4个操作的11:getstatic2获取指定类的静态域instance索引2,并将其值压入栈顶14:ifnonnull27不为空17:new31。创建对象SingleDoubleCheck,并将对象引用压入栈20:dup2。将操作数栈顶的数据复制一份,并将其压入栈,此时栈中有两个引用值21:invokespecial43。pop出栈引用值,调用SingleDoubleCheck其构造函数,完成对象的初始化24:putstatic24。SingleDoubleCheck对象指向指定类的静态域instance索引2
  new指令并不能完全创建一个对象,对象只有在调用初始化方法完成后(即调用了invokespecial指令之后),对象才创建成功。
  所以instancenewSingleDoubleCheck()并非一个原子操作(atomic)
  原子操作就是不可分割的操作,在计算机中,就是指不会因为线程调度被打断的操作。
  而在我们现代的计算机中CPU是乱序执行。CPU的速度是超级快的,但同时其价格也是非常昂贵的。为了充分压榨CPU,我们要把CPU的时间进行分片,让各个程序在CPU上轮转,造成一种多个程序同时在运行的假象,即并发。
  并发是针对单核CPU提出的,而并行则是针对多核CPU提出的。和单核CPU不同,多核CPU真正实现了同时执行多个任务
  在CPU中为了能够让指令的执行尽可能地同时运行起来,采用了指令流水线。一个CPU指令的执行过程可以分成4个阶段:取指、译码、执行、写回。这4个阶段分别由4个独立物理执行单元来完成。理想的情况是:指令之间无依赖,可以使流水线的并行度最大化
  但是如果两条指令的前后存在依赖关系,比如数据依赖,控制依赖等,此时后一条语句就必需等到前一条指令完成后,才能开始。所以CPU为了提高流水线的运行效率,对无依赖的前后指令做适当的乱序和调度
  接着上面的内容,在生成字节码后,JVM的编译器同样也会对其指令进行重排序的优化(指令重排)。
  所谓指令重排是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。JVM中并没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排序的优化。
  无论是编译期的指令重排还是CPU的乱序执行,主要都是为了让CPU内部的指令流水线可以填满,提高指令执行的并行度。
  指令重排对于非原子性的操作,在不影响最终结果的情况下,其拆分成的原子操作可能会被重新排列执行顺序。instancenewSingleDoubleCheck()的操作1234可能变成1243。这样会存在一个instance已经不为null但是SingleDoubleCheck仍没有完成初始化的状态这个时候其他的线程过来,走到part1if(instancenull)处时会产生:明明instance不为空,但是SingleDoubleCheck却没有的问题
  这种问题我们如何解决呢?4。懒汉式单例双重校验锁volatile版
  不过好在JDK1。5及之后版本增加了volatile关键字。volatile保证该变量对所有线程的可见性,还有一个语义是禁止指令重排序优化,这样可以保证instance变量被赋值的时候对象已经是初始化完成的,从而避免了上面说到的问题。懒汉双层校验锁2publicclassSingleVolatile{privatestaticvolatileSingleVolatileinstance;加上volatile关键字privateSingleVolatile(){}将构造器私有化,防止外部调用publicstaticSingleVolatilegetInstance(){if(instancenull){synchronized(SingleVolatile。class){if(instancenull){instancenewSingleVolatile();}}}returninstance;}}
  我们查看一下这个文件的字节码:javapcSingleVolatile。classCompiledfromSingleVolatile。javapublicclasstest。SingleVolatile{publicstatictest。SingleVolatilegetInstance();Code:0:getstatic2Fieldinstance:LtestSingleVolatile;3:ifnonnull376:ldc3classtestSingleVolatile8:dup9:astore010:monitorenter11:getstatic2Fieldinstance:LtestSingleVolatile;14:ifnonnull2717:new3classtestSingleVolatile20:dup21:invokespecial4Methodinit:()V24:putstatic2Fieldinstance:LtestSingleVolatile;27:aload028:monitorexit29:goto3732:astore133:aload034:monitorexit35:aload136:athrow37:getstatic2Fieldinstance:LtestSingleVolatile;40:areturnExceptiontable:fromtotargettype112932any323532anypublicstaticvoidmain(java。lang。String〔〕);Code:0:invokestatic5MethodgetInstance:()LtestSingleVolatile;3:pop4:return}
  可以看出和SingleDoubleCheck。class的字节码基本一模一样,看不出啥区别
  那我们继续对SingleVolatile。class文件反汇编一下:
  server
  Xcomp
  XX:UnlockDiagnosticVMOptions
  XX:PrintAssembly
  XX:CompileCommandcompileonly,SingleVolatile。getInstance
  VM参数我贴了一下,大家感兴趣可以去试试。。。0x000001cdb13c7313:movdwordptr〔r1168h〕,r10d0x000001cdb13c7317:movr10,76bf9bc68h;{oop(ajavalangClasstestSingleVolatile)}0x000001cdb13c7321:shrr10,9h0x000001cdb13c7325:movr11,1cdbd065000h0x000001cdb13c732f:movbyteptr〔r11r10〕,r12l0x000001cdb13c7333:lockadddwordptr〔rsp〕,0h;putstaticinstance;test。SingleVolatile::getInstance24(line13)0x000001cdb13c7338:jmp1cdb13c71e4h0x000001cdb13c733d:movrdx,7c0060828h;{metadata(testSingleVolatile)}。。。
  汇编代码比较长,省略了很多,根据putstatic
  我们定位到第7行0x000001cdb13c7333:lockadddwordptr〔rsp〕,0h;putstaticinstance
  我们再对SingleDoubleCheck。class反汇编一下:
  VM参数:
  server
  Xcomp
  XX:UnlockDiagnosticVMOptions
  XX:PrintAssembly
  XX:CompileCommandcompileonly,SingleDoubleCheck。getInstance
  它的汇编代码,我们根据putstatic同样截取一段:。。。0x00000209690592e4:movrax,76bf9bd90h;{oop(ajavalangClasstestSingleDoubleCheck)}0x00000209690592ee:movrsi,qwordptr〔rsp20h〕0x00000209690592f3:movr10,rsi0x00000209690592f6:shrr10,3h0x00000209690592fa:movdwordptr〔rax68h〕,r10d0x00000209690592fe:shrrax,9h0x0000020969059302:movrsi,20974cf5000h0x000002096905930c:movbyteptr〔raxrsi〕,0h;putstaticinstance;test。SingleDoubleCheck::getInstance24(line18)0x0000020969059310:movrax,76bf9bd90h;{oop(ajavalangClasstestSingleDoubleCheck)}0x000002096905931a:learax,〔rsp28h〕0x000002096905931f:movrdi,qwordptr〔rax8h〕。。。
  我们发现第9行0x000002096905930c:movbyteptr〔raxrsi〕,0h;putstaticinstance
  这个时候我们发现了区别,加了Volatile关键字后,汇编代码中多了一个lock,其他的都是正常赋值的汇编语句
  我们知道在汇编中LOCK指令前缀功能如下:被修饰的汇编指令成为原子的与被修饰的汇编指令一起提供内存屏障效果(LOCK指令可不是内存屏障,不能画等号哦)
  内存屏障(MemoryBarrier)这里就不展开说了,再说文章越写越多了,我们这里只要知道:
  它的几个作用:确保一些特定操作执行的顺序,让cpu必须按照顺序执行指令另一个作用是强制更新一次不同CPU的缓存,保证任何试图读取该数据的线程将得到会是最新值
  instance声明为volatile之后,告诉JVM编译器不允许指令重排优化,告诉CPU不允许乱序执行。这样就保证new对象等等过程中,一个写操作完成之前,不会调用读操作。这样避免了上面示例3中的说到的问题。
  这样懒汉单例就比较完美了,即保证了效率也是线程安全的。5。饿汉式单例
  本文到现在一直介绍懒汉实现单例,我们来看下饿汉是怎么实现单例的饿汉publicclassSingleHungry{privatestaticSingleHungryinstancenewSingleHungry();privateSingleHungry(){}publicstaticSingleHungrygetInstance(){returninstance;}}
  这是饿汉实现单例的标准写法,没啥大问题,线程安全的,执行效率高
  缺点:类加载时instance就初始化了,造成资源的浪费;开发者无法手动控制类实例化的时机6。懒汉式单例静态工厂版
  介绍一下《EffectiveJava》第3版给出的方法:单例静态工厂publicclassSingleStatic{privatestaticclassSingletonHolder{publicstaticSingleStaticinstancenewSingleStatic();}privateSingleStatic(){}publicstaticSingleStaticnewInstance(){returnSingletonHolder。instance;}}
  使用方式:SingleStaticsingleStaticSingleStatic。newInstance();
  我们来看下这种实现方法的巧妙之处:从内部来看对于静态内部类SingletonHolder,它是一个饿汉式的单例实现,在SingletonHolder初始化的时候会由ClassLoader来保证同步,使INSTANCE是一个单例。同时,由于SingletonHolder是一个内部类,只在外部类的Singleton的getInstance()中被使用,所以它被加载的时机也就是在getInstance()方法第一次被调用的时候。从外部看来,又的确是懒汉式的实现
  使用类的静态内部类实现的单例模式,既保证了线程安全有保证了懒加载,同时不会因为加锁的方式耗费性能。
  推荐这种实现方法7。枚举实现单例
  最后再介绍一个《EffectiveJava》第3版推荐的写法publicenumSingleInstance{INSTANCE;publicvoidfunDo(){System。out。println(doSomething);}}
  使用方式:SingleInstance。INSTANCE。funDo()
  这种方法充分利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。除此之外,写法极其简洁。
  分外优雅!尾语
  虽然本文核心通篇是:单例可以避免创建不必要的对象,减少每次创建对象的时间开销,还可以节约内存空间
  这样可能会让一些人误以为:JAVA创建对象的代价非常昂贵,应该要尽可能地避免创建对象
  事实恰恰相反,由于小对象的构造器只做很少量的显式工作,所以小对象的创建和回收动作是非常廉价的,特别是在现代的JVM实现上更是如此通过创建附加的对象,提升程序的清晰性、简洁性和功能性,所以通常是件好事。
  单例模式真的是最简单的设计模式吗?当我们去看其字节码、汇编是如何实现的原理时,往往发现其中细节无数充满前人的智慧结晶,平时我们日常学习中不能过于功利只盯着面试题去刷,也要深入底层去挖掘实现的细节和设计原理。感谢您看到最后。
  参考资料:
  《深入理解计算机系统》
  《EffectiveJava》
  《Java虚拟机规范》
  《汇编语言》王爽
  很感谢你能看到最后,如果喜欢的话,欢迎关注点赞收藏转发,谢谢!
  计算机内功、JAVA源码、职业成长、项目实战、面试相关资料等更多精彩文章在公众号小牛呼噜噜

美媒亚马逊将开启史上最大规模裁员上万人将被辞退海外网11月15日电据美国纽约邮报报道,美国电子商务巨头亚马逊计划于本星期裁员1万人。此次裁员是亚马逊史上最大规模的裁员行动。裁员将集中在公司的销售人力资源及设备等部门。一名熟悉裁手机游戏画质看齐PC?联发科上硬件光追近几年,随着手机图形处理器以及异构计算能力的逐年提升,手机游戏端游化的概念越来越多地被提起。手机配置越来越高,手机游戏画质有更多空间放飞自我,甚至开始向PC看齐,这也是会出现原神这晋级世界杯!中国男篮8067击败巴林,赵睿最后三分球你怎么看?2023男篮世预赛第五窗口期,中国男篮8067加时赛击败巴林男篮,中国男篮两战全胜,提前获得世界杯入场券!王哲林全场比赛出战32分钟,投篮13中10,三分球1中1,得到27分14篮2022年世界杯最好的时代?卡塔尔世界杯或将成为葡萄牙球员克里斯蒂亚诺罗纳尔多参与的最后一届世界杯赛。(图视觉中国)2022年世界杯要来了。群星璀璨。哪位会说了不是啊,意大利都没晋级,当红的哈兰德和萨拉赫都去中国男篮vs巴林男篮感想上午把中国对巴林比赛又看了一遍。几点感想一常规时间打平其实赵睿责任更大,屡投不中,突破被帽,几次差点被断,特别是巴林连得5分扳平的时候,巴林那个2分上篮赵睿直接就放了,这明显是态度国产数据库发展之路国产化提升国产数据库需求,分布式数据库成为行业数据库发展机遇。作者鲁立国防科技大学计算机科学硕士,六年跨国信息技术实业经验,七年证券和基金从业经验,擅长数字经济新兴科技领域研究和个宝宝为什么会反复咳嗽幼儿咳嗽可以说是幼儿生病中最常见的一个症状,常常贯穿幼儿的整个生病周期,甚至反复咳嗽出现数周甚至数月之久。首先我们了解一下,什么是咳嗽!咳嗽在西医临床当中是一个症状,但是近年来也逐拒绝沉沦痛失冠军后绿军双基石再度进化新季率队打得比勇士强上赛季凯尔特人在新帅乌度卡上任之后,虽然赛季初期的表现并不理想,不过到了中后段,这支绿军却突然迎来苏醒,战绩直线上升,最终以东部第二的身份闯入季后赛,并且相继淘汰篮网雄鹿热火这样的全球大通胀,你的钱包缩水了吗?一分钟带你了解今年热议的话题就是美国通胀率超过8,欧元区通胀率也超过8,大家都在赶着加息提高利率,减少市场上流通的钱,抑制通货膨胀。通胀率8是个什么概念呢?简单的说,年初你钱包里有1万块,鸡蛋5星际争霸2每周突变任务地狱列车毁灭快车给我死吧焦土政策星际争霸2地狱列车每周突变任务星际争霸2每周突变任务地狱列车毁灭快车给我死吧焦土政策这种突变不怎么好玩没有挑战性我玩的是毛子,毛子玩起来太随意了星际争霸2每周突变任务地狱列车毁灭快永恒岛手游前期等级玩法攻略链接永恒岛手游1。前期冲等级前期冲级很多人都看到了有许多人第一天没有卡在29而是冲到了31级或者是35级,这个是通过压主线任务外加打怪所突破的,大家都知道每天获取的经验是有上限的,
C罗无球跑动最出色?从梅西内马尔的兼容入手,戳破罗粉谬论足球助力团C罗无球跑动最出色?从梅西内马尔的兼容入手,戳破罗粉谬论在皇马后期,C罗丧失了持球能力,开始专职吃饼,数据井喷,地位猛涨,但是依然有不少人为他惋惜,觉得他的改变是一种退而国乒3消息乒联弄错马龙国籍,梁靖崑现身合照,肖战教练传离队最近国乒在新乡世界杯取得圆满成功,最终两个项目都包揽冠军,圆满结束本站的赛事任务。在这次比赛结束之后,国乒内部发生三个非常值得关注的消息,接下来和各位球迷具体回顾一下。首先第1个消夜晚的天空都能看到东南方挂着三颗排在一条直线的三颗恒星每当夜晚天气晴朗的时候人们往往会抬头仰望天空,大家有没有发现在天空的东南面有这样三颗恒星,连在一起连成了一条直线,闪闪发亮三星连线它们位于猎户座的腰带上,名叫猎户座三星,又名福寿禄北斗卫星导航系统目前现状如何?为您汇总北斗导航系统最新消息2000年的10月31日,第一颗北斗实验卫星在西昌卫星发射基地成功发射,标志着我国完全自主研发的卫星导航系统北斗导航系统正式进入组网阶段。那么经过22年的发展,北斗导航系统目前的现太阳笑了?可能是坏笑外媒NASA捕捉到太阳的微笑,却预示地磁暴有可能袭击地球微笑的太阳有可能给地球制造问题,美国全国公共广播电台(NPR)英国卫报美国趣味科学网站等媒体29日消息,当地时间10月26日一起来拍中国空间站今天下午,中国空间站第三大舱段梦天实验舱顺利发射升空,并将在随后与空间站组合体进行交会对接。对接完成后,中国空间站将形成三舱T字基本构型,中国载人航天工程三步走发展战略也将迈出重要独孤月与马兰星新传宇宙之锤爆裂的一瞬间,宇宙空间发生扭曲,独孤月看到眼前一片光芒,然后在远处出现一个黑点,自己随着宇宙碎片向黑点里飞去。在飞行的过程中,他的身体时而被拉长,时而被压扁,又时又四分五裂宇宙到这里终止?科学家从韦伯望远镜拍摄的新图像中发现了什么?头条创作挑战赛在哈勃望远镜首次拍摄到创世之柱的27年后,同样的结构出现在韦伯望远镜令人惊叹的新图像中。它们使科学家能够比以往更详细地研究鹰状星云的独特部分。韦伯望远镜拍摄了创世之柱第三批航天员即将登场10月最后一天,中国空间站最后一块拼图,梦天号实验舱就要在文昌启航,经过十几个小时追逐后,会和组合体对接,后续经过转位对接于永久停泊口,形成与核心舱,问天实验舱的三舱T字型构型,从新一轮较量开始了,欧美等国被我国卫星紧盯,得到联合国大力支持人们常说科技改变生活,以前人们都只是停留在追求飞机飞向天空这样的需求,但随着科技的不断发展,这些已经无法满足人类的持续发展,人们借助强大的科技,开始向外太空迈进。卫星的出现可以说给今年双十一平板怎么选?这三款产品实力很强,价格最低仅2K出头对于笔者而言,平板是生活中不可或缺的一类数码产品,它不仅拥有着比笔记本更加方便携带的优势,也有着比平时使用的手机更大的屏幕,配合上智能键盘,日常的办公不是问题,如果再加上手写笔,拿
友情链接:快好找快生活快百科快传网中准网文好找聚热点快软网