设计模式-单例模式详解

单例模式
单例模式的定义

在整个程序的生命周期中,有且只有一个这样的实例,并且保证是线程安全的。

单例模式的应用场景

例如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();
    }
}

为什么说这种方法会优于其他的懒汉式单例呢,这个我们从类的加载顺序开始讲解,首先,当LazySingletonJVM装载的时候,第一步会去装载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为对应值,将不同的线程隔离开,保证在线程之间操作不会存在线程安全问题,其实也是容器单例的体现。

一些总结

单例模式的实现,其实很简单,使用也非常简单,我们不必要对这种模式生搬硬套,而是要找到最适合自己的方式。不然就会有点束缚住了自己。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值