今天再来聊聊单例设计模式

今天咱们再来探讨一下单例设计模式,能够说,单例设计模式在面试考察中是最常出现的,单例模式看似简单,每一个人可能均可以写出来,可是能不能写好就是一个问题,往深了考察,又能考察出面试者对于并发、类加载、序列化的掌握程度。java

单例有多种写法,可是哪一种写法更好的,为何好呢,这些问题咱们都会在今天的文中一一解读,首先咱们须要知道什么是单例模式。面试

什么是单例模式?

单例模式指的是,保证一个类只有一个实例,而且提供一个全局能够访问的入口。数据库

图片

那么咱们为何须要单例呢?

其中第一个理由就是节省内存、节省计算。在咱们平时的程序中,咱们不少时候就只须要一个实例就够了,若是出现了更多的实例反而属于浪费。设计模式

举个例子,咱们就拿一个初始化比较耗时的类来讲,在这个类构造的时候,须要查询数据库,并对查到的数据作大量的计算,因此在第一次构造的时候,咱们花了不少时间来初始化这个对象,假设咱们数据库里的数据是不变的,而且把这个对象保存在了内存中,那么之后就可使用同一个实例了,若是每次生成新的实例就没有必要了。安全

第二个理由就是为了保证结果的正确,比咱们须要一个全局的计数器,用来统计人数,若是有多个实例,反而可能会形成混乱。多线程

第三个理由就是方便管理,不少工具类咱们只须要一个实例,咱们经过一个统一的入口,好比经过getInstance方法,就能够获取到这个单例,这是很方便的,太多的实例不但没有帮助,反而会显得有点混乱。并发

单例模式有哪些使用场景?

  • 无状态工具类:如日志工具类、字符串工具类...ide

  • 全局信息类:如全局计数、环境变量类...函数

常见单例模式的写法:工具

主要有五种:饿汉式、懒汉式、双重检查式、静态内部类式、枚举式。接下来根据难度依次展开讲述:

1. 饿汉式

public class Singleton{    private static Singleton singleton = new Singleton();     private Singleton(){}     public static Singleton getInstance(){        return singleton;    }}

咱们来看看饿汉式的写法,用static修饰咱们的实例,而且把构造函数用private修饰,这种写法比较简单,在类装载的时候就完成了实例化,避免了线程同步的问题,缺点就在于类装载的时候就完成了实例化,没有达到懒加载的要求,若是从始至终都没有使用过这个实例,就可能会形成内存的浪费。

还有一种方式与饿汉式比较相似,就是静态代码块式;

public class Singleton{    private static Singleton singleton;     static{      singleton = new Singleton();    }     private Singleton(){}     public static Singleton getInstance(){        return singleton;    }}

这种方式和饿汉式相似,只不过把类实例化的过程放到了静态代码块中,也是在类装载的时候就执行了静态代码块中的代码,完成了实例化。因此这种方式和饿汉式的优缺点也是同样的。

2. 懒汉式

在了解了饿汉式的缺点以后咱们来看看第二种写法,懒汉式,这种写法在getInstance方法被调用的时候,才去实例化咱们的实例,可是只能在单线程下使用。若是在多线程下使用,若是一个线程进入了if(singleton == null)判断语句块,还没来得及往下执行,另外一个线程也经过了这个判断语句,这时就会屡次建立实例。因此这里须要注意,多线程环境下不能使用这种方式。

public class Singleton{    private static Singleton singleton;     private Singleton(){}     public static Singleton getInstance(){      if (singleton == null) {        singleton = new Singleton();      }        return singleton;    }}

若是要保证线程安全,咱们能够对前边的写法进行升级,线程安全的懒汉式是怎么样的呢,咱们能够在getInstance方法上加synchronized关键字,这样就能够解决上边出现的线程安全问题。不过就是效率过低了,每一个线程在得到类的实例的时候,执行getInstance方法,都要进行同步,多个线程不能同时访问,然而这在大多数状况下都是没有必要的。

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

这个地方有人会说,把synchronized关键字加在方法上效率过低了,那么缩小范围,把synchronized从方法上移除,而后把synchronized关键字放到了咱们的方法内部,采用了代码块的形式来保证线程安全,不过这种方法是有问题的,有可能产生多个实例。加入一个线程进入了第一个if(singleton == null)判断语句块,还没来得及往下执行,此时又一个线程经过了这个判断,此时就会产生多个实例。

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

3. 双重检查模式

双重检查模式的出现就是为了解决上边出现的问题,就有了双重检查模式。

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

咱们重点来看一下getInstance方法,咱们进行了两次singleton == null的判断,就能够保证线程安全了,这样实例化代码只用调用一次,后面再次访问的时候,只会判断第一次的if (singleton == null)就能够了,而后会跳过整个if块,直接返回实例化对象,这种写法的好处就是,不只线程安全,并且延迟加载,效率也更高。

这里就会出现一个面试题,为何是两次判断,去掉第二次的if判断行不行?

这个时候须要考虑这样一种状况,有两个线程同时调用了getInstance方法,而且因为singleton是空的,因此两个线程均可以经过第一个if判断,而后因为锁机制,会有一个线程先进入同步语句,并进入第二个if判断,而另一个线程须要等待锁释放,不过当第一个线程执行完new Singleton()语句后,就会退出synchrinized保护的区域,这时若是没有第二个判断,那么第二个线程也会建立一个实例,这就破坏了单例。那么去掉第一个判断,全部的线程都会串行执行,效率低下。综上,两个判断都是须要保留的。

还有一个点须要注意,咱们给Singleton对象加了volatile关键字修饰,这又是为何呢?

这主要在于singleton = new Singleton()这句,这并非一个原子操做,在JVM中,这条语句至少作了三件事,第一步,给singleton分配内存空间;第二步,调用Singleton的构造函数等来初始化singleton;第三步,讲singleton对象指向分配的内存空间(执行完这一步singleton就不是null了)。

图片

这个地方须要注意一下1-2-3的顺序,存在着重排序的优化,也就是说第二步和第三步的顺序不能保证的,最终的执行顺序多是1-2-3,也多是1-3-2。

图片

若是顺序是1-3-2,那么第3步执行完以后,singleton就不是null了,但是此时并无执行第2步,假设此时又有一个线程进入了getInstance方法,因为此时的singleton已经不是null了,就会经过第一个判断,直接返回对象,其实这个时候的singleton并无完成初始化,因此使用这个实例的时候就会报错。

使用volatile的意义就在于,它能够防止上边出现的那种重排序的发生,也就避免了拿到未完成初始化的对象。

4. 静态内部类

静态内部类的方式和饿汉式采用的机制有点相似,都采用了类装载的机制,来保证咱们初始化实例时只有一个线程。因此在这个地方是由JVM实现的一个线程安全。

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

饿汉式 的方式,在类被加载的时候,就会实例化对象,而静态内部类方法在Singleton类被装载时,并不会被马上实例化,只有在调用getInstance方法的时候,才会进行实例化。

看到这里咱们已经学会了双重检查和静态内部类两种方法来线程 安全、高效、延迟加载的建立单例,这两种方式都是不错的写法,可是它们不能防止被反序列化,生成多个实例。

5. 枚举法

借助枚举类来实现单例,这不只能避免多线程同步的问题,并且还能反正反序列化,和反射建立新的对象,来破坏单例状况的出现。

public enum Singleton {  INSTANCE;  public void whatverMethod() {   }}

至此,咱们已经学了五种方法实现单例,可是怎么选择呢,其实仍是优先推荐枚举法,仍是要看看枚举写法的优势,枚举写法的优势:

其一是写法简单,不须要咱们去考虑线程安全和懒加载,代码也比较短小精悍,是最简练的写法。

其二是线程安全有保障,经过反编译一个枚举类,咱们发现枚举类中的各个枚举项是经过static代码块来定义和初始化的,它们会在类加载的时候完成初始化,而Java类的加载由JVM保证线程安全,因此建立一个Enum类型枚举类是线程安全的。前面几种实现单例的方式都存在一些问题,那就是可能被反序列化破坏,反序列化生成的新的对象从而产生了多个实例。

其三是防止破坏单例,Java对于枚举的序列化作了要求,仅仅是将枚举类对象的name属性输出到结果中,在反序列化时,就是经过java.lang.Enum的valueOf方法,来根据名字查找对象,而不是新建一个新的对象,因此这就防止了反序列化致使的单例破坏问题的出现。对于反射破坏单例的问题,枚举一样有措施,反射在经过newInstance建立对象时,会检查这个类是否是枚举类,若是是枚举类就抛出illegalArgumentException("Cannot reflectively create enum objects")异常,反射建立对象失败。能够看出枚举是能够防止反序列化和发射破坏单例。这就是枚举在实现单例上的优点。

相关文章
相关标签/搜索