单例设计模式概念

就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。如果我们要让类在一个虚拟机中只能产生一个对象,我们首先必须将类的构造器的访问权限设置为private,这样,就不能用new操作符在类的外部产生类的对象了,但在类内部仍可以产生该类的对象。因为在类的外部开始还无法得到类的对象,只能调用该类的某个静态方法以返回类内部创建的对象,静态方法只能访问类中的静态成员变量,所以,指向类内部产生的该类对象的变量也必须定义成静态的。

饿汉式

简单的饿汉式

案例:

static变量在类加载的时候初始化,此时不会涉及到多个线程对象访问该对象的问题,虚拟机保证只会装载一次该类,肯定不会发生并发问题,无需使用synchronized 关键字

静态代码块

两种写法的总结

饿汉式的这两种写法都是一样的,在类初始化的时候就创建了一个对象实例,当调用这个静态方法的时候返回的也是同一个对象实例,符合单例的设计思想。但是如果使用的单例比较多的情况下,使用这种方法在加载类的时候就会耗费很多资源,有些单例类可能暂时用不上,造成资源浪费。

存在的问题:如果只是加载了本类,而并不需要调用getUser,则会造成资源的浪费。

总结:线程安全、非懒加载、效率高,资源浪费

懒汉式

延迟对象的创建

方式1:普通创建

如果是多线程环境,以上代码会出现线程安全问题。

方式2:方法加锁

以上使用同步方法会造成每次获取实例的线程都要等锁,会对系统性能造成影响,未能完全发挥系统性能,可使用同步代码块来解决

方式3:双重检查锁(推荐)

对于 getInstance() 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式

为什么判断两次instance==null

第一次判断是在代码块前,第二次是进入代码块后,第二个判断想必都知道,多个线程都堵到代码块前等待锁的释放,进入代码块后要获取到最新的instance值,如果为空就进行创建对象。 那么为什么还要进行第一个判断,第一个判断起到优化作用,假设如果instance已经不为空了,那么没有第一个判断仍然会有线程堵在代码块前等待进一步判断,所以如果不为空,有了第一个判断就不用再去进入代码块进行判断,也就不用再去等锁了,直接返回。

为什么要加volatile?

是为了防止指令重排序,给私有变量加 volatile 主要是为了防止第 ② 处执行时,也就是“instance = new Singleton()”执行时的指令重排序的,这行代码看似只是一个创建对象的过程,然而它的实际执行却分为以下 3 步:

  1. 创建内存空间。

  2. 在内存空间中初始化对象 Singleton。

  3. 将内存地址赋值给 instance 对象(执行了此步骤,instance 就不等于 null 了)。

试想一下,如果不加 volatile,那么线程A在执行到上述代码的第 ② 处时就可能会执行指令重排序,将原本是 1、2、3 的执行顺序,重排为 1、3、2。但是特殊情况下,线程 A在执行完第 3 步之后,如果来了线程 B执行到上述代码的第 ① 处,判断 instance 对象已经不为 null,但此时线程 A还未将对象实例化完,那么线程B将会得到一个被实例化“一半”的对象,从而导致程序执行出错,这就是为什么要给私有变量添加 volatile 的原因了。

优化作用,synchronized块只有执行完才会同步到主内存,那么比如说instance刚创建完成,不为空,但还没有跳出synchronized块,此时又有10000个线程调用方法,那么如果没有volatile,此使instance在主内存中仍然为空,这一万个线程仍然要通过第一次判断,进入代码块前进行等待,正是有了volatile,一旦instance改变,那么便会同步到主内存,即使没有出synchronized块,instance仍然同步到了主内存,通过不了第一个判断也就避免了新加的10000个线程进入去争取锁。

总结:线程安全、懒加载、效率高。

静态内部类-延迟初始化占位类(推荐)

这里用了Java的类加载机制,静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。也就是说,静态内部类只有使用的时候才会被加载。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。

第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。

静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。但是使用反射强制获取构造方法的时候就会破环单例,使用反射获得的构造方法可以创建无数个实例。

总结:线程安全、懒加载、效率高。

注册式

枚举

枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。

提供了序列化机制,保证线程安全,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。

枚举方式属于饿汉式方式,程序启动时即占用内存,可能造成资源浪费。因此存在大规模生产单例的问题。

总结:线程安全、非懒加载、效率高。

容器式

这其实是枚举单例的优化版本,解决了可能造成内存浪费的问题,但是引发了线程安全问题,Spring IOC 容器使用的这种方式,只不过Spring IOC 容器做了优化。

几种方式对比

方式优点缺点饿汉式线程安全、效率高非懒加载,资源浪费懒汉式synchronized方法线程安全、懒加载效率低懒汉式双重检测线程安全、懒加载、效率高无静态内部类线程安全、懒加载、效率高无枚举线程安全、效率高非懒加载,资源浪费容器式懒加载、效率高线程不安全 可能有人看了以上表格,觉得枚举有缺点,为什么Joshua Bloch还推荐使用枚举?

这就要提到单例的破解了。普通的单例模式是可以通过反射和序列化/反序列化来破解的,而Enum由于自身的特性问题,是无法破解的。当然,由于这种情况基本不会出现,因此我们在使用单例模式的时候也比较少考虑这个问题。

枚举类是实现单例模式最好的方式

在单例模式的实现中,除去枚举方法实现的单例模式,其它的实现都可以利用反射构造新的对象,从而破坏单例模式,但是枚举就不行,下面说说原因:

破坏单例的方式有 3 种,反射、克隆以及序列化,下面详细介绍:

反射

常见的单例模式实现中,往往有一个私有的构造函数,防止外部程序的调用,但是通过反射可以轻而易举的破坏这个限制:

显然,通过反射可以破坏所有含有无参构造器的单例类,如可以破坏懒汉式、饿汉式、静态内部类的单例模式

但是反射无法破坏通过枚举实现的单例模式,利用反射构造新的对象,由于 enum 没有无参构造器,结果会抛出 NoSuchMethodException 异常

枚举安全的原因解释: 对 EnumSingleton 文件进行反编译,可以发现 EnumSingleton 继承于 Enum,而 Enum 类确实没有无参的构造器,所以抛出 NoSuchMethodException。

进一步,通过调用父类有参构造器构造枚举实例对象,程序又抛出 IllegalArgumentException 异常。

因为 Constructor 的 newInstance 方法限定了 clazz 的类型不能是 enum,否则抛出异常

所以枚举类不能通过反射构建构造函数的方式构建新的实例

序列化

先看看通过序列化破坏单例的例子,其中 Singleton 实现了 Serializable 接口,才有可能通过序列化破坏单例。

枚举类实现,枚举类不实现 Serializable 接口,都可以进行序列化,并且返回原来的单例。

原因: 枚举类的 writeObject 方法仅仅是将 Enum.name 写到文件中,反序列化时,根据 readObject 方法的源码定位到 Enum 的 valueOf 方法,他会根据名称返回原来的对象。

克隆

实现 Cloneable 接口重写 clone 方法,但是 Enum 类中 clone 的方法是 final 类型,无法重写,也就不能通过克隆破坏单例。

不用枚举如何防止单例模式破坏

序列化

若实现了序列化接口,重写 readResolve 方法即可,反序列化时将调用该方法返回对象实例

readResolve()方法是 Java 序列化机制提供的一个特殊钩子(hook)。它的核心作用在于:当对象通过 ObjectInputStream被反序列化时,如果该对象的类定义了这个方法,那么序列化机制会自动调用它,并用这个方法的返回值替换掉默认反序列化过程新创建的那个对象。

正是利用了这个特性,我们可以在单例类中重写 readResolve()方法,让其返回已经存在的那个唯一实例,从而“欺骗”序列化系统,让它放弃新创建的对象,转而使用我们提供的单例

重写 readResolve 方法

反射

通过反射破坏单例的场景,可以在构造方法中判断实例是否已经创建,若已创建则抛出异常

克隆

通过clone破坏单例的场景,可以重写clone方法,返回已有单例对象。但更重要的是,单例场景就不应该同时实现克隆能力,单例和克隆本身就是互斥的

Runtime类

Runtime类就是使用的单例设计模式。

可以看出Runtime类使用的是饿汉式(静态属性)方式来实现单例模式的。