【笔记】单例
特点
- 类自己创建单例实例,并且这个类只有一个实例。
 - 单例对象都是静态的,保证整个内存中只有一份,调用时指向同一个对象。(静态存储区引用指向堆中的对象)。
 - 一般都会私有构造函数,避免其他类通过new来创建实例。
 
分类
分类讲解时,默认语言为Java,代码示例也都是Java编写。
饿汉式
类加载时就创建单例对象。
class SingletonHungry{  
	private SingletonHungry(){}  
	private static SingletonHungry instance = new SingletonHungry();  
	public static SingletonHungry getInstance(){  
		return instance;  
	}  
}
声明静态变量,并且直接创建实例赋值。而类加载的方式是按需加载,并且只加载一次。所以在类被加载时,静态变量仅在此时被初始化。所以这种单例对象在线程访问前就已经创建好了,线程每次只能也必定只可以拿到这个唯一的对象,所以饿汉式单例是天生线程安全的。
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。
优缺点&选型
优点:线程安全。
缺点:类加载,没有用到实例时就创建了,会导致内存被浪费。
适合单例内存占用小,在初始化时就会被用到的场景。
懒汉式
class SingletonLazy{  
	private SingletonLazy(){}  
	private static SingletonLazy instance = null;  
	public static SingletonLazy getInstance(){  
		if (instance == null){  
			instance = new SingletonLazy();  
		}  
		return instance;  
	}  
}
当使用时,才生成实例。
因为使用时才创建实例,所以是线程不安全的。
优缺点&选型
和饿汉正好相反,优点是使用时才创建,会节省不必要时的资源。
但是在多个线程中可能会并发创建多个实例。为了解决这个问题,获取实例的方法一般都使用加锁的方式解决线程同步问题。
懒汉式适用于单例对象使用次数比较少,单个对象资源消耗比较多的情况。
为什么线程不安全
举例来说:
- A线程调用实例方法,判断是null时进入创建实例代码。
 - 此时另一个线程B抢到了CPU资源,切换到B。A中已经进入的创建实例代码被暂时停止,所以实际上实例仍然为null,导致在线程B中同样判断进入创建实例。
 - 到CPU切换回A线程执行,依旧会执行原先的创建实例的代码,导致单例类创建了两个实例。
 
双重校验锁
双重校验锁(DCL)是加锁的懒汉式的一种优化方案。
class SingletonDCL{  
	private SingletonDCL(int params){}  
	private static volatile SingletonDCL instance = null;  
	public static SingletonDCL getInstance(int params){  
		if (instance == null){  
			synchronized (SingletonDCL.class){  
				if (instance == null){  
					instance = new SingletonDCL(params);  
				}  
			}  
		}  
		return instance;  
	}  
}
首先普通的加锁的懒汉式,有一个问题,就是通过synchronized关键字锁定getInstance方法会导致整个方法的效率降低。而这样设计的好处仅是确保实例为null时,第一次创建的线程同步。
双重校验锁就是为了解决这个问题,达到既要保证实例的创建是线程同步的,也要保证之后的调用不会因为加锁导致代码执行效率的降低。
为了保证代码的运行速度,我们只针对第一次判空成功后做加锁操作,避免每次调用都需要加锁。同时,避免两个线程执行了判空流程后,都去执行创建代码。在同步代码块中,进行第二次判空,保证第一个线程创建实例完成后,第二个线程在执行同步代码时,检测到已经创建了实例。这就是双重校验锁得名的地方。
隐患-指令重排
通过这种双重校验,我们实现了上面的需求,但是这样是否就万无一失了呢?
Java的代码会被JVM编译为字节码,然后成为具体平台的机器指令。
为了获取更好的性能JVM可能会对指令进行重排序,通过调整指令的执行顺序让程序运行的更快。
比如
a = b + c;
d = e - f ;
执行第一行代码的时候,先加载了b和c。
按顺序应该执行add(b,c),但是因为在加载b和c,可能会有一定的停顿。
指令重排的作用就在此时发挥的作用。
它会在将加载e和f的顺序提前到add(b,c)之前。这样原先一定要加载的e和f也已经加载好了,节省了add(b,c)等待b,c加载好的停顿。
指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。所以在多线程下,指令重排序可能会导致一些问题。
在双重校验锁中就是,虽然我们通过加锁的方式保证对代码的执行是原子性的。
但是,指令重排可能会导致我们instance的赋值和初始化函数调用被重排了。
instance = Singleton();
理论上说,应该是初始化函数先行调用,然后再执行对instance的赋值。
但是重排后,可能会发生,线程A通过调用getInstance进入创建实例代码,并且完成了instance赋值的指令。
然后线程B抢到CPU资源同样调用getInstance,此时进行判空时,因为instance已经被赋值,所以会直接返回instance。然而此时的instance空有引用地址,对应的实例还未创建,导致程序出错。
Happens-Before
在JDK1.5中,引入了volatile关键字,它有好几种语义,既是禁用指令重排,又是happens-before。
在上面的例子中,那就是创建实例先于instance赋值。并且由于写操作happens-before于任意后续对这个volatile域的读。所以线程B无法先行getInstance,只能等待线程A完成赋值后调用。(这段我估计的,不能保证正确)
总结
所以完整的双重校验锁模式是JDK1.5后才能实现。要点如下:
- 变量声明用
volatile修饰。 - 在判空后加锁,锁住的代码中再次判空。
 
静态内部类
class SingletonInnerStatic{  
	private SingletonInnerStatic(){}  
  
	private static class SingletonInnerClass{  
		private static SingletonInnerStatic instance = new SingletonInnerStatic();  
	}  
  
	public static SingletonInnerStatic getInstance(){  
		return SingletonInnerClass.instance;  
	}  
}
做法:在需要提供单例的类中声明一个静态内部类,然后在该内部类中创建一个外部类型的静态属性。外部类再提供一个getInstance返回静态内部类的这个属性。
优缺点&选型
优点:同样利用类加载机制保证只创建一个实例,所以是线程安全的。并且只要不调用内部类,就不会创建这个实例,同时实现延迟加载。某种程度上属于饿汉式的优化版本。
缺点:无法传参,这应该是通过类加载机制创建实例的通用问题,也就是说饿汉式也有这个缺点。
选型:饿汉的上位替代,和DCL间,应该是根据是否需要传参数去决定吧。
枚举
class SingletonEnum{  
	private SingletonEnum(){  
		System.out.println("SingletonEnum初始化");  
	}  
	private enum InnerEnum{  
		INSTANCE;  
		private final SingletonEnum instance; 
		 
		InnerEnum(){  
			instance = new SingletonEnum();  
		}  
  
		private SingletonEnum getInstance(){  
			return instance;  
		}  
	}  
  
	public static SingletonEnum getInstance(){  
		//InnerEnum.INSTANCE.instance也一样?应该是单例的命名规则  
		//统一提供单例获取方法getInstance  
		return InnerEnum.INSTANCE.getInstance();  
	}  
}
枚举模式最安全,反射和序列化都是单例。
《Effective Java》作者也是强烈推荐枚举方式实现单例。
利用枚举的几个特性实现单例:
- 构造器私有
 - 默认添加final和static修饰符,只会实例化一次
 - Enum类实现Serializable,所以是支持序列化的
 
优缺点&选型
通过枚举构建单例,可以避免上面几种单例共同的问题:
- 需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。
 - 可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)
 
缺点的话,感觉和饿汉式也是类似的,不能通过外部传入的参数进行构建。
总结
单例大体分为5种
- 饿汉式
 - 懒汉式
 - 双重校验锁
 - 静态内部类
 - 枚举
其中,双重校验锁上位替代懒汉式,静态内部类上位替代饿汉式。如果不考虑简洁代码的话,一般都选择上位替代。
如果有序列化需求,那优先枚举实现的单例。 
Kotlin写法
饿汉式
object A{}
懒汉式
class SingletonLazy private constructor(){  
  
	companion object{  
		//为了提供统一的getInstance方法,特意改instance为mInstance  
		private var mInstance : SingletonLazy? = null  
			get() {  
				if (field == null){  
					field = SingletonLazy()  
				}  
				return field  
			}  
		@Synchronized  
		fun getInstance():SingletonLazy{  
			return mInstance!!  
		}  
	}  
}
DCL
class SingletonDCL private constructor(){  
	companion object{  
		//默认mode = LazyThreadSafetyMode.SYNCHRONIZED
		val instance : SingletonDCL by lazy{  
			SingletonDCL()  
		}
	}
}
带参数的例子,和Java就很相似了:
class SingletonDCLWithParam private constructor(param:Int){  
	companion object{  
		@Volatile  
		private var instance : SingletonDCLWithParam? = null  
		fun getInstance(param:Int):SingletonDCLWithParam{  
			return instance?: synchronized(this){
				instance?:SingletonDCLWithParam(param).also { instance = it }  
			}  
		}  
	}  
}
静态内部类
class SingletonInnerClass{  
	companion object{  
		fun getInstance() = SingletonInnerHolder.instance  
	}  
  
	private object SingletonInnerHolder{  
		val instance = SingletonInnerClass()  
	}  
}
枚举
class SingletonEnum{  
	companion object{  
		fun getInstance() = SingletonEnumHolder.INSTANCE.instance  
	}  
	private enum class SingletonEnumHolder(val instance:SingletonEnum){  
		INSTANCE(SingletonEnum())  
	}  
}
Thanks:
主要来源:Java单例模式的不同写法(懒汉式、饿汉式、双检锁、静态内部类、枚举)
Kotlin下的5种单例模式
Java多线程总结
7 重排序与happens-before
深入理解单例模式:静态内部类单例原理
“双重校验锁不可靠”声明
                    
                
                
            
        
浙公网安备 33010602011771号