本文转载自微信公众号《三太子敖丙》,作者三太子敖丙。转载本文请联系三太子敖丙公众号。不知道大家在工作或者面试的时候有没有遇到过单例模式。面试的时候记得2017年第一次实习的时候遇到了单例模式,面试官就是我后来的leader。我手写了单个案例。我记得我写过饿汉式和懒汉式,但是没分清懒汉和反派的区别。当时他给我解释了一下,我就知道了其中的奥妙。在写这篇文章之前,我特意在手头的项目中进行了搜索。我发现每个项目都会用到单例,后面提到的实现基本都会涉及到。这很有趣。一开始给大家提一个思考问题:为什么不用静态方法代替单例模式呢?我会在最后公布问题的答案,大家可以带着问题阅读,看看你们的思路是不是和我一样。你肯定也经常听到身边的同学说单例很简单,自己也能搞定,但是真正来到自己身边的时候,能不能把一个知识点讲透,能不能发散性思考,引出更多的答案呢?或者你能说出每种模式更适合他的情况吗?这是值得深思的。先给单例下一个定义:在当前流程中,通过单例模式创建的类只有一个实例。单例具有以下特点:在Java应用程序中,单例模式可以保证对象在一个JVM中只存在一个实例。构造函数必须是私有的,外部类不能通过调用构造函数方法来创建实例。没有publicset方法,外部类无法调用set方法创建实例提供publicget方法获取唯一实例,那么单例模式有什么好处呢?有些类的创建比较频繁,对于一些大对象来说,这是一个很大的系统开销,省去new操作符,降低系统内存使用频率,减轻GC压力。系统中的一些类,比如spring中的controller,控制着处理流程。如果可以创建多个类,系统就彻底乱套了,避免重复占用资源。单例模式的定义也清楚了,好处也明白了。首先看一个饿汉式写法饿汉式publicclassSingleton{//创建一个实例对象privatestaticSingletoninstance=newSingleton();/***防止实例化的私有构造方法*/privateSingleton(){}/***staticgetmethod*/publicstaticSingletongetInstance(){returninstance;}}之所以叫饿了中国式可以理解为他饿了,他想提前new出对象,这样即使别人拿到this的对象第一次创建类,这个类会直接存在,省去了创建类的开销。懒人我介绍完了,大家对比一下就知道两者的区别和适用场景了。懒惰式线程-不安全模式publicclassSingleton{privatestaticSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instance==null){instance=newSingleton();}returninstance;}}懒惰式大家可以理解他懒,别人先调用一次,发现他的实例是空的,然后初始化,然后赋值,后面的调用就和小人没什么两样了。lazy和villain的对比:可以发现两者的区别基本上就是第一次创建的开销,以及线程安全问题(thread-unsafe模式下的lazy)。这样一对比,他们的场景就很容易理解了。在很多电商场景下,如果数据是频繁访问的热点数据,那么我可以使用小人模式在系统启动时提前加载(类似于缓存预加载)。Hot)这样即使第一次用户调用也不会有创建开销,频繁调用也不会有内存浪费。至于懒人风,我们可以在不太热的地方使用。比如你不确定是否有人会长时间调用数据,那就用懒人吧。被人调用,提前加载的类是内存资源的浪费。我没有故意在线程安全问题上锁定懒人。懒人的线程安全问题大家肯定都知道了吧????在运行过程中可能会出现这样一种情况:多个线程调用getInstance方法获取Singleton的实例,那么可能会出现这样的情况,当第一个线程在执行if(instance==null)时,此时instance是null的入口语句。当instance=newSingleton()还没有执行完(此时instance为null),第二个线程也进入if(instance==null)语句,因为进入这条语句的线程还没有执行instance=newSingleton(),所以它会执行instance=newSingleton()来实例化Singleton对象,因为第二个线程也进入了if语句所以它会实例化Singleton对象。这就导致实例化了两个Singleton对象,那么怎么解决呢?简单粗暴,就是加锁。这是锁定后的代码。publicclassSingleton{privatestaticSingletoninstance=null;/***私有构造方法防止被实例化*/privateSingleton(){}/***静态get方法*/publicstaticsynchronizedSingletongetInstance(){if(instance==null){instance=newSingleton();}returninstance;}}这是一种典型的时间交换空间的方式。无论发生什么情况,每次创建实例时,都是先加锁再判断,严重降低了系统的处理速度。有没有更好的方法来处理它?是的,通过double-checklock做两个判断,代码如下:publicclassSingleton{privatestaticSingletoninstance=null;privateSingleton(){}publicstaticSingletongetInstance(){//首先检查实例是否存在,如果不存在则进入下面的同步块if(instance==null){//同步块,线程安全创建实例synchronized(Singleton.class){//再次检查实例是否存在,不存在则创建真正的实例if(instance==null){instance=newSingleton();}}}returninstance;}}里面加了synchronized关键字,也就是说调用的时候不需要加锁,只有当instance为null,创建对象的时候加锁才行在一定的时间需要,并且性能得到了一定程度的提高。然而,这样就没有问题了吗?看下面这种情况:Java指令中的对象创建和赋值操作是分开进行的,也就是说instance=newSingleton();语句分两步执行。但是JVM不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给实例成员,然后初始化Singleton实例。这可能会出错。以A、B两个线程为例:A、B线程先进入if判断A先进入synchronized块。由于实例为null,所以执行instance=newSingleton();由于JVM内部的优化机制,JVM先抽取一些分配给Singleton实例的空白内存分配给实例成员(注意此时JVM还没有开始初始化实例),然后A离开同步块。image-20201212010622553B进入同步块。由于此时instance不为null,所以立即离开synchronized块,将结果返回给调用该方法的程序。这时B线程打算使用Singleton实例,但是发现还没有初始化,于是报错。添加volatile修饰Singleton,再做一个优化:publicclassSingleton{privatevolatilestaticSingletoninstance=null;privateSingleton(){}publicstaticSingletongetInstance(){//首先检查实例是否存在,如果不存在,则进入下面的同步块if(instance==null){//同步块,线程安全创建实例synchronized(Singleton.class){//再次检查实例是否存在,不存在则创建真正的实例if(instance==null){instance=newSingleton();}}}returninstance;}}**volatile修饰的变量不会被线程缓存到本地,所有线程对该对象的读写都会第一时间同步到主存,从而保证对象在多线程间的准确性**volatile的作用是防止指令重排序,因为instance=newSingleton()不是保证内存可见的原子操作。这是一种比较完美的写法。这种方法可以安全地创建一个唯一的实例,而不会对性能造成太大影响。影响。但是由于volatile关键字可能屏蔽了虚拟机中一些必要的代码优化,运行效率不是很高。有没有更好的写法?使用静态内部类publicclassSingleton{/*private构造方法防止被实例化*/privateSingleton(){}/*这里使用一个内部类维护一个单例*/privatestaticclassSingletonFactory{privatestaticSingletoninstance=newSingleton();}/*Getinstance*/publicstaticSingletongetInstance(){returnSingletonFactory.instance;}/*如果对象用于序列化,可以保证对象在序列化前后保持一致*/publicObjectreadResolve(){returngetInstance();}}使用内部类来维护单例的实现,JVM的内部机制可以保证在加载一个类时,这个类的加载过程是线程互斥的。这样,当我们第一次调用getInstance时,JVM可以帮我们保证实例只被创建一次,并且会保证分配给实例的内存被初始化,这样我们就不用担心以上问题。同时该方法只会在第一次调用时使用互斥机制,解决了性能低下的问题。这样,我们就暂时总结出一个完美的单例模式。有没有更完美的写法,通过枚举:publicenumSingleton{/***定义一个枚举元素,代表Singleton的一个实例。*/Instance;}使用枚举实现单实例控制会更简洁,JVM从根本上提供了绝对防止多次实例化的保证,是一种更简洁、高效、安全的单实例实现方式。最后一个也是我最喜欢的(代码少)。总结最后大家应该知道单例模式的写法,以及优缺点和使用场景。你有答案一开始的问题吗?什么?你连问题都忘了?问题:为什么不使用静态方法而不是单例模式?两者其实都可以达到我们加载的最终目的,只不过其中一个是基于对象的,一个是面向对象的。就像我们不用面向对象也能解决问题一样,面向对象代码提供了更好的编程思想。如果一个方法与其类的实例对象无关,那么它应该是静态的,否则就应该是非静态的。如果我们真的应该使用非静态方法,但我们真的只需要在创建类时维护一个实例,我们就需要使用单例模式。我们的电子商务系统中有很多类,有很多配置和属性。这些配置和属性必须存在并且是公共的。同时,它们需要在整个生命周期中都存在,所以只需要一份。这时候如果再需要的时候又需要一个新的,然后再给它赋值,显然是浪费内存,再赋值也没有意义。所以我们使用单例模式或者静态方法来维护这些值的一个副本并且只有这个值,但是此时这些配置和属性是通过面向对象编码获取的,我们应该使用单例模式,或者它不是对象-面向对象,但它自身的属性应该是面向对象的。虽然我们可以使用静态方法来解决同样的问题,但是最好的解决方案应该也是使用单例模式。资讯参考:?,《为什么要用单例模式?》好了,以上就是本期的全部内容了。我是敖丙你知道的越多,你不知道的就越多。下期见。
