定义
保证一个类仅有一个实例,并提供一个全局访问点。
类型:创建型。
适用场景
- 想确保任何情况下都绝对只有一个实例。
优点
- 在内存里只有一个实例,减少了内存的开销。
- 可以避免对资源的多重占用。
- 设置全局访问点,严格控制访问。
缺点
1.没有对外的接口,扩展困难。
重点
- 私有构造器,不能让外部new出该对象。
- 线程安全,防止在同一时刻多个线程进行对象的实例化。
- 延迟加载。
- 序列化和反序列化安全。
- 反射,防止反射攻击。
代码示例
懒汉模式
将类的实例化延后,在使用时进行初始化,但是此模式要注意线程安全的问题。
下面的代码,假如两个线程同时进行初始化的操作,就有可能出现线程安全问题。
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
}
public static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
上述代码进行演进使其线程安全,最简单的方式是在静态方法上加上synchronized,这种方式随便避免了线程安全的问题,但是在线程竞争比较激烈的情况,可能会引起线程堵塞饥饿等问题。
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
}
public synchronized static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
DoubleCheck双重检查
重点
- 将synchronized关键字的位置放到了方法体内部,降低锁的粒度,增加程序的并发性。
- 第一重判断lazyDoubleCheckSingleton是否为null的意义在于,如果已经有线程完成了实例的初始化,那别的线程就没有必要再进行等待或者进入下一步了,直接可以拿到该实例进行返回。
- 加锁,同时刻只能有一个线程进行处理。
- 第二重判断,假如两个线程同时经过了第一重判断,但是在锁住的代码块中,只能有一个线程处理,另一个线程则等待获得monitor锁,直到第一个线程完成对象的初始化并释放锁,第二个线程进入锁的代码块,那第二重判断就起到了作用,此时无需在进行实例化,直接返回对象即可。
- 为什么要加volatile关键字,此处主要是在类的初始化过程中,看似是原子操作,实时并非如此,正常情况下它包含三个步骤:(1)分配内存给这个对象(2)初始化对象(3)设置lazyDoubleCheckSingleton 指向刚分配的内存地址。但是在实例化的过程中,JVM可能进行优化,实例化有可能的执行顺序变成了(1)(3)(2),那这样的话,在第一重的判断中,就会导致有的线程可能正在进行实例的初始化完成了(3),但是还没有完成(2),但是别的线程判断该对象已经分配了内存地址,会跳过if判断,直接返回该没有初始化完成的对象。
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){
}
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class){
if(lazyDoubleCheckSingleton == null){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
//1.分配内存给这个对象
// //3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址
//2.初始化对象
// intra-thread semantics
// ---------------//3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址
}
}
}
return lazyDoubleCheckSingleton;
}
}
静态内部类-基于类初始化的延迟加载解决方案
类初始化的时机
- 类的实例被创建。
- 类中的静态方法被调用。
- 类中的静态成员被赋值。
- 类中的静态成员被使用,并且该成员不是常量成员。
- 该类是一个顶级类,并且类中有嵌套的断言语句。
初始化流程
- 当两个线程同时调用getInstance方法时,InnerClass的成员变量被使用,执行类的初始化。
- 类初始化过程中,JVM会首先加class对象的monitor锁,使得同一时刻只有一个线程能拿到monitor锁。
- 线程在创建StaticInnerClassSingleton实例的过程中,无论是否发生指令重排序,外部的线程是无法感知的,因为他们一直在堵塞等待。
- 当线程实例化对象完成之后,释放锁,其余线程拿到锁,获取成员变量就已经创建完成的了。
public class StaticInnerClassSingleton {
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
private StaticInnerClassSingleton(){
}
}
饿汉式
- 使用static修饰的成员变量,并且在static代码块中完成对象的初始化,当类加载时,会执行static代码块完成对象的初始化。
- 此处可以使用final,也可不使用final,使用final修饰的成员变量必须要在类加载过程中,完成变量的初始化,并且不允许修改。
public class HungrySingleton {
private final static HungrySingleton hungrySingleton;
static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
序列化破坏单例模式原理解析及解决方案
初始版本
以HungrySingleton为例,创建的单例对象,通过输出流将对象写到文件中,再从文件中读出该对象,两个对象不是同一个,也就破坏了单例。
public class HungrySingleton implements Serializable{
private final static HungrySingleton hungrySingleton;
static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
public class Test {
public static void main(String[] args) throws Exception {
HungrySingleton instance = HungrySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
HungrySingleton newInstance = (HungrySingleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
执行结果如下
原理解析
那为什么会出现上述情况呢,通过查看ObjectInputStream的源码,readObject方法调用了readObject0
继续往下看,当是Object类型时,走圈出来的逻辑
readOrdinaryObject方法中,判断desc.isInstantiable(),如果是true,则调用反射生成一个新的对象。
该方法通过注释可以看出来,如果一个class是serializable/externalizable,并且可以通过序列化运行时实例化对象,则返回true,而我们的HungrySingleton实现了serializable,所以执行到此处时返回true,导致反射生成了新的对象。
readOrdinaryObject方法接着往下看,
上述分析之后,我们得知obj不是null,点进去desc.hasReadResolveMethod()方法,通过注释可以看出来,当类是serializable或者是externalizable类型,并且定义了readResolve方法,则返回true。
判断为true之后接着调用invokeReadResolve方法,调用该类的readResolve方法返回对象。
解决方案
通过上述分析,解决方案也基本清晰,那就是在单例的类中添加readResolve方法,该方法返回单例对象即可。
public class HungrySingleton implements Serializable{
private final static HungrySingleton hungrySingleton;
static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
private Object readResolve(){
return hungrySingleton;
}
}
再次执行test方法,两个对象就是同一个对象了。
反射攻击解决方案及原理分析
基于类加载的单例初始化
public class Test {
public static void main(String[] args) throws Exception {
Class objectClass = HungrySingleton.class;
Constructor constructor = objectClass.getDeclaredConstructor();
constructor.setAccessible(true);
HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
HungrySingleton instance = HungrySingleton.getInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
public class HungrySingleton implements Serializable{
private final static HungrySingleton hungrySingleton;
static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
private Object readResolve(){
return hungrySingleton;
}
}
执行main方法结果如下,很明显通过反射破坏了单例。
上述HungrySingleton中的单例是通过类加载的时候进行的初始化,静态内部类实例的初始化也是在类加载的时候进行的,所以静态内部类也有同样的问题,为了防止反射攻击,将两处的代码进行演进,当通过反射再次进行实例的创建时,判断hungrySingleton如果不为空,直接报错或者返回该实例。
public class HungrySingleton implements Serializable{
private final static HungrySingleton hungrySingleton;
static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
if(hungrySingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
private Object readResolve(){
return hungrySingleton;
}
}
public class StaticInnerClassSingleton {
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
private StaticInnerClassSingleton(){
if(InnerClass.staticInnerClassSingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
}
再次执行main方法,结果如下
懒汉式的单例初始化
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
if(lazySingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
public synchronized static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
public class Test {
public static void main(String[] args) throws Exception {
Class objectClass = LazySingleton.class;
Constructor constructor = objectClass.getDeclaredConstructor();
constructor.setAccessible(true);
LazySingleton instance = LazySingleton.getInstance();
LazySingleton newInstance = (LazySingleton) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
执行main方法,相信大家已经能猜到结果了,直接抛出异常,因为我们在构造方法中,禁止了反射的攻击。
如果对上述main方法的顺序进行调整,改变这两行的顺序,LazySingleton instance = LazySingleton.getInstance();LazySingleton newInstance = (LazySingleton) constructor.newInstance();
public class Test {
public static void main(String[] args) throws Exception {
Class objectClass = LazySingleton.class;
Constructor constructor = objectClass.getDeclaredConstructor();
constructor.setAccessible(true);
LazySingleton newInstance = (LazySingleton) constructor.newInstance();
LazySingleton instance = LazySingleton.getInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
那执行结果会是啥样的呢?先进行了反射攻击,此时instance还没有赋值,于是会构造出对象实例,再次执行getInstance()方法,再次构造出实例对象,导致了会产生两个对象,显然也不是我们想要看到的。
总结:上述的懒汉模式无论怎么进行逻辑调整都抵挡不住反射的攻击,要想从根本上解决这个问题可以枚举创建的方式。
枚举单例
序列化破坏枚举单例
public enum EnumInstance {
INSTANCE{
protected void printTest(){
System.out.println("Galen Print Test");
}
};
protected abstract void printTest();
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance(){
return INSTANCE;
}
}
我们首先尝试通过序列化破坏枚举单例
public class Test {
public static void main(String[] args) throws Exception {
EnumInstance instance = EnumInstance.getInstance();
instance.setData(new Object());
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
EnumInstance newInstance = (EnumInstance) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
System.out.println(instance.getData());
System.out.println(newInstance.getData());
System.out.println(instance.getData() == newInstance.getData());
}
}
执行结果如下,仍然是同一个对象,且持有的data属性也是同一个对象。
那这是为什么呢?之前我们分析过ObjectOutputStream.readObject方法,当类型是枚举时,会调用readEnum方法。
在readEnum方法中,获取枚举对象的名字,然后通过Enum.valueOf获取枚举常量,因为枚举的名称都是唯一的,且都对应一个枚举常量,就都是同一个对象实例了。
反射破坏枚举单例
public class Test {
public static void main(String[] args) throws Exception {
EnumInstance instance = EnumInstance.getInstance();
instance.setData(new Object());
Class objectClass = EnumInstance.class;
Constructor constructor = objectClass.getDeclaredConstructor();
constructor.setAccessible(true);
EnumInstance newInstance = (EnumInstance) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
System.out.println(instance.getData());
System.out.println(newInstance.getData());
System.out.println(instance.getData() == newInstance.getData());
}
}
执行结果直接抛出异常,没有获取到无参构造器。
下面分析一下Enum的源码,看一下为什么获取不到无参构造器呢,通过源码可以看出enum只有一个有参构造器。
那我们按enum的有参构造函数的格式,获取enum的有参构造器,再次执行。
public class Test {
public static void main(String[] args) throws Exception {
EnumInstance instance = EnumInstance.getInstance();
instance.setData(new Object());
Class objectClass = EnumInstance.class;
Constructor constructor = objectClass.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
EnumInstance newInstance = (EnumInstance) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
System.out.println(instance.getData());
System.out.println(newInstance.getData());
System.out.println(instance.getData() == newInstance.getData());
}
}
直接抛出异常,无法通过反射创建enum对象。
容器单例
此处容器用的hashMap,在多线程的情况下,可能会存在安全隐患,可以加锁,也可以选择一些线程安全的容器,此处讲述一下容器单例的思想,类似在Spring中的单例bean,就用到了容器单例。
public class ContainerSingleton {
private ContainerSingleton(){
}
private static Map<String,Object> singletonMap = new HashMap<String,Object>();
public static void putInstance(String key,Object instance){
if(StringUtils.isNotBlank(key) && instance != null){
if(!singletonMap.containsKey(key)){
singletonMap.put(key,instance);
}
}
}
public static Object getInstance(String key){
return singletonMap.get(key);
}
}
源码解析
JDK中的Runtime类
getRuntime方法运用了恶汉式单例模式,在类加载时就完成了对象的初始化。
JDK中的Desktop
源码中的context就是一个容器,是很明显的容器单例模式,并且运用了锁保证了操作容器的线程安全。
总结
单例模式是一个高频的面试题,看似是最简单的模式,其实有很多值得去挖掘的点,本文整理的单例模式的类型、优缺点、序列化的破坏、反射攻击、源码中的使用,希望大家能结合实践加深理解~