文章标题
前言
(一): 饿汉式
(二): 懒汉式
(三): 懒汉式 + 同步锁
(四):双重锁检测(Double Check)方式
(五):双重锁检测(Double Check Locking)方式
(六):内部内部类类方式,《Java并发编程实战》中推荐使用这种方式来代替DCL(双重锁检测)的方式。
(七):枚举方式
(八):延伸知识(面试回答,也是一个加分项)
(九):总结
温馨提示: 本文大约5032字,阅读完大概需要2-3分钟,希望您能耐心看完,倘若你对该知识点已经比较熟悉,你可以直接通过目录跳转到你感兴趣的地方,希望阅读本文能够对您有所帮助,如果阅读过程中有什么好的建议、看法,欢迎在文章下方留言或者私信我,如果觉的文章给你带来一点帮助,可以帮忙点一下赞和关注,谢谢!!
前言
单例模式(Singleton Pattern)是 Java 23种设计模式中最简单的设计模式之一,但是也是面试中出现最频繁的设计模式之一,常见实现方法有:“饿汉式”、“懒汉式”,但实际上,它总共有7种写法,要搞懂单例模式,首先要知道它有什么特点,它的特点如下:
一个类只能有一个实例对象
类的构造方法是private修饰
类需要提供获取唯一实例的方法
(一): 饿汉式
特点: 可以理解成已经饿到极致,上来就"吃"(创建),也就是类加载的时候就创建实例。
优点: 简单、线程安全
缺点: 类加载的时候就创建,如果实例没有被使用过,就造成内存浪费
// final修饰,类不能被继承 public final class SingletonTest { // 设置为静态属性,类加载时进行创建对象 private static final SingletonTest INSTANCE = new SingletonTest(); // 构造方法私有 private SingletonTest() { } // 提高方法返回唯一实例对象 public synchronized static SingletonTest hungryTypeGetInstance() { return INSTANCE; } }
(二): 懒汉式
特点: 很懒,只有需要使用的时候才会去创建实例
优点: 只有使用到的时候才会去创建,节省内存
缺点: 多线程环境下会存在多个实例的情况,不能保证对象的唯一性。
// final修饰,类不能被继承 public final class SingletonTest { // 唯一实例对象 private static SingletonTest singleItem; // 构造方法私有 private SingletonTest() { } public static SingletonTest lazyTypeGetInstance() { if(null == singleItem) { singleItem = new SingletonTest(); } return singleItem; } }
(三): 懒汉式 + 同步锁
特点: 很懒,只有需要使用的时候才会去创建实例
优点: 只有使用到的时候才会去创建,节省内存
缺点: 同步锁保证了对象的唯一性,但是效率会下降
// final修饰,类不能被继承 public final class SingletonTest { // 唯一实例对象 private static SingletonTest singleItem; // 构造方法私有 private SingletonTest() { } // 返回对象方法 public static SingletonTest lazyTypeGetInstance() { synchronized (SingletonTest.class) { if(null == singleItem) { singleItem = new SingletonTest(); } } return singleItem; } }
(四):双重锁检测(Double Check)方式
特点: 双重判断,使用时才创建对象
优点: 只有使用到的时候才会去创建,节省内存
缺点: 多线程环境下会存在可能会出现异常,可以使用第五种创建方式解决此问题
疑问一: 为什么要使用两层判断?
疑问二: 为什么说这种方式多线程环境下会存在可能会出现异常?
// final修饰,类不能被继承 public final class SingletonTest { // 唯一实例对象 private static SingletonTest singleItem; // 构造方法私有 private SingletonTest() { } // 返回对象方法 public static SingletonTest doubleCheckTypeGetInstance() { if (null == singleItem) { synchronized (SingletonTest.class) { if(null == singleItem) { singleItem = new SingletonTest(); } } } return singleItem; } }
(五):双重锁检测(Double Check Locking)方式
特点: 双重判断,使用时才创建对象
优点: 只有使用到的时候才会去创建,节省内存,锁的范围缩小,提高效率
疑问一: 为什么单例对象属性使用volatile修饰,它能解决第四种创建方式的问题?
小优化提示(面试回答到这点,绝对的一个加分项): 可以使用局部变量来进行双重锁的优化,由于 volatile变量创建对象时需要禁止指令重排序,这就需要一些额外的操作,可能会影响到一些性能,此处可以参考下spring框架中的双重锁代码,具体如下图: 可以定义一个局部变量来存储创建后的对象,然后再将唯一变量实例指向这个局部变量,这样就可以减少volatile进行指令重排序带来的影响。
// final修饰,类不能被继承 public final class SingletonTest { // 唯一实例对象(使用volatile修饰属性,防止指令重排序) private static volatile SingletonTest singleItem; // 构造方法私有 private SingletonTest() { } // 返回对象方法 public static SingletonTest doubleCheckTypeGetInstance() { if (null == singleItem) { synchronized (SingletonTest.class) { if(null == singleItem) { singleItem = new SingletonTest(); } } } return singleItem; } }
(六):内部内部类类方式,《Java并发编程实战》中推荐使用这种方式来代替DCL(双重锁检测)的方式。
特点: 加载外部类的时候不会进行创建
优点: 第一次调用返回实例对象的时候才会创建节省内存,不需要加锁,提高效率
// final修饰,类不能被继承 public final class SingletonTest { // 构造方法私有 private SingletonTest() { } // 静态内部类 public static class SingletonHolder{ private static final SingletonTest instance = new SingletonTest(); } // 获取单例实例对象 public static SingletonTest innerClassTypeGetInstance() { return SingletonHolder.instance; } }
(七):枚举方式
特点: 默认枚举就是单例的,线程安全
优点: 简单,不会存在反序列的问题
// 获取时直接通过: 枚举类名.属性名称即可如:SingleEnum.SINGLETON public enum SingleEnum{ SINGLETON; }
(八):延伸知识(面试回答,也是一个加分项)
相信大家在看到第七种枚举方式创建的时候,在优点中我有提到不会存在反序列问题,大家可能会存在疑问,下面我们直接通过代码来验证是不是真的存在问题!!
一: 单例模式(此处以“饿汉式“”为例)
// 类实现序列化 public final class SingleSerialized implements Serializable { private static final SingleSerialized instance = new SingleSerialized(); private SingleSerialized() { } // 返回实例对象 public static SingleSerialized getInstance() { return instance; } }
二: 进行序列化前后对象比较
public class ItemTest { public static void main(String[] args) { try { // 获取得到通过"饿汉式"返回的单例对象 SingleSerialized instance = SingleSerialized.getInstance(); System.out.println("反序列化之前单例对象的地址:"+instance); // 通过序列化,反序列,看是否对象还是同一个 FileOutputStream out = new FileOutputStream(new File("demo.db")); ObjectOutputStream outputStream = new ObjectOutputStream(out); InputStream in = new FileInputStream(new File("demo.db")); ObjectInputStream inputStream = new ObjectInputStream(in); outputStream.writeObject(instance); // 反序列的对象 SingleSerialized serialized = (SingleSerialized) inputStream.readObject(); System.out.println("反序列化之后得到单例对象的地址:"+serialized); System.out.println("判断反序列后得到的对象是否跟序列化之前得到的单例对象是否是同一个:"); System.out.println(instance == serialized); } catch (Exception e) { System.out.println(e.getMessage()); } } }
三: 比较结果
四: 疑问解答
通过上面的案例可以知道,如果使用前六种方式实现单例模式,实际上还是不能完全保证单例模式中的:"一个类只能存在一个实例对象"的要求,因为当进行反序列的时候,会新创对象(具体的需要看到ObjectInputStream的readObject源码,此篇幅就不进行具体的深入讲解,如果想了解,后面会具体开一个专门的文字进行讲解),但是,序列化提供了readResolve方法,这个方法可以让开发人员控制对象的反序列化,所以要保证前六种实现单例模式的方法都保障一个类只能存在一个实例的话,就需要在单例类中添加以下方法:
// 返回的值是单例类中的唯一实例 private Object readResolve() throws ObjectStreamException{ return instance; }
五: 添加readResolve方法后的执行结果
(九):总结
码字不易(默默的留下的眼泪,整理到凌晨一点多)、如果你觉得本文对你有一点点帮助,可以点赞和关注!
如果你看完本文觉得有疑问或者本文有错误的地方,欢迎私信或者在下方留言指出。