面试中的单例问题

给大家普及一下,我在面试中遇到的单例问题。

当我兴冲冲的带着笔记答案参加面试时,突然发现面前的面试官显得很严肃而且眉头紧锁,不知道是工作太累了,还是说他对今天的面试者不是很满意。

于是我就勇敢的坐过去在他的面前坐了下来,没想到第一道题就让面试官看出了我的水平,因此今天跟大家聊聊面试中单例的问题,希望大家都能了解这块内容。

在早期的项目代码中,如果我们想使用类的某个方法,我们基本都会创建一个类的对象实例然后再调用方法,这样的实现往往在系统内就会存在某个类的大量实例。如此一来,项目框架很难管理大量的对象,而且如果java虚拟机不能及时回收,容易造成内存溢出。

首先我们要明白什么是单例,所谓单例就是说在项目框架内某个类的对象实例只存在一个,任何调用方获取到的对象实例都是一个,那么很明显这个类是不能够被外部直接调用类构造器创建的。

只适合单线程环境(不好)

我们先看下一个简单的单例设计:

上面代码在单线程是没有问题的,而且只有当线程调用类的静态方法时,才会生成类的静态变量。但是当多线程访问时,上面代码是有问题的,会生成多个对象的实例。

例如:当两个线程同时运行到判断instance是否为空的if语句,并且instance确实没有创建好时,那么两个线程都会创建一个实例。


多线程的情况(懒汉式,不好)

那么,我们可以用另外一种方法实现,比如说在类加载时候就初始化对象的实例,这样后面无论怎么调用类静态方法都不创建新的实例。还有一种方法,但是会牺牲部分系统性能,意思就是在多线程访问方法时通过锁机制让线程排队访问。我们先通过在类方法上加锁来实现类的单例,比如:

注解:在解法一的基础上加上了同步锁,使得在多线程的情况下可以用。

例如:当两个线程同时想创建实例,由于在一个时刻只有一个线程能得到同步锁,当第一个线程加上锁以后,第二个线程只能等待。

第一个线程发现实例没有创建,创建之。第一个线程释放同步锁,第二个线程才可以加上同步锁,执行下面的代码。由于第一个线程已经创建了实例,所以第二个线程不需要创建实例。保证在多线程的环境下也只有一个实例。

缺点:每次通过getInstance方法得到singleton实例的时候都有一个试图去获取同步锁的过程。而众所周知,加锁是很耗时的。能避免则避免。


双重锁(Double CheckLock)机制

一方面需要在实例上加上volatile关键字 通知操作系统实现线程访问时内存屏障,然后还需要在方法中通过虚拟机实现的synchronized来同步方法访问,写法如下:

注解:只有当 singleton 为 null 时,需要获取同步锁,创建一次实例。当实例被创建,则无需试图加锁。

缺点:用 双重 if 判断,复杂,容易出错。


饿汉式(建议使用)

如果说我们不考虑服务负载问题,在多线程环境下可以预先加载类的静态实例,当虚拟机加载完成类后就会创建类的静态变量,甭管你到时用不用,反正给你留在那里。所有线程访问到的都是同一静态实例,有人也称这种方式为饿汉式,具体写法如下:

注解:初试化静态的 singleton 创建一次。如果我们在 Singleton类 里面写一个静态的方法不需要创建实例,它仍然会早早的创建一次实例。而降低内存的使用率。

缺点:没有 延迟加载 的效果,从而降低内存的使用率。

静态内部内(建议使用)

上面写法实现单例也是没有问题的,但是有些同学就会觉得如果我只是想调用一个类的某个静态方法,并不想生成它的实例,那有没有其他方法呢,经过各路大神的指点结合自身的总结,可以使用内部静态类来实现这个需求。

开发的同学都知道,虚拟机在加载类的过程中一开始并不会初始化类的内部静态类。如果线程调用内部静态类时,虚拟机只会初始化一次,这样既可以实现单例,同时也是线程安全的。具体写法如下:

注解:定义一个私有的内部类,在第一次用这个嵌套类时,会创建一个实例。而类型为SingletonHolder的类,只有在Singleton.getInstance()中调用时,由于私有的属性,他人无法使用SingletonHolder,不调用Singleton.getInstance()就不会创建实例。

优点:达到了 延迟加载 的效果,即按需创建实例。


枚举

除了以上讲到的几种方式外,JDK自身的枚举类型本身就是单例的实现,调用者不能显式的调用构造器完成实例创建,因此很多Java规范文档推荐使用枚举来实现单例。

上面提到的四种实现多线程下单例的方式都有共同的缺点:

1)需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。

2)可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)

而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,《Effective Java》作者推荐使用的方法。不过,在实际工作中,很少看见有人这么写。


总结

本文总结了五种Java中实现单例的方法,其中懒汉式和饿汉式两种都不够完美,双重校验锁 和 静态内部类的方式可以解决大部分问题,平时工作中使用的最多的也是这两种方式。枚举方式虽然很完美的解决了各种问题,但是这种写法多少让人感觉有些生疏。

个人的建议是,在没有特殊需求的情况下,使用 双重校验锁静态内部类 方式实现单例模式。


  目录