当前位置: 首页 > 科技观察

单例模式的这些细节你都知道吗?

时间:2023-03-16 18:40:26 科技观察

本文转载自微信公众号“冰冰”,作者冰冰。转载本文请联系冰冰公众号。从去年开始,Java程序员跳槽和面试的难度越来越大。不懂多线程,JVM,mysql,一些分布式组件都不好意思出去面试。经常听身边的同学说,面试题很简单,自己也能做,但是真正来到自己的时候,能不能把一个知识点讲透,能不能发散思考,引出更多的答案?这份手写的单例模式的A笔试题,应该是很多资深程序员都会遇到的吧。他们第一眼看到题目的时候,可能会感到惊讶和庆幸,但是你真的能100%做好这道题吗?请手写单例,要求线程安全。先给单例下一个定义:在当前流程中,通过单例模式创建的类只有一个实例。单例具有以下特点:在Java应用程序中,单例模式可以保证对象在一个JVM中只存在一个实例。构造函数必须是私有的,外部类不能通过调用构造函数方法来创建实例。没有publicset方法,外部类无法调用set方法创建实例。提供一个publicget方法获取唯一实例。单例模式有什么好处?有些类的创建比较频繁,对于一些大对象来说,这是一个很大的系统开销,省去new操作符,降低系统内存使用频率,减轻GC压力。系统中的一些类,比如spring中的controller,控制着处理流程。如果可以创建多个类,系统就彻底乱了,单例模式的定义也清楚了,好处也明白了。首先看一个饿汉式的写法publicclassSingleton{privatestaticSingletoninstance=newSingleton();/***私有构造方法防止被实例化*/privateSingleton(){}/***静态get方法*/publicstaticSingletongetInstance(){returninstance;}}如果你在面试时提供了这个答案,那么冰冰建议你先回家好好学习两个月,然后再出来找工作。你必须说这太低了。乍一看,线程并不安全。方法要通过synchronized加锁,同时在创建前验证。改造后写成: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从根本上提供了绝对防止多次实例化的保证,是一种更简洁、高效、安全的单实例实现方式。