diff --git "a/docs/\345\274\200\345\247\213\351\230\205\350\257\273/\350\256\276\350\256\241\346\250\241\345\274\217/\345\215\225\344\276\213\346\250\241\345\274\217.md" "b/docs/\345\274\200\345\247\213\351\230\205\350\257\273/\350\256\276\350\256\241\346\250\241\345\274\217/\345\215\225\344\276\213\346\250\241\345\274\217.md" index 900052f6..ddb98257 100644 --- "a/docs/\345\274\200\345\247\213\351\230\205\350\257\273/\350\256\276\350\256\241\346\250\241\345\274\217/\345\215\225\344\276\213\346\250\241\345\274\217.md" +++ "b/docs/\345\274\200\345\247\213\351\230\205\350\257\273/\350\256\276\350\256\241\346\250\241\345\274\217/\345\215\225\344\276\213\346\250\241\345\274\217.md" @@ -37,6 +37,9 @@ public class SingletonOfLazy { private static SingletonOfLazy INSTANCE = null; private SingletonOfLazy() { + if (INSTANCE != null) { + throw new RuntimeException("单例对象无法重复创建"); + } } public static SingletonOfLazy getInstance() { @@ -48,8 +51,121 @@ public class SingletonOfLazy { } ``` -# ➋ 饿汉式 +## ➋ 饿汉式 + +
+饿汉式就是无论你用不用的到,我都先创建对象,你要用就直接用,比喻使用者饿了,不用等待创建过程直接“吃”。 +
-饿汉式就是无论你用不用的到,我都先创建对象,你要用就直接用,比喻使用者饿了,不要等待创建过程直接“吃”。
+首先它在类加载时就创建实例,同时Java的类加载机制确保了类是线程安全的;
+然后私有构造函数,防止外部实例化;
+最后提供全局访问点以获取对象。
+前面两种,一种懒汉式,用到才创建,避免了对象创建而长时间不使用的问题,但是它有线程安全问题; +一种饿汉式,直接创建对象,将创建对象的资源消耗放到类加载时,也避免了线程安全问题,但是对象一直在内存里而不使用, +会有浪费。两者各有优劣。 +
+ ++双检锁懒汉式是正对懒汉式线程安全问题的一种改良,很好的体现了对于锁🔒的理解。使用起来很简单,理解里面的意思就难了, +比如两次判断的含义,也算是面试会考的题目了。 +
+ +```java +public class SingletonOfDCLLazy { + /** + * 用volatile主要是为了保证变量的有序性,因为CPU会自己优化代码执行顺序, + * 单线程下无所谓,多线程下会有问题,而volatile可以防止指令重排,它会在赋值操作后加上一个内存屏障, + * 这样就可以让之前的操作不允许越过这个赋值操作来执行,而防止CPU的自动优化. + */ + private static volatile SingletonOfDCLLazy INSTANCE = null; + + private SingletonOfDCLLazy() { + //构造器必须私有 不然直接new就可以创建 + } + + public static SingletonOfDCLLazy getInstance() { + // 1️⃣第一次判断,假设会有好多线程,如果doubleLock没有被实例化,那么就会到下一步获取锁,只有一个能获取到, + // 如果已经实例化,那么直接返回了,减少除了初始化时之外的所有锁获取等待过程 + if (INSTANCE == null) { + synchronized (SingletonOfDCLLazy.class) { + // 2️⃣第二次判断是因为假设有两个线程A、B,两个同时通过了第一个if,然后A获取了锁,进入,然后判断doubleLock是null, + // 他就实例化了doubleLock,然后他出了锁, + // 这时候线程B经过等待A释放的锁,B获取锁了,如果没有第二个判断,那么他还是会去new DoubleLock(), + // 再创建一个实例,所以为了防止这种情况,需要第二次判断 + if (INSTANCE == null) { + // 下面这句代码其实分为三步: + // 1.开辟内存分配给这个对象 + // 2.初始化对象 + // 3.将内存地址赋给虚拟机栈内存中的doubleLock变量 + // 注意上面这三步,第2步和第3步的顺序是随机的,这是计算机指令重排序的问题 + // 假设有两个线程,其中一个线程执行下面这行代码,如果第三步先执行了,就会把没有初始化的内存赋值给doubleLock + // 然后恰好这时候有另一个线程执行了第一个判断if(doubleLock == null),然后就会发现doubleLock指向了一个内存地址 + // 这另一个线程就直接返回了这个没有初始化的内存,所以要防止第2步和第3步重排序 + INSTANCE = new SingletonOfDCLLazy(); + } + } + } + return INSTANCE; + } +} +``` +## ➍ 内部类单例 + ++这个模式使自己比较喜欢的,没有那么多弯弯绕,利用Java的静态内部类机制来实现懒加载和线程安全,用起来也方便。 +
+ +```java +public class SingletonOfInnerClass { + private SingletonOfInnerClass() { + } + + private static class Holder { + static SingletonOfInnerClass INSTANCE = new SingletonOfInnerClass(); + } + + public static SingletonOfInnerClass getInstance() { + return Holder.INSTANCE; + } +} +``` + +## ➎ 枚举单例 + ++Java的枚举类型天然支持单例模式,并且是线程安全的,就是使用范围小,只能说算是,但是你要写一个单例, +枚举肯定不在考虑范围,枚举的设计就不是为单例而生,只是“凑巧”它天然是单例。 +
+ +```java +public enum SingletonOfHungryEnum { + INSTANCE; + + // 其他方法 + public void someMethod() { + // 方法实现 + } +} +``` \ No newline at end of file