java设计模式之单例模式详解
一、单例模式介绍
1.1 什么是单例模式
因为某些业务或代码逻辑需求,某些特定的类只能对其创建单一实例,用于实现这类需求的方法叫做单例模式。
1.2 为什么要用单例
①对象只可以被实例化一次,意味创建此对象实例次数受到了控制,减少了系统内存消耗和大量创建实例产生的GC的消耗;
②对于某个类或某些通用服务(日志、打印等)需要被频繁创建实例的业务场景下,可以使用单例模式,减少代码耦合,清晰设计思路。
1.3 单例模式的优缺点
优点:
①正常情况下阻止其他对象实例化获得该实例副本,保证所有对象都仅能访问到该类的唯一实例;
②实例化过程由类完全控制,灵活性高。
缺点:
①单例类实例化需要严格规范,容易产生不必要的内存开销或降低系统运行效率;
②除java之外的某些语言可以对实例进行写操作,容易出现悬浮引用情况;
③程序员编码过程中需要了解哪些是单例类,可能会有一点点开发效率上的影响(不会吧不会吧不会还有程序员不知道单例吧)?
二、饿汉、懒汉单例模式
1、饿汉模式
类在被加载时,就创建了该类的实例,代码如下:
public class StarvingSingleton { //静态final修饰,该变量将只会被初始化/赋值一次 private static final StarvingSingleton starvingSingleton = new StarvingSingleton(); private StarvingSingleton(){ } //静态方法,提供获取该类实例的入口 public static StarvingSingleton getInstance(){ return starvingSingleton; } public static void main(String[] args) { //打印查看两次获取实例的结果 System.out.println(StarvingSingleton.getInstance()); System.out.println(StarvingSingleton.getInstance()); } }
饿汉单例模式中,构造方法使用private修饰符修饰,扼杀了使用构造方法创建实例的方式。在类加载的时候,静态变量得到加载,生成该类的实例。
运行main方法之后:

我们可以看到两次getInstance()方法获得的该类的实例是一样的。
2、懒汉单例模式
懒汉单例模式的设计思路,仅仅是在饿汉模式的基础上,将类的实例化过程延迟了,不再是类加载的时候直接实例化该类的对象。下面放代码:
public class LazySingleton { //声明实例变量但不对其进行初始化/实例化 private static LazySingleton instance; //私有构造方法,防止通过构造实例化 private LazySingleton(){} public static LazySingleton getInstance(){ //判断是否已经存在该类实例,不存在则对该类进行实例化,返回已经存在的实例 if (instance == null){ instance = new LazySingleton(); } return instance; } }
上面的代码当中,我们发现在类开头的实例变量并没有被初始化,我们把类的初始化放在类该类的静态方法getInstance()当中去实现。意味着我们需要在使用到这个方法的时候才会获取到该类的实例。又因为在getInstance()方法中增加了实例是否存在的判断,在某种程度上实现了单例。
三、饿汉单例模式可能出现的问题及其解决方案
问题:
1.饿汉单例模式在类加载时就实例化该类的对象,直接产生了内存消耗。(我们的希望更应该是在实例被使用的时候再产生内存消耗?)
2. private修饰符修饰的构造方法可以被反射机制(Class.setAccessible(true)方法设置访问)强行使用,从而达到构造方法实例对象的效果。
3. 通过序列化可以将创建出来的单例对象写入文件中,再通过反序列化创建实例,此时可能出现多个该类的实例。
解决方式:
使用枚举类。枚举类在能够保证线程安全的同时,也可以抵挡反射方式创建类实例,讲实例化的过程放在枚举类类型中,类加载的过程中,枚举类得到加载并且创建出该类的实例。
public class EnumStarvingSingleton { //私有构造 private EnumStarvingSingleton(){} //获取实例的静态方法 public static EnumStarvingSingleton getInstance(){ return ContainerHolder.HOLDER.instance; } //枚举类型,初始化单例类的实例 private enum ContainerHolder{ HOLDER; private EnumStarvingSingleton instance; ContainerHolder(){ instance = new EnumStarvingSingleton(); } } }
此时,我们再使用反射方式并且setAccessible(true)之后,使用构造方法获取该类实例的话,将会得到一个IllegalArgumentException的异常,并提示Cannot reflectively create enum object的信息。
对于序列化的产生的问题,留给大家自己查看。小tip:突破口在ObjectInputStream的readObject中,可以自己查看源码学习。
四、懒汉单例模式可能出现的问题及其解决方案
问题:
众所周知,多线程情况下,代码的执行顺序变得不可知了,在判断实例化对象是否存在的时候,有多个线程卡在这里,假如有两个线程同时进入,则有可能有多个实例产生,破坏单例的生态;
解决方案:
1.使用synchronized锁住该实例方法,但是synchronized属于重量级锁 ,直接对静态方法加锁,将整个类都锁住,大大影响了系统的性能(在此不做讲解)。
2.使用双重检查锁机制,在获取实例方法的实例判断中锁住该类,再做一次实例是否存在的判断,代码如下:
public class LazyDoubleCheckSingleton { private static LazyDoubleCheckSingleton instance; private LazyDoubleCheckSingleton(){} public static LazyDoubleCheckSingleton getInstance(){ //第一次检测 if (instance==null){ //在此处上锁,减少锁的消耗 synchronized (LazyDoubleCheckSingleton.class){ //第二次检测,此时只有可能有一条线程进入,不会出现代码乱序 if (instance == null){ instance = new LazyDoubleCheckSingleton(); } } } return instance; } }
那么,这么做就真的能够解决问题吗?答案是不一定!
我们都知道,对象被创建的过程一般是分配内存空间,初始化对象,然后将内存地址赋值给变量。我们上面的代码中,new实例的过程中假如出现了重排序,还没有初始化对象的时候,先将内存地址赋值给了变量(这种情况是可能存在的),当线程B进入时,发现变量不为null,就会直接返回这个实例,然而此时可能拿到的是还没有初始化完成的对象。
当然,别害怕,这个时候我们不能忘记了曾经学过的一个重要的关键字:voliate。voliate关键词的作用不光是保证线程修改可见,他还有一个关键的作用,就是一定程度上防止重排序的出现。当然,由于voliate也是一个重量级的关键词,在内存消耗上可能就不如预期了。对于这个关键词就不做过多的讲解,感兴趣的可以去查询相关资料。
3. 使用静态内部类,应用静态内部类的特殊之处(外部类被加载的时候,不会产生内部类实例)做单例的实例化。
public class MySingleton { private MySingleton(){} //把实例化的任务交给内部类来做 private static class SingletonHolder{ public final static MySingleton instance = new MySingleton(); } //获取该类实例 public static MySingleton getInstance(){ return SingletonHolder.instance; } }
五、总结
只有最适合自己需要的,才是最好的设计。

浙公网安备 33010602011771号