详解五种单例模式

详解五种单例模式

一、什么是单例模式

单例模式,属于创建类型的一种常用的软件设计模式。定义是:一个类有且只有唯一一个实例,提供系统使用。

二、单例模式应用的场景

  • Web应用配置文件的读取,由于配置文件是共享资源,一般采用单例模式
  • 网站的计数器,一般也是采用单例模式实现,否则难以同步。
  • 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。

三、单例模式的优缺点

  • 优点:节约资源以及提高资源的利用率,如果一个应用总是产生相同的实例,实例一多,就会导致系统内存不足,运行响应缓慢,甚至宕机
  • 缺点: 如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失

四、单例模式四种模式

单例模式三个主要特点:1、构造方法私有化;2、实例化的变量引用私有化;3、获取实例的方法共有。

0、idea 多线程debug调试

在接下来讲解中,使用多线程测试,因此如何使用idea 多线程debug是非常重要的,以懒汉模式为例

  • 打下断点,右键改为Thread,点击Done
  • 点击运行debug按钮

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1、懒汉模式

类初始化时不会立刻创建实例,而是当调用方法时创建

缺点:线程不安全,延迟初始化,严格意义上不是不是单例模式 ,因为可能创建多个实例

class LazySingleton {

    private static LazySingleton singleton = null;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        // 判断实例是否已创建
        if (null == singleton) {
            System.out.println("Thread of creating instance: " + Thread.currentThread().getName());
            singleton = new LazySingleton();
            // 创建实例
            System.out.println("Thread of instance: " + singleton);
        }
        return singleton;
    }
}
==============================测试======================================
public class SingleTest {

    public static void main(String[] args) {
        for (int i = 1; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep((long) (Math.random() * 10));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + LazySingleton.getInstance());
                }
            }, "thread " + i).start();
        }

    }
}

测试结果
在这里插入图片描述

2、饿汉模式

类初始化时立刻创建实例,线程安全

缺点: 资源利用率不高,可能永远不被调用

class HungrySinglenton {

    // 类初始化时就已经创建
    private static HungrySinglenton singlenton = new HungrySinglenton();

    private HungrySinglenton(){
        System.out.println("类初始化就创建");
    }

    public static HungrySinglenton getInstance() {
        return singlenton;
    }

}

==========================测试===============================================
public class SingleTest {

    public static void main(String[] args) {
        for (int i = 1; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep((long) (Math.random() * 10));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + HungrySinglenton.getInstance());
                }
            }, "thread " + i).start();
        }

    }
}

测试结果
在这里插入图片描述

3、双重检测锁

进行了两次的判断,第一次是为了避免不要的实例,第二次是为了进行同步,避免多线程问题。由于singleton对象的创建在JVM中可能会进行重排序,在多线程访问下存在风险,使用volatile修饰signleton实例变量有效,解决该问题。

class DoubleCheckLockSingleton {

    /*使用volatile 防止 类初始化重排*/
    private volatile static DoubleCheckLockSingleton singleton;

    private DoubleCheckLockSingleton() {}

    public static DoubleCheckLockSingleton getInstance() {
        // 判断是否存在,避免不要的实例,不过多线程可以进入
        if (null == singleton) {
            // 使用synchronized,单线程通行,防止多线程进入
            synchronized (DoubleCheckLockSingleton.class) {
                System.out.println("thread of passing the first lock:" + Thread.currentThread().getName());
                // 再次判断是否存在,防止多线程创建,达到双重检测
                if (null == singleton) {
                    System.out.println("thread of creating instance: " + Thread.currentThread().getName());
                    singleton = new DoubleCheckLockSingleton();
                }
            }
        }
        return singleton;
    }
}
==============================测试===========================================
// 使用多线程测试
public class SingleTest {

    public static void main(String[] args) {
        for (int i = 1; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep((long) (Math.random() * 10));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + DoubleCheckLockSingleton.getInstance());
                }
            }, "thread " + i).start();
        }

    }
}	

测试结果
测试结果[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0e3T32EH-1579094660038)(C:\Users\20423\AppData\Roaming\Typora\typora-user-images\1579064576184.png)]

4、静态内部类

只有第一次调用方法时,虚拟机才加载 Inner 并初始化instance ,只有一个线程可以获得对象的初始化锁,其他线程无法进行初始化,保证对象的唯一性。目前此方式是所有单例模式中最推荐的模式,但具体还是根据项目选择。

缺点:无法传参,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去

class StaticInnerSingleton {

    private static StaticInnerSingleton singleton = null;

    private StaticInnerSingleton(){
        System.out.println("类被调用创建实例");
    }

    public static StaticInnerSingleton getInstance() {
        return StaticInner.singleton;
    }

    private static class StaticInner {
        private static StaticInnerSingleton singleton = new StaticInnerSingleton();
    }
}

==============================测试===================================
    public class SingleTest {

    public static void main(String[] args) {
        for (int i = 1; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep((long) (Math.random() * 10));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName());
                    System.out.println(StaticInnerSingleton.getInstance());
                }
            }, "thread " + i).start();
        }

    }
}

测试结果
在这里插入图片描述

5、枚举单例

在介绍枚举单例前说下,上面四种单例模式有一个缺点:能通过反射机制调用私有的构造器,创建新的实例,破坏单例模式;而枚举单例就是能解决这一问题

5.1 反射攻击双重检查锁单例模式

// 使用多线程测试
public class SingleTest {

    public static void main(String[] args) {
        
        DoubleCheckLockSingleton singleton1 = DoubleCheckLockSingleton.getInstance();
        Constructor<DoubleCheckLockSingleton> constructor =
                            DoubleCheckLockSingleton.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);  // 设置访问权限
        DoubleCheckLockSingleton singleton3 = constructor.newInstance();
        System.out.println(singleton3 == singleton1);
               
    }
}	

测试结果
在这里插入图片描述

5.2 如何防止反射破坏

在第四节第3段的DoubleCheckLockSingleton类中的私有构造方法添加判断

private DoubleCheckLockSingleton() {
        // 防止多线程进入,会发布又没用同步代码库快的测试结果
        synchronized(DoubleCheckLockSingleton.class) {
            if(flag == false) {
                flag = !flag;
                System.out.println(Thread.currentThread().getName() + " come in");
            } else {
                throw new RuntimeException("单例模式被侵犯!");
            }
        }
    }
    
=============================测试================================================
public class SingleTest {

    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                // 非反射案例
                DoubleCheckLockSingleton singleton1 = DoubleCheckLockSingleton.getInstance();
                System.out.println(singleton1);
            }
        }, "thread-1 ").start();

      
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 反射
                try {
                    Constructor<DoubleCheckLockSingleton> constructor =
                            DoubleCheckLockSingleton.class.getDeclaredConstructor(null);
                    constructor.setAccessible(true);  // 设置访问权限
                    DoubleCheckLockSingleton singleton3 = constructor.newInstance();
                    System.out.println("反射创建实例是否为同一个:" + (singleton3 == singleton1));
                } catch (InstantiationException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }, "thread-3").start();

    }
}

测试结果
没有使用synchronized的结果
在这里插入图片描述
使用synchronized的结果
在这里插入图片描述

5.3 枚举单例的出现

在jdk1.5后出现枚举实现单例模式

enum EnumSingleton {

    ENUM_SINGLETON,
    ;
    public EnumSingleton getInstance(){
        return ENUM_SINGLETON;
    }
}

=================================测试============================================
public class SingleTest {

    public static void main(String[] args) {
         //四种单例多线程检测
//        multiThreadSingle();

        // 反射检测
//        invokeSingleton();

        // 枚举检测
        EnumSingleton singleton1=EnumSingleton.ENUM_SINGLETON;
        EnumSingleton singleton2=EnumSingleton.ENUM_SINGLETON;
        System.out.println("非反射情况下是否为同一个实例:"+(singleton1==singleton2));
        Constructor<EnumSingleton> constructor= null;
        try {
            constructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);//其父类的构造器
            constructor.setAccessible(true);
            EnumSingleton singleton3 = constructor.newInstance("ENUM_SINGLETON", 666);
            System.out.println("反射情况下是否为单例:" + (singleton1==singleton3));
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

测试结果
在这里插入图片描述

5.4 枚举单例抵制反射分析

  • 为什么使用getDeclaredConstructor(String.class,int.class),而不是 getDeclaredConstructor()?

    public abstract class Enum<E extends Enum<E>>
            implements Comparable<E>, Serializable {
    
        // ==========枚举Enum类,实际上就是Enum========
        // 所以反射调用父类构造方法
        protected Enum(String name, int ordinal) {
            this.name = name;
            this.ordinal = ordinal;
        }
    
    }
    
    

Constructor类的newInstance方法源码中:

public final class Constructor<T> extends Executable {

    @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
    // =============================如果是枚举的话,反射失败====================================
        if ((clazz.getModifiers() & Modifier.ENUM ) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }
}

发布了3 篇原创文章 · 获赞 1 · 访问量 88

猜你喜欢

转载自blog.csdn.net/weixin_39147889/article/details/103996455