java设计模式精讲 Debug 方式+内存分析 第8章 单例模式

8-1 单例模式讲解

在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


8-2 单例设计模式-懒汉式及多线程Debug实战

懒汉式单例:

public class LazySingleton {
    /** 懒汉模式的话,开始没有进行初始化 */
    private static LazySingleton lazySingleton = null;
    /** 构造器要进行私有化 */
    private LazySingleton(){

    }
    public static LazySingleton getInstance() {
        /** 这个是有线程安全的问题 */
        if (lazySingleton == null) {
            /**
             * 如果一个线程进来了,但是这个时候,在new实例的时候,阻塞了或者还没有new出实例,
             * 这个时候,另外一个线程判断lazySingleton依然是空的,那么就这时候,也进来了,
             * 那么这个时候,就是有线程安全问题的
             */
            lazySingleton =  new LazySingleton();
        }
        return lazySingleton;
    }
}


我们来测试一下:

public class Test {
    public static void main(String[]args){
        LazySingleton lazySingleton = LazySingleton.getInstance();
        System.out.println(lazySingleton);
    }
}

debug调试:
在这里插入图片描述


我们再来看看多线程的时候,会出现什么问题:

public class T implements Runnable{
    @Override
    public void run() {
        LazySingleton lazySingleton = LazySingleton.getInstance();
        System.out.println(Thread.currentThread().getName()+" "+lazySingleton);
    }
}

测试代码如下:

public class Test {
    public static void main(String[]args){
        Thread t1 = new Thread(new T());
        Thread t2 = new Thread(new T());
        t1.start();
        t2.start();
        System.out.println("program end");
    }
}

program end
Thread-1 com.ldc.design.pattern.creational.singleton.LazySingleton@5102a646
Thread-0 com.ldc.design.pattern.creational.singleton.LazySingleton@5102a646


这个时候,我们就要用到多线程debug来进行调试:
在这里插入图片描述


模拟两个线程,一个线程在进入if之后,还没有new出实例, 这个时候,另外一个线程也进来了,if判断这个时候还没有实例,于是也进入了if里面,这个时候,就new出来两个实例:
在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


然后,我们在切换到thread0,让它赋值上:
在这里插入图片描述


接着我们切换到Thread1:
在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


我们接着向下执行:
Thread0和Thread1都放过:
虽然此时,两个对象还是同一个对象,但是是经过了修改了的:
在这里插入图片描述


我们再debug返回不同的对象:
我们先都两个线程进入if判断,然后一个线程直接执行完成,再另外一个线程执行完成,这个时候,返回的就是两个对象了:
在这里插入图片描述


我们对懒汉式单例模式的线程安全问题有几种解决方案:
方式一:在获取实例的方法上添加synchronized关键字
如果锁加载静态方法上 ,那么就相当于锁是加在这个类的class文件;如果不是静态方法相当于是在堆内存中生成的对象:

public class LazySingleton {
    /** 懒汉模式的话,开始没有进行初始化 */
    private static LazySingleton lazySingleton = null;
    /** 构造器要进行私有化 */
    private LazySingleton(){

    }
    public synchronized static LazySingleton getInstance() {
        if (lazySingleton == null) {
            lazySingleton =  new LazySingleton();
        }
        return lazySingleton;
    }
}

方式二:把获取实例的方法内添加synchronized代码块

public class LazySingleton {
    /** 懒汉模式的话,开始没有进行初始化 */
    private static LazySingleton lazySingleton = null;
    /** 构造器要进行私有化 */
    private LazySingleton(){

    }
    public  static LazySingleton getInstance() {
        synchronized (LazySingleton.class) {
            if (lazySingleton == null) {
                lazySingleton =  new LazySingleton();
            }
        }
        return lazySingleton;
    }
}

在这里插入图片描述


我们再运行Thread1,发现运行不下去了,已经是阻塞的状态了:
在这里插入图片描述


最后拿的就是同一个对象:
在这里插入图片描述
但是,我们知道,加锁和解锁的时候,是会带来额外的开销,对性能会有一定的影响;我们再来进行演进,在性能和安全上进行平衡;


8-3 单例设计模式-DoubleCheck双重检查实战及原理解析

我们 可以这样来写:

public class LazyDoubleCheckSingleton {
    /** 懒汉模式的话,开始没有进行初始化 */
    private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    /** 构造器要进行私有化 */
    private LazyDoubleCheckSingleton(){

    }
    public static LazyDoubleCheckSingleton getInstance() {
        if (lazyDoubleCheckSingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazyDoubleCheckSingleton == null) {
                    lazyDoubleCheckSingleton =  new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

在这里插入图片描述


这个时候,会有一个风险:那就是发生了重排序:

public class LazyDoubleCheckSingleton {
    /** 懒汉模式的话,开始没有进行初始化 */
    private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    /** 构造器要进行私有化 */
    private LazyDoubleCheckSingleton(){

    }
    public static LazyDoubleCheckSingleton getInstance() {
        /** 如果2和3进行重排序,那么这里的判断并不为空,这个时候,实际上对象还没有初始化好,就可以进行这个判断 */
        if (lazyDoubleCheckSingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazyDoubleCheckSingleton == null) {
                    /**
                     * 实际上有三个步骤:
                     * 1. 分配内存给这个对象
                     * 2. 初始化对象
                     * 3.设置lazyDoubleCheckSingleton指向刚分配的内存地址
                     * 2和3的顺序有可能会被颠倒,
                     *
                     * 这个时候,就规定所有的线程在执行java程序的时候,必须要遵守intra-thread semantics这么一个规定
                     * 它保证了重排序不会改变单线程内的程序执行结果
                     */
                    lazyDoubleCheckSingleton =  new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

下图就是表示单线程的情况:规定所有的线程在执行java程序的时候,必须要遵守intra-thread semantics这么一个规定,它保证了重排序不会改变单线程内的程序执行结果。
在这里插入图片描述


现在来看一下多线程的情况:
在这里插入图片描述


我们可以尝试不允许重排序:
我们在初始化的时候,给它加上一个volatile关键字:这个时候,就可以实现线程安全的延迟初始化,这样的话,重排序就是会被禁止,在多线程的时候,CPU也有共享内存,我们加上了这个关键字了之后,所有线程就能看到共享内存的最新状态,保证了内存的可见性,使用volatile的时候,在进行写操作的时候,会多出一些汇编代码,起到两个作用1)将当前处理器缓存好的数据写回到系统内存中,其他内存从共享内存中同步数据,这样的话,就保证了共享内存的可见性,这里就是使用了缓存一致性的协议,当发现缓存内存中的数据无效,会重新从系统内存中把数据读回处理器的内存里;

public class LazyDoubleCheckSingleton {
    /** 懒汉模式的话,开始没有进行初始化 */
    private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    /** 构造器要进行私有化 */
    private LazyDoubleCheckSingleton(){

    }
    public static LazyDoubleCheckSingleton getInstance() {
        /** 如果2和3进行重排序,那么这里的判断并不为空,这个时候,实际上对象还没有初始化好,就可以进行这个判断 */
        if (lazyDoubleCheckSingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazyDoubleCheckSingleton == null) {
                    /**
                     * 实际上有三个步骤:
                     * 1. 分配内存给这个对象
                     * 2. 初始化对象
                     * 3.设置lazyDoubleCheckSingleton指向刚分配的内存地址
                     * 2和3的顺序有可能会被颠倒,
                     *
                     * 这个时候,就规定所有的线程在执行java程序的时候,必须要遵守intra-thread semantics这么一个规定
                     * 它保证了重排序不会改变单线程内的程序执行结果
                     */
                    lazyDoubleCheckSingleton =  new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

通过volatile和doubleCheck的这种方式既兼顾了性能,又兼顾了线程安全的问题;


我们来进行测试一下:
在这里插入图片描述


这个时候,拿到这个就是同一个对象:
在这里插入图片描述

8-4 单例设计模式-静态内部类-基于类初始化的延迟加载解决方案及原理解析


这个就是用静态内部类来实现单例模式:

public class StaticInnerClassSingleton {
    private static class InnerClass {
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.staticInnerClassSingleton;
    }
    private StaticInnerClassSingleton(){

    }
}

在这里插入图片描述


在这里插入图片描述

8-5 单例设计模式-饿汉式

在类加载的时候,就完成了实例化,避免了线程同步的问题,缺点就是在类加载的时候,就完成了初始化,没有延迟加载,这个时候,就是会造成内存的浪费

public class HungrySingleton {
    private final static HungrySingleton hungrySingleton = new HungrySingleton();
    private HungrySingleton() {

    }
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

也可以这样来写:

public class HungrySingleton {
    private final static HungrySingleton hungrySingleton;
    static {
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton() {

    }
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

饿汉式和懒汉式最大的区别就是在有没有延迟加载;


8-6 单例设计模式-序列化破坏单例模式原理解析及解决方案

我们给这类实现一个序列化接口:

public class HungrySingleton implements Serializable {
    private final static HungrySingleton hungrySingleton;
    static {
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton() {

    }
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

我们来测试:

public class Test {
    public static void main(String[]args) throws IOException, ClassNotFoundException {
        HungrySingleton instance = HungrySingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
        oos.writeObject(instance);

        File file = new File("singleton_file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        HungrySingleton newInstance = (HungrySingleton) ois.readObject();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

输出结果:

com.ldc.design.pattern.creational.singleton.HungrySingleton@312b1dae
com.ldc.design.pattern.creational.singleton.HungrySingleton@443b7951
false


这个时候,通过序列化和反序列化拿了不同的对象;而我们希望拿到的是同一的对象,我们可以这样来做:

public class HungrySingleton implements Serializable {
    private final static HungrySingleton hungrySingleton;
    static {
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton() {

    }
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
    /** 我们加上这样的一个方法 */
    private Object readResolve() {
        return hungrySingleton;
    }
}

我们再来进行测试,这个时候,两个对象就是同一个对象:

com.ldc.design.pattern.creational.singleton.HungrySingleton@312b1dae
com.ldc.design.pattern.creational.singleton.HungrySingleton@312b1dae
true


我们通过用debug的方式查看源码,我们可以看出这个方法是通过反射出来的:
在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述
一旦,我们在程序中使用了序列化的时候,一定要考虑序列化对单例破坏;


8-7 单例设计模式-反射攻击解决方案及原理分析


我们用反射来写:

public class Test {
    public static void main(String[]args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class objectClass = HungrySingleton.class;
        Constructor constructor = objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        HungrySingleton instance = HungrySingleton.getInstance();
        HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance==newInstance);
    }
}

测试结果:

com.ldc.design.pattern.creational.singleton.HungrySingleton@12edcd21
com.ldc.design.pattern.creational.singleton.HungrySingleton@34c45dca
false


现在,我们就来写反射防御的代码:
在这里插入图片描述
在用静态内部类来生成的也可以用上这个反射防御的方式:

public class StaticInnerClassSingleton {
    private static class InnerClass {
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.staticInnerClassSingleton;
    }
    private StaticInnerClassSingleton(){
        if (InnerClass.staticInnerClassSingleton != null) {
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }
}

8-8 单例设计模式-Enum枚举单例、原理源码解析以及反编译实战


public enum  EnumInstance {
    INSTANCE;
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
    public static EnumInstance getInstance() {
        return INSTANCE;
    }
}

在这里插入图片描述


我们要调用方法的话,那么我们就是可以这样来写:

public enum  EnumInstance {
    INSTANCE {
        @Override
        protected void printTest() {
            System.out.println("Geely Print Test");
        }
    };
    protected abstract void printTest();

    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
    public static EnumInstance getInstance() {
        return INSTANCE;
    }
}

8-9 单例设计模式-容器单例

我们可以这样来写:

public class ContainerSingleton {
    private static Map<String, Object> singletonMap = new HashMap<>();
    public static void putInstance(String key,Object instance) {
        if (StringUtils.isNotBlank(key) && instance!=null) {
            if (!singletonMap.containsKey(key)) {
                singletonMap.put(key, instance);
            }
        }
    }

    public static Object getInstance(String key) {
        return singletonMap.get(key);
    }

    private ContainerSingleton() {

    }
}

我们来测试一下:

public class Test {
    public static void main(String[]args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Thread t1 = new Thread(new T());
        Thread t2 = new Thread(new T());
        t1.start();
        t2.start();
        System.out.println("program end");
    }
}

如果map使用了HashTable,那么它就是线程安全的,但是这样的话,性能会有降低;


8-10 单例设计模式-ThreadLocal线程单例

在这里插入图片描述


8-11 单例模式源码分析(jdk+spring+mybatis)

在这里插入图片描述


懒汉式:
在这里插入图片描述


在这里插入图片描述


在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_37778801/article/details/84349407
今日推荐