Part1_精讲设计模式_1.11 深入理解单例模式

                                        Part 1 精讲设计模式

                                               1.11 深入理解单例模式

                                                                                                                                                                                              田超凡

                                                                                                                                                                                2019年11月13日

1 单例模式

在单例模式(singleton)中,同一个类型的对象有且只有一个(在一个JVM中同一个类型的对象只有一个)。这种设计模式的特点就是可以提高对象之间的引用效率,实现全局共享,节约JVM内存空间。在单例模式中,每个单例类需要提供一个全局访问点,实现单例模式的核心是定义私有构造函数(不允许在单例类外部创建单例类实例,只能在单例类内部创建这个唯一实例),最后定义方法返回这个单例实例给其他对象访问。

 

2 单例模式的特点

  1. 单例类有且仅有一个实例
  2. 单例类必须自己创建自己的实例
  3. 单例类必须提供这个实例给外部其他对象访问

 

3 单例模式实现方式

  1. 懒汉模式(线程不安全)
  2. 懒汉模式(线程安全)
  3. 饿汉模式
  4. 静态内部类
  5. 双重校验锁
  6. 枚举(最靠谱的单例模式实现,防止反射和序列化破坏单例)
  7. 容器类加载

 

4 单例模式优缺点

优点:

1、单例类有且只有一个实例

2、共享资源,全局使用

3、节省创建时间,提高性能

 

缺点:

可能存在线程不安全的问题需要考虑并解决线程安全问题

 

5 单例模式实现

5.1 懒汉模式(线程不安全)

public class SingletonV2 {

    /**
     * 懒汉式 (线程不安全)
     */
    private static SingletonV2 singletonV2;

    private SingletonV2() {

    }

    /**
     * 在真正需要创建对象的时候使用...
     *
     * @return
     */
    public static SingletonV2 getInstance() {
        if (singletonV2 == null) {
            try {
                Thread.sleep(2000);
            } catch (Exception e) {
            }
            singletonV2 = new SingletonV2();
        }
        return singletonV2;
    }

    public static void main(String[] args) {
//        SingletonV2 instance1 = SingletonV2.getInstance();
//        SingletonV2 instance2 = SingletonV2.getInstance();
//        System.out.println(instance1 == instance2);
        // 1.模拟线程不安全
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                public void run() {
                    SingletonV2 instance1 = SingletonV2.getInstance();
                    System.out.println(Thread.currentThread().getName() + "," + instance1);
                }
            }).start();
        }
    }
}

 

 

5.2 懒汉模式(线程安全)

public class SingletonV3 {
    /**
     * 懒汉式 线程安全
     */
    private static SingletonV3 singletonV3;

    private SingletonV3() {

    }

    /**
     * 能够解决线程安全问题,创建和获取实例时都上锁 ,效率非常低,所以推荐使用双重检验锁
     *
     * @return
     */
    public synchronized static SingletonV3 getInstance() {
        try {
            Thread.sleep(2000);
        } catch (Exception e) {
        }
        if (singletonV3 == null) {
            System.out.println("创建实例SingletonV3");
            singletonV3 = new SingletonV3();
        }
        System.out.println("获取SingletonV3实例");
        return singletonV3;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                public void run() {
                    SingletonV3 instance1 = SingletonV3.getInstance();
                    System.out.println(Thread.currentThread().getName() + "," + instance1);
                }
            }).start();
        }
    }
}

 

 

5.3 饿汉模式

public class SingletonV1 {

    /**
     * 饿汉式 优点:先天性线程是安全的,当类初始化的 就会创建该对象 缺点:如果饿汉式使用过多,可能会影响项目启动的效率问题。
     */
    private static SingletonV1 singletonV1 = new SingletonV1();

    /**
     * 将构造函数私有化 禁止初始化
     */
    private SingletonV1() {

    }

    public static SingletonV1 getInstance() {
        return singletonV1;
    }

    public static void main(String[] args) {
        SingletonV1 instance1 = SingletonV1.getInstance();
        SingletonV1 instance2 = SingletonV1.getInstance();
        System.out.println(instance1 == instance2);

    }
}

 

饿汉模式:类加载速度慢,获取对象速度快,线程安全,启动效率低 

优点先天性线程是安全的,当类初始化的时候就会创建该对象

缺点如果饿汉模式使用过多,会导致每个单例类在初始化的时候就要创建实例,可能会影响项目启动的效率问题。

 

5.4 静态内部类

public class SingletonV5 {

    private SingletonV5() {
        System.out.println("对象初始...");
    }

    public static SingletonV5 getInstance() {
        return SingletonV5Utils.singletonV5;
    }

    /**
     * 静态内部方式能够避免同步带来的效率问题和有能实现延迟加载
     */
    public static class SingletonV5Utils {
        private static SingletonV5 singletonV5 = new SingletonV5();
    }

    public static void main(String[] args) {
        System.out.println("项目启动成功");
        SingletonV5 instance1 = SingletonV5.getInstance();
        SingletonV5 instance2 = SingletonV5.getInstance();
        System.out.println(instance1 == instance2);
    }
}

 

 

5.5 双重校验锁(Double Check Lock)

public class SingletonV4 {
    /**
     * volatile 禁止重排序和 提高可见性
     */
    private volatile static SingletonV4 singletonV4;

    private SingletonV4() {

    }

    public static SingletonV4 getInstance() {
        if (singletonV4 == null) { // 第一次判断如果没有创建对象 开始上锁...
            synchronized (SingletonV4.class) {
                if (singletonV4 == null) { // 当用户抢到锁,判断初始化
                    System.out.println("第一次开始创建实例对象....获取锁啦...");
                    try {
                        Thread.sleep(2000);
                    } catch (Exception e) {
                    }
                    singletonV4 = new SingletonV4();
                }
            }
        }
        return singletonV4;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                public void run() {
                    SingletonV4 instance1 = SingletonV4.getInstance();
                    System.out.println(Thread.currentThread().getName() + "," + instance1);
                }
            }).start();
        }
    }
}

 

 

5.6 枚举

枚举的方式实现代理模式可以避免使用反射和序列化破坏单例

public enum EnumSingleton {
    INSTANCE;

    // 枚举能够绝对有效的防止实例化多次,和防止反射和序列化破解
    public void add() {
        System.out.println("add方法...");
    }
}

 

public static void main(String[] args) throws Exception {
    EnumSingleton instance1 = EnumSingleton.INSTANCE;
    EnumSingleton instance2 = EnumSingleton.INSTANCE;
    System.out.println(instance1 == instance2);
    Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor();
    declaredConstructor.setAccessible(true);
    EnumSingleton v3 = declaredConstructor.newInstance();
    System.out.println(v3==instance1);
}

 

 

 

5.7 容器类加载

public class SingletonManager {
    private static Map<String, Object> objMap = new HashMap<String, Object>();
    public static void registerService(String key, Object instance) {
        if (!objMap.containsKey(key)) {
            objMap.put(key, instance);
        }
    }
    public static Object getService(String key) {
        {
            return objMap.get(key);
        }
    }
}

 

这种使用容器类加载实现代理模式的方式,将多种单例类统一管理,在使用时根据key获取不同单例类对应类型的对象这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。

 

5 单例破坏和单例保护

尽管单例类都使用了私有构造函数来确保在单例类外部不能创建单例类实例,但是我们仍然可以通过反射和序列化机制来破坏单例。

基于反射机制破坏单例模式的例子如下:

// 1. 使用懒汉式创建对象
SingletonV3 instance1 = SingletonV3.getInstance();
// 2. 使用Java反射技术初始化对象 执行无参构造函数
Constructor<SingletonV3> declaredConstructor = SingletonV3.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
SingletonV3 instance2 = declaredConstructor.newInstance();
System.out.println(instance1 == instance2);

输出:false

 

基于序列化机制破坏单例模式的例子如下:

Singleton instance = Singleton.getInstance();
FileOutputStream fos = new FileOutputStream("E:\\code\\Singleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance);
oos.flush();
oos.close();

FileInputStream fis = new FileInputStream("E:\\code\\Singleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Singleton singleton2 = (Singleton) ois.readObject();
System.out.println(singleton2==instance)

 

//返回序列化获取对象 ,保证为单例
public Object readResolve() {
    return singletonV3;
}

输出:false

 

单例保护的实现:

  1. 可以通过在单例类的私有构造函数中加入校验锁来防止单例模式被反射或序列化破坏。
  2. 把单例类定义为枚举类型,枚举具有先天性单例安全性,可以防止反射和序列化破坏单例。

private SingletonV3() throws Exception {
    synchronized (SingletonV3.class) {
        if (singletonV3 != null) {
            throw new Exception("该对象已经初始化..");
        }
        System.out.println("执行SingletonV3无参构造函数...");
    }

}

 

6 基于反编译技术深入理解枚举单例底层实现

Java反射机制

Java反射机制用来动态获取当前类的信息,比如属性、方法等。

Java反射技术使用场景

  1. JDBC加载驱动
  2. Spring IOC/DI
  3. 初始化对象
  4. 提供扩展功能

使用反射初始化无参对象

public static UserEntity getReflexUser() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
    Class<?> classInfo = Class.forName("com.demo.singleton.v8.UserEntity");
    UserEntity userEntity = (UserEntity) classInfo.newInstance();
    return userEntity;
}

 

使用反射初始化有参对象

public static UserEntity getReflexUser(String userName, Integer age) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    Class<?> classInfo = Class.forName("com.demo.singleton.v8.UserEntity");
    Constructor<?> declaredConstructor = classInfo.getDeclaredConstructor(String.class, Integer.class);
    Object mayikt = declaredConstructor.newInstance(userName, age);
    return (UserEntity) mayikt;
}

 

枚举单例为什么不能够反射初始化

 

枚举底层原理

1.首先如果使用java的反射机制使用无参构造函数创建枚举实例来破坏枚举单例模式,报错

通过该错误说明,枚举类中没有默认的无参构造函数

 

2.使用java反编译技术,查看枚举类

 

 

从该图可以得出一个结论,枚举底层实现其实还是

 

1.枚举类底层原理分析

使用静态代码快方式,当静态代码快执行的时候初始化该对象,从而可以让开发者使用通过EnumSingleton.INSTANCE使用。

  1. 在该反编译源码中,定义了一个类继承了Enum,该类中是没有无参构造函数的,所以反射机制调用无参构造函数是无法初始化的,在该类中只有一个带参构造函数。

 

调用父类构造构造函数

 

name表示调用对象的名称,ordinal表示调用对象的序号

 

4.注入有参构造函数也不能破坏枚举的单例模式。

 

Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
EnumSingleton v3 = declaredConstructor.newInstance();
System.out.println(v3 == instance1);

 

 

枚举无法通过反射和序列化破坏单例的主要原因是在基于Java反射初始化对象,只要对象是枚举类型根本不会进行初始化也就不会调用枚举的构造函数。因此,强制使用反射或序列化破坏枚举单例模式实现会直接抛出异常。

 

java.lang.reflect.Constructor类 newInstance方法关于枚举实例创建部分的代码实现如下:

 

综上所述,在单例模式常用的七种实现方式中,枚举是最可靠的,因为它具有先天的单例安全性,可以防止反射和序列化破坏单例,而不需要显式控制枚举实例是否单例,因为反射和序列化破坏枚举单例的拦截在Java反射机制中已经默认帮我们实现了。

 

转载请注明原作者

 

发布了100 篇原创文章 · 获赞 10 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/qq_30056341/article/details/103041491