单例模式
单例模式的定义
在整个程序的生命周期中,有且只有一个这样的实例,并且保证是线程安全的。
单例模式的应用场景
例如Spring
中的ApplicationContext
,还有数据库的连接池都属于单例的模式,他们只会存在一个实例。
单例模式的几种写法
饿汉式单例
我们先来看写法
public class HungrySingleton{
private final HungrySingleton instance = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance{
return instance;
}
}
饿汉式的单例写法,就是在一开始就创建好了对象,并且调用getInstance
时候直接将对象返回,这也是最简单的单例写法。
首先我想说的是,由于在JVM
层面,这个对象就已经创建完成,所以不存在线程安全问题
但是,如果这种单例的写法多了,那么就会存在明明我没有使用这个类却还是被JVM
分配内存的情况,造成不必要的资源浪费。
不过,如果系统中这种单例写法比较少的话,还是挺方便的不是吗。
懒汉式单例
相比饿汉单例
的写法,其实对于内存浪费
这一块问题的优化,由于懒汉遵循,你如果不使用我,那么我就不初始化
的原则,所以对于饿汉单例
的写法要相对好一些。
这里要说明的一点,懒汉式其实也有不下两种的写法,我这里就直接使用静态内部类的方法
来代表懒汉式单例。这样是比较优的一种写法。
public class LazySingleton{
public static LazySingleton getInstance(){
return LazySingletonHolder.LazySingleton;
}
private LazySingleton(){}
private static class LazySingletonHolder{
public static final LazySingleton = new LazySingleton();
}
}
为什么说这种方法会优于其他的懒汉式单例
呢,这个我们从类的加载顺序开始讲解,首先,当LazySingleton
被JVM
装载的时候,第一步会去装载LazySingletonHolder
这个静态的内部类,而由于存在于内部类中的LazySingleton
未被用于逻辑处理,所以暂时
不会被初始化,当外部调用了getInstance
的时候,才会被初始化。这样就避免了内存浪费的问题。
这种形式避免了内存的浪费,又因为内部类一定会在方法调用之前初始化,所以巧妙的避免了线程安全问题。
所以懒汉式的单例模式,我比较推荐这种,当然为了让看客
能够理解,这种的好处,我就简单的写出不使用这种方法会有什么隐患。
那么第一种最简单的写法
public class LazySingleton{
private static lazySingleton instance = null;
private lazySingleton(){}
public static LazySingleton getInstance(){
instance = new LazySingleton();
return instance;
}
}
大家可以很明显看出,这种单例模式虽然符合你需要我的时候,我才初始化
的定义,但是这个方法很明显的线程安全问题。
那么我们为了解决线程安全问题最简单的方法,就是加上synchronized
public class LazySingleton{
private static lazySingleton instance = null;
private lazySingleton(){}
public synchronized static LazySingleton getInstance(){
instance = new LazySingleton();
return instance;
}
}
但是,这线程安全虽然保证了,却有明显的性能问题,这把同步锁
锁在了类
上,那么势必会造成使用的时候造成范围较大的挂起,那么我们可以尝试将synchronized
放到方法里面,使用块来做。
public class LazySingleton{
private static lazySingleton instance = null;
private lazySingleton(){}
public static LazySingleton getInstance(){
if(null==instance){
synchronized(LazySingleton.class){
instance = new LazySingleton();
}
}
return instance;
}
}
这应该来说,已经是非常好的写法了,因为我们保证了他一定是同一个对象
,不过,如果我们通过多线程调试可以发现,如果当两个线程同时准备进入synchronized
块的时候,势必只有一个线程能够拿到对象的锁,另外一个线程被阻塞,直到另一个线程释放锁。当持有锁的线程成功new
出了LazySingleton
对象的时候,释放锁并离开同步块,另外一个在外边等待的线程拿到锁进入同步块的时候,其实又再一次new
了这个对象。
虽然看起来是同一个对象
但是,其实是已经初始化了两次
。所以这里还是存在有瑕疵的地方,那么如何解决呢?
这里有一种叫做双重锁
的写法,即在同步块内再判断一次是否为空,这样就一定保证对象只有一次初始化了。
public class LazySingleton{
private static lazySingleton instance = null;
private lazySingleton(){}
public static LazySingleton getInstance(){
if(null==instance){
synchronized(LazySingleton.class){
if(instance==null){
instance = new LazySingleton();
}
}
}
return instance;
}
}
希望看到这里你会发现,我们在处理线程安全和资源浪费的问题上,其实已经花了很多的精力和时间,那么这也是我推荐使用静态内部类
的原因,让JVM
替我们处理这些繁杂的琐事。
反射破坏与序列化破坏
作为单例模式的的设计者,肯定不希望自己定下的getInstance
规则被破坏。
但是你不能排除,就是有这种想法的人去按照自己的意愿去使用你设计的类。
在这里,所谓的破坏
即是不用你提供的获得单例的方法
而得到你的实例
。
其实在上面几种设计中,反射和序列化都可以轻易破坏你的设计。
反射破坏
public static void main(String[] arg){
try{
Class<?> clazz = LazySinglton.class
Constructor c = clazz.getDeclareConstructor(null);
c.setAccessible(true);
//通过反射初始化
Object obj = c.newInstance();
Object obj1 = LazySinglton.getInstance();
// obj==obj1 false
}catch(Exception e){
// todo
}
}
这个情况下,显然违背了系统中只有一个实例
的定义,所以为了不让使用反射破坏我们的单例,可以在构造方法里写点判断。
public class LazySingleton{
private static lazySingleton instance = null;
private lazySingleton(){
if(instance!=null){
throw new RunTimeException("can not create more instance!")
}
}
public static LazySingleton getInstance(){
instance = new LazySingleton();
return instance;
}
}
序列化破坏
为什么序列化可以破坏我们的已经单例呢,这是因为,序列化是不走构造方法的
,他的本质是对类进行字节码的重组
,直接组装一个类的结构,再被直接加载到JVM
中,所以本质上,他不会按照一个类的正常生命周期去走构造方法,自然我们的刚刚的反射措施也就没了意义。
那么如何预防呢,首先,在ObjectInputStream
中,有一个readObject
方法。也就是从序列化出去的文件中去读取被写在文件中的对象,那么在源码中,默认会调用一个readResolve
方法,这是序列化在嗅探
,如果被序列的类有重写这个方法,那么所返回的Object
就拿这个方法的返回值,并且覆盖之前字节码重组后的对象,虽然本质上还是new了两次
,但是这是JDK
层面的问题,最后会有GC
回收,有点无伤大雅的感觉。
public class LazySingleton{
public static LazySingleton getInstance(){
return LazySingletonHolder.LazySingleton;
}
// 重写方法
public Object readResolve(){
return LazySingletonHolder.LazySingleton;
}
private LazySingleton(){}
private static class LazySingletonHolder{
public static final LazySingleton = new LazySingleton();
}
}
注册式单例
其实上面说了那么多,无非就是想要介绍这个注册式单例
,这种单例的写法,完美的解决了以上我们说的所有问题。没错,他就是这么强大,以至于以后小伙伴可以直接使用这种方法,而无需有后顾之忧。
注册式单例也有两种的写法,一种是容器缓存
,另外一种是枚举登记
。
枚举登记
public enum EnumSingleton {
INSTANCE;
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
这中枚举的单例,当你想要序列化时,在readObject
的底层实现中,判断是如果是枚举的时候,将会调用Enum.valueOf
方法,获得带有class类型和枚举位置的枚举实例,而这个枚举实例早就在static
块中初始化完成了,这里是用了饿汉单例的设计思想,所以序列化后的依旧还是那个static变量
。
而当你想要使用反射的时候,即便是填对了参数,也依旧无法正常实例化,这是因为在Constructor
有对newInstance
的底层方法做了判断,如果是Enum
类型,就会在newInstance
报错,阻止你反射暴力使用。
这也是在《Effective Java》
中最推荐的单例实现方法。
容器缓存
这个大家可以联想一下spring
中的ioc
容器,就是很典型的容器单例。
ThreadLocal 单例
public class ThreadLocalSingletion{
private static final ThreadLocal<ThreadLocalSingletion> tl =
new ThreadLocal<ThreadLocalSingletion>(){
@Override
protected ThreadLocalSingletion initialValue(){
//保证在此工作线程操作的值都是唯一的、
return new ThreadLocalSingletion ();
}
};
private ThreadLocalSingletion (){}//单例的常用做法
public static ThreadLocalSingletion getInstance(){
return tl.get();
}
}
ThreadLocal 之所以可以完成这种在某个线程中是单个线程的情况,来源于ThreadLocal中有一个ThreadMap,这个map以线程作为key,value为对应值,将不同的线程隔离开,保证在线程之间操作不会存在线程安全问题,其实也是容器单例的体现。
一些总结
单例模式的实现,其实很简单,使用也非常简单,我们不必要对这种模式生搬硬套
,而是要找到最适合自己的方式。不然就会有点束缚住了自己。