在运行时确保一个类只有一个实例,并提供该实例的全局访问点。
在多种写法中需要考虑到以下几点:1.是否线程安全;2.是否懒加载;3.是否能通过反射,序列化破坏
饿汉式
饿汉式实现的单例模式不会产生线程不安全的问题。但是直接实例化的方式在类加载就初始化了,可能占用不必要的内存。
1 | package cn.xiaohupao.singleton; |
懒汉式——线程不安全
在该实现中,私有静态变量被延迟初始化——在使用时才初始化。如果没有用到该类,则不会实例化lazyMan,从而节约资源。但是该实现的单例模式在多线程环境下将会被破坏(线程不安全的)。如果多个线程同时进入到判断条件if (lazyMan == null)时,并且此时lazyMan为null,那么将会有多个线程执行实例化操作即:lazyMan = new LazyMan();这将导致实例化多次lazyMan。
1 | package cn.xiaohupao.singleton; |
懒汉式——线程安全之同步方法
在getInstance方法上加锁,那么在一个时间点只能有一个线程进入到该方法,从而避免了多次实例化lazyMan2。但当一个线程进入该方法之后,其他线程都必须等待,即使当lazyMan2已经被实例化后,也需要等待。这将让线程阻塞时间过长,有一定的性能损耗。
1 | package cn.xiaohupao.singleton; |
懒汉式——线程安全之双重检验锁(DCL)
在该实现中,lazyMan3只需要被实例化一次,在这之后的判断不会涉及到多线程排队取对象的问题。加锁的部分只对实例化的代码,只有当lazyMan3没有被实例化时,才需要进行加锁。双重检验锁先判断lazyMan3是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁。
注意,对lazyMan3采用volatile关键字修饰也是十分有必要的。我们注意在执行实例化操作时,使用构造方法不是原子性操作。构造方法的可以分解为三步执行:1.为lazyMan3分配内存空间;2.初始化lazyMan3;3.将lazyMan3指向分配的内存地址。但是由于JVM具有指令重排的特性,执行顺序有可能为1->3->2,此时当第二个线程调用getInstance()后发现lazyMan3不为空,因此返回lazyMan3,但此时lazyMan3还未被初始化。
使用volatile关键字可以禁止JVM的指令重排,保证在多线程环境中也能正常运行。
1 | package cn.xiaohupao.singleton; |
静态内部类实现
当Holder类被加载时,静态内部类SingletonHolder没有被加载进内存。只有当调用getInstance()方法从而触发SingletonHolder.HOLDER时SingletonHolder才会被加载,此时HOLDER初始化实例,并且JVM保证HOLDER只被实例化一次。这种方法不仅具有延迟初始化的好处,而且JVM提供了对线程安全的支持。
1 | package cn.xiaohupao.singleton; |
上述的所有方法都可以通过反射机制来破坏单例模式。若想防止反射攻击,与序列化破坏单例模式。反射机制可以通过setAccessible()方法可以将私有构造器的访问级别设置为public,然后调用构造函数从而实现实例化对象,若要防止这种攻击,可以在构造函数中添加防止多次实例化的代码。
使用枚举类实现单例
使用枚举类实现单例可以防止通过反射机制和序列化来破坏单例。但是枚举类无法实现懒加载,它在程序启动之初,那就已经把这个内部的实例完全构建好来提供给使用者。
1 | package cn.xiaohupao.singleton; |