前言
这一章的课题看起来就很和蔼可亲了, 比起前面绕的我不要不要的工厂模式, 那感觉真是太好了, 但是正是因为简单, 那么问题就来了, 我怎么才能把这个东西叙述清楚? 怎么样才能老少咸宜呢?
如何能够在把这个东西讲清楚的同时, 引入一些新的东西让这个设计模式能显得不那么普通呢? 我不知道能不能做到, 不过, 吹 x 马上开始
首先, 还是贴一波 HeadFirst 源码地址:
GitHub 地址: https://github.com/bethrobson/Head-First-Design-Patterns
单例入门浅析
HeadFirst 的原文是由一个巧克力锅炉的例子引入了经典的单例模式, 具体例子不赘述, 直接进入经典单例模式的贴代码环节 (注意: 以下所有代码为了方便区分和源代码稍有不同)
经典的单例模式 (线程不安全):
- public class ClassicSingleton {
- private static ClassicSingleton UNIQUE_INSTANCE;
- private ClassicSingleton() {
- }
- public static ClassicSingleton getInstance() {
- if (UNIQUE_INSTANCE == null) {
- UNIQUE_INSTANCE = new ClassicSingleton();
- }
- return UNIQUE_INSTANCE;
- }
- public String getDescription() {
- return "I'm a classic ClassicSingleton!";
- }
- }
首先来说, 为什么它是一个非常适合入门的单例?
因为它确实很简单, 这段代码用一段话来描述就是: 保证需要使用的对象在内存中的唯一性
个人觉得, 这个就是单例的核心思想, 后面各种单例模式都是为了这个操作在做各种各样的努力, 只是实现的优劣之分而已.
再来聊聊为什么不推荐使用?
因为这个写法是一个线程不安全的, 多线程下会有问题. 所以这里用词是不推荐, 万一你的用法就是单线程呢? 这样写也没问题, so, 只要能符合业务, 通俗易懂, 那么它就没有问题. 老生常谈的问题, 没有最好的, 只有最适合的, 简单高效才是硬道理, 个人认为设计模式也只是为了辅助达成这个目标吧
记得我刚实习的时候, 看网上的单例要用 volatile, 要用 synchronized, 看得我那真是一愣一愣的, 当时我 synchronized, 知道是干啥的, 但是用得上, volatile, 还要靠百度才知道是个啥. 本着追求牛 X 技术的心情, 直接就拷了一个个人感觉最牛 X 的 volatile 双重判断的写法上去. 其实当时并不明白其中原理, 只是觉得很牛 X 而已, 幸好没出什么生产环境的 bug, 也是万幸.
不知道原理的代码是很恐怖的, 因为这个东西有些可能是没有通过时间, 业务检验的, 即便是通过了测试, 只要生产环境出问题, 那就是毁灭性的 (别问我怎么知道的). 技术是为了支撑业务, 而不是为了炫技, 写出简单, 易用, 高效的代码才是技术应该做的事情 (当然并非是不鼓励尝试新技术, 只是需要控制在一个可控的范围内)
打个比方, 我写了一个处理权限的功能, 其他人需要接进来的时候, 我告诉他们, 你们要去配一个 xml 文件, properties 文件里面加两个参数, 最后使用的时候, 要调用 xx 方法, 他们第一感觉就是, 你写的这个太难用了. 如果你告诉他们, 把这个包引进去加个注解就可以了, 其他的都不用管呢? 是不是感觉完全不一样?
我擦, 扯远了, 总的来说就是它适合用于学习, 不适合用于商业, 那么有没有适合用于商业的呢? 当然有, 网上文章一大堆
第一种, 简单粗暴的线程安全
- public class ThreadSafeSingleton {
- private static ThreadSafeSingleton UNIQUE_INSTANCE;
- private ThreadSafeSingleton() {
- }
- public static synchronized ThreadSafeSingleton getInstance() {
- if (UNIQUE_INSTANCE == null) {
- UNIQUE_INSTANCE = new ThreadSafeSingleton();
- }
- return UNIQUE_INSTANCE;
- }
- public String getDescription() {
- return "I'm a thread safe singleton!";
- }
- }
效率很低, 但是能用, 依然是不推荐的类型, 有的朋友可能要问了, 那不推荐你写出来干啥?
它还是有优势的, 它理解起来真的很简单, 同时编程也不复杂, 这个不就是我们一直追寻的东西吗? 如果一个问题没有更好的解决方案, 那么理解简单, 编程简单的方案也不失为一个方案吧? 至少能看懂啊
当然单例模式这里确实是不推荐的, 因为我知道的还有至少 3 种比它好, 所以不推荐
第二种, 使用静态初始化变量
- public class StaticallyInitializedSingleton {
- private static StaticallyInitializedSingleton UNIQUE_INSTANCE = new StaticallyInitializedSingleton();
- private StaticallyInitializedSingleton() {}
- public static StaticallyInitializedSingleton getInstance() {
- return UNIQUE_INSTANCE;
- }
- public String getDescription() {
- return "I'm a statically initialized ClassicSingleton!";
- }
- }
这种算是最推荐的写法了, 首先它写法简单, 其次线程安全问题可以通过 jvm 去保证 (每个类的静态变量在 jvm 中只会被初始化一次), 最后, 获取单例类的操作没有加锁处理, 性能很高
但是, 它也不是没有问题, 一般来说, 需要单例的类都比较耗性能, 创建了不用还是比较伤的 (当然, 有的时候, 有钱能解决很多这样的问题), 还有一种可能是如果在初始化类的时候, 创建单例类失败了, 那这个类里面所有的方法都没法用了, 如果在 spring 的环境下, 再有一个 @Component 之类的注解, 或者能够被 spring 扫描到的其他操作那就更好玩了, 可能项目都启动不起来.
这个东西就要看这个单例对于项目是不是强依赖了, 仁者见仁智者见智了, 此处就不赘述, 不然又要跑偏了
第三种, 双重加锁检查
- public class DoubleCheckSingleton {
- private volatile static DoubleCheckSingleton UNIQUE_INSTANCE;
- private DoubleCheckSingleton() {}
- public static DoubleCheckSingleton getInstance() {
- if (UNIQUE_INSTANCE == null) {
- synchronized (DoubleCheckSingleton.class) {
- if (UNIQUE_INSTANCE == null) {
- UNIQUE_INSTANCE = new DoubleCheckSingleton();
- }
- }
- }
- return UNIQUE_INSTANCE;
- }
- public String getDescription() {
- return "I'm a double check singleton!";
- }
- }
这个就是个人觉得一看就是很牛 X 的那种写法, 当然, 它的各方面也都是相当优秀的, 线程安全, 容错, 性能都不错
但是, 如果对它的理解比较模糊的话, 那么写的时候是很容易写掉一些重点的东西的, 举两个点:
1, 静态变量里面的 volatile 容易写掉吧?
2,synchronized 里面那个判空容易写掉吧?
对于 2 这点, 我是深有体会的, 如果 A,B 线程同时调用 getInstance() 方法, 假设 UNIQUE_INSTANCE 还没有初始化, 同时 A 线程先进入 synchronized 块, 没有 if null 判断, 那么它就 new 了一个对象出来吧, 当 A 执行完了以后释放了锁, 这个时候 B 就会进入, 没有 if null 判断, B 也 new 一个对象出来, 这就有问题了啊.
对于 1 这点, 如果不理解 volatile, 是很容易写掉的, 毕竟, 如果能很好的理解第 2 点的话, 就会感觉, 不加这啥 volatile 感觉也没啥问题啊, 双重锁稳的不行啊. 但是不加还真有可能有问题
先说说 volatile 一般的作用: 禁止指令重排序, 内存可见性
这里的作用是禁止指令重排序, 在创建对象并访问的过程中, 可以分为 4 个步骤:
- memory = allocate(); //1: 分配对象的内存空间
- ctorInstance(memory); //2: 初始化对象
- instance = memory; //3: 设置 instance 指向刚分配的内存地址
- instance.invoke() //4: 初次访问对象
只要保证在访问对象之前完成 1,2,3, 对于 Java 语言规范来说都是允许的, 所以这里 2 和 3 是可以重排序的, 但是多线程的情况下, 假如 A 线程正在进行对象初始化, B 线程可能会在第一个 if null 判断的时候拿到一个不为空, 但是还没有初始化完成的对象 (2,3 被重排序), 然后就会出现一些未知的错误
如果使用 volatile 的话, 那么 2,3 就不会重排序, 即使有其他线程拿到对象, 也就说明, 肯定是已经执行了 2,3 两步, 不然的话 if null 判断肯定是空
这里是参考了一位大佬的文章:
https://www.infoq.cn/article/double-checked-locking-with-delay-initialization
单例模式的破解与防御
前面大量的篇幅用来说明了怎么在多线程的情况下保证单例模式, 这里讲讲怎么从语法层面上来保证.
首先, 语法层面上保证单例的一般操作是这样的:
- private StaticallyInitializedSingleton() {
- }
相当于告诉所有人, 这个类只能我自己初始化, 你们别搞事情哈, 但是它并不是牢不可破的
第一种, 使用反射机制
- public class SingletonClient {
- public static void main(String[] args)
- throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, InterruptedException {
- Constructor cons = StaticallyInitializedSingleton.class.getDeclaredConstructor(null);
- cons.setAccessible(true);
- StaticallyInitializedSingleton singleton = (StaticallyInitializedSingleton)cons.newInstance(null);
- System.out.println(singleton.getDescription());
- }
- }
这种操作怎么防御呢? 可以去控制它的类只能初始化一次, 具体的操作可以这样:
- public class StaticallyInitializedSingleton implements Serializable{
- private static StaticallyInitializedSingleton UNIQUE_INSTANCE = new StaticallyInitializedSingleton();
- private static boolean INITIALIZED;
- private StaticallyInitializedSingleton() {
- if(INITIALIZED){
- throw new RuntimeException();
- }
- INITIALIZED = true;
- }
- public static StaticallyInitializedSingleton getInstance() {
- return UNIQUE_INSTANCE;
- }
- public String getDescription() {
- return "I'm a statically initialized ClassicSingleton!";
- }
- }
在类构造函数中, 添加标记 INITIALIZED, 标明是否已经初始化如果已经初始化, 那么就抛出异常. 这里因为反射调用的时候, 也会先去初始化类, 初始化类的时候, 就会在静态变量赋值的时候触发创建一次对象, 等到反射调用 newInstance 的时候, 就会报错
第二种, 使用 Java 的序列化与反序列化
当然, 首先要实现 Serializable 接口, 不然也没有这个问题
- public class SerializeTest {
- public static void main(String [] args) throws IOException, ClassNotFoundException {
- StaticallyInitializedSingleton s = StaticallyInitializedSingleton.getInstance();
- ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(new File("E:/test.txt")));
- objectOutputStream.writeObject(s);
- ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:/test.txt"));
- StaticallyInitializedSingleton s1 = (StaticallyInitializedSingleton)ois.readObject();
- System.out.println(s);
- System.out.println(s1);
- }
- }
执行结果就会发现 s,s1 不是一致的, 解决这种情况, 需要在单例类中加入 readResolve() 方法来控制, JVM 在反序列化的时候, 使用我们自定义的类作为结果
- public class StaticallyInitializedSingleton implements Serializable {
- private static StaticallyInitializedSingleton UNIQUE_INSTANCE = new StaticallyInitializedSingleton();
- private static boolean INITIALIZED;
- private StaticallyInitializedSingleton() {
- if(INITIALIZED){
- throw new RuntimeException();
- }
- INITIALIZED = true;
- }
- public static StaticallyInitializedSingleton getInstance() {
- return UNIQUE_INSTANCE;
- }
- public String getDescription() {
- return "I'm a statically initialized ClassicSingleton!";
- }
- // 解决序列化与反序列化问题
- private Object readResolve(){
- return uniqueInstance;
- }
- }
花了这么多功夫, 终于解决了, 那么实际开发中, 有没有必要这样去处理这些问题呢?
这个我给不出答案, 可以给出的参考有两个: 1, 编程成本; 2, 程序边界
通俗点讲就是: 1, 改这个要花多久时间 (编写, 测试, 上线);2, 不按规范来调用, 是不是程序需要关注地方
当然, 有没有一劳永逸的方法来解决各种各样问题, 并且编写简单, 容易理解呢?
请看:
- public enum Singleton {
- INSTANCE;
- public String getDescription() {
- return "枚举单例, 就是这么简单";
- }
- }
没错, 枚举类, 是不是感觉比上面所有都简单;)
来源: https://www.cnblogs.com/skyseavae/p/9870867.html