设计模式(一):单例模式

运用场景

一个类在全局只需要一个实例,例如线程池,缓存注册或者配置对象。

实现方式

饿汉式单例

原理

它是在类加载的时候就立即初始化,并且创建单例对象。

优点

实现简单,绝对线程安全,在线程还没出现以前就实例化了没有加任何的锁,执行效率高

缺点

如果该类实例化需要加载大量资源,创建比较耗时,类加载的时候无论之后用没用到都会初始化,浪费了内存空间

测试代码


/**
 * @author X3471 wwh
 * @description  恶汉式单例
 * @create 2019-03-25
 **/
public class HungrySingleton {

    //私有化构造函数
    private HungrySingleton(){}

    private static HungrySingleton hungrySingleton = new HungrySingleton();

    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }

    public static class TestThread implements Runnable{

        private static CountDownLatch cd;

        public TestThread(CountDownLatch countDownLatch) {
            this.cd = countDownLatch;
        }

        @Override
        public void run() {
            try {
                cd.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            HungrySingleton hungrySingleton = HungrySingleton.getInstance();
            System.out.println(Thread.currentThread().getName() +":"+hungrySingleton);

        }
    }

    public static void main(String[] args) {
        CountDownLatch cdl = new CountDownLatch(1000);
        for(int i=0;i<1000;i++){
            Thread t = new Thread(new TestThread(cdl),i+"");
            t.start();
            cdl.countDown();
        }

    }
}

懒汉式单例

原理

所谓懒汉可以理解为延迟加载,相比于用静态变量的方式,只有在使用的时候才会初始化对象

实现方式一

/**
 * @author X3471 wwh
 * @description
 * @create 2019-03-25
 **/
public class LazySingletonSynchronized {

    private LazySingletonSynchronized() {
    }
    private static LazySingletonSynchronized lazySingleton = null;

    /**
     * 这个方法简单粗暴,保证了线程安全,达到了延迟加载的目的,但是存在的问题是每次进行getInstance的时候需要加锁,性能不是很好
     * @return
     */
    public static synchronized LazySingletonSynchronized getInstance(){

        if(null==lazySingleton){
            lazySingleton = new LazySingletonSynchronized();
        }
        return lazySingleton ;
    }

    public static class TestThread3 implements Runnable{
        private static CountDownLatch cd;
        public TestThread3(CountDownLatch countDownLatch) {
            this.cd = countDownLatch;
        }

        @Override
        public void run() {
            try {
                cd.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LazySingletonSynchronized lazySingleton = LazySingletonSynchronized.getInstance();
            System.out.println(Thread.currentThread().getName() +":"+lazySingleton);
        }
    }

    public static void main(String[] args) {
        CountDownLatch cdl = new CountDownLatch(1000);
        for(int i=0;i<1000;i++){
            Thread t = new Thread(new TestThread3(cdl),i+"");
            t.start();
            cdl.countDown();
        }
    }
}

方法分析

线程安全,懒加载。但是由于方法加锁,多线程的情况下性能不好。

仔细分析使用场景,发现第一次创建类对象的时候给方法加锁是对的,由于锁是加在方法上,当创建完对象后,每次获取实例仍然进行了加锁操作,这是可以优化的地方,所以有了下面的实现方式二

查漏补缺

看了双重检查锁的实现后,我有个疑问,lazySingleton是不是也要用volatile修饰呢?

问题就变成了synchronized如何保证可见性?原来获取锁的线程在释放锁之前会把工作内存中修改过的变量同步到主内存中,当其他线程重新获取同一个锁之后会重新获取主内存中的数据到自己的工作内存中去操作。

实现方式二(Double Check Lock)

/**
 * @author X3471 wwh
 * @description
 * @create 2019-03-26
 **/
public class DoubleCheckSingleton {

    //这里volatile是重点,保证可见性
    private static volatile DoubleCheckSingleton doubleCheckSingleton ;

    private DoubleCheckSingleton(){}

    /**
     * 优点:实现了延时加载,性能更佳,锁粒度更小,减少竞争。因为大部分情况第一次null判断返回的是false,就不需要进入锁了
     * @return
     */
    public static DoubleCheckSingleton getInstance(){
        if( null==doubleCheckSingleton){
            synchronized (DoubleCheckSingleton.class){
                if(null==doubleCheckSingleton){
                    doubleCheckSingleton = new DoubleCheckSingleton();
                }
            }
        }
        return doubleCheckSingleton;
    }
}

方法分析

  • 第一个null判断作用当对象创建完成后,读取对象的时候直接返回已经创建好的对象,优化了性能
  • 第二个null判断是避免当前线程获取到锁之后,对象已经被上一个获取到锁的线程创建完。
  • 声明对象的时候要用volatile修饰。假设A,B线程都从主内存拷贝了一份到自己的线程中操作,A线程创建了对象,如果不用volatile修饰,B线程是不可见的,会导致创建多个实例

注册式单例

原理

将对象的class作为key,实例对象作为value放入一个线程安全的集合容器中,容器中没有则利用反射创建实例,否则从容器中获取已经创建的实例

实现方式

public class SingletonManager {
    //线程安全的容器,饿汉式保证容器对象本身为单例
    private static Map map = new ConcurrentHashMap();

    //外部访问点,传入类名,返回该类的单例对象.该类会被登记进入上面的容器进行单例管理
    //在类中务必保证构造方法私有化,对这一点这个管理类是无法控制的,需要自己保证
    public static Object getInstance(String className) {
        //如果还没登记到容器
        if (!map.containsKey(className)) {
            //用反射的方式创建对象(因为已经构造函数私有化),并登记到容器中
            try {
                map.put(className, Class.forName(className).newInstance());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        //从容器中获取管理的单例对象并返回
        return map.get(className);
    }    
}

静态内部类单例

原理

利用静态内部类的方式在饿汉式的基础上进行了延时加载的优化。只有需要加载内部类的时候才会初始化单例对象

实现方式

public class InnerClassSingleton  implements Serializable {

    private InnerClassSingleton() {
    }

    private static class InnerClassSingletonHolder{

        private static InnerClassSingleton innerClassSingleton = new InnerClassSingleton();

    }

    public static InnerClassSingleton getInstance(){
        return InnerClassSingletonHolder.innerClassSingleton;
    }

}

以上方式存在的问题

序列化反序列化后会创建2个对象

public class SerializableFlawSingleton {

    public static void main(String[] args) {

        InnerClassSingleton singleton = InnerClassSingleton.getInstance();
        System.out.println(singleton);

        InnerClassSingleton singleton2 = deepcloneObj(singleton);
        System.out.println(singleton2);

        System.out.println(singleton==singleton2);
    }
    /**
     * 对象深拷贝,新的对象的修改不会影响原始对象的值
     */
    public static <T>T deepcloneObj(T obj) {
        ByteArrayOutputStream byteOut = new ByteArrayOutputStream() ;
        ObjectInputStream objIn;
        try
        {
            ObjectOutputStream objOut =new ObjectOutputStream(byteOut);
            objOut.writeObject(obj);
            ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());
            objIn = new ObjectInputStream(byteIn);
            return (T) objIn.readObject();
        }
        catch (Exception e)
        {
            System.out.println(e);
        }
        return null ;
    }
}

可以通过反射获取对象

public class ReflectFlawSingleton {

    public static void main(String[] args) throws Exception {

        InnerClassSingleton innerClassSingleton = InnerClassSingleton.getInstance();
        System.out.println(innerClassSingleton);

        Class clz = Class.forName("com.fh.iquery.design.singleton.inner.InnerClassSingleton");

        Constructor constructor = clz.getDeclaredConstructor();
        constructor.setAccessible(true);
        InnerClassSingleton innerClassSingleton2 = (InnerClassSingleton) constructor.newInstance();
        System.out.println(innerClassSingleton2);
        System.out.println(innerClassSingleton==innerClassSingleton2);
    }
}

应对方式

public class InnerClassSingleton implements Serializable{

    //保证是否被创建的标志位
    private static volatile boolean flag = true;

    private InnerClassSingleton() {
        //创建后改为false
        if(flag){
            flag=false;
        }
        //第二次调用构造函数的时候抛出异常
        else{
            throw new RuntimeException("repeat construction");
        }
    }

    private static class InnerClassSingletonHolder{

        private static InnerClassSingleton innerClassSingleton = new InnerClassSingleton();

    }

    public static InnerClassSingleton getInstance(){

        return InnerClassSingletonHolder.innerClassSingleton;
    }

    //反序列时直接返回当前对象
    private Object readResolve() {
        return InnerClassSingletonHolder.innerClassSingleton;
    }

}

枚举式单例

枚举的构造函数本身就是私有的,而且可以*序列化、线程安全、保证单例。使用枚举是实现单例模式的最佳方式。以前对枚举不太了解,实际上枚举就是一个final类,也一样可以有其它属性和方法,当成普通类来用实现单例极为简便。

public enum EnumSingleton {

    INSTANCE;

    private String name;

    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public static void main(String[] args) {
    
        System.out.println(EnumSingleton.INSTANCE==EnumSingleton.INSTANCE);
    }
}

枚举如何避免反射和序列化的

验证代码

package com.fh.iquery.design.singleton.enumtest;

import java.io.*;
import java.lang.reflect.Constructor;

/**
 * @author X3471 wwh
 * @description
 * @create 2019-05-06
 **/
public class EnumAttact {


    public static void main(String[] args) {
        EnumAttact.reflectionAttack();
        System.out.println("------------");
        EnumAttact.serializableAttack();
    }




    /**
     * 反射攻击。
     * 由于Enum天然的不允许反射创建实例,所以可以完美的防范反射攻击。
     */
    private static void reflectionAttack() {
        System.out.println("反射攻击单例对象-----------开始");

        try {
            Constructor con = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
            con.setAccessible(true);
            Object obj = con.newInstance("INSTANCE", 0); // 反射新建对象以破坏单例
            System.out.println(obj);
            System.out.println(EnumSingleton.INSTANCE);
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("反射攻击单例对象-----------结束");
    }

    /**
     * 序列化攻击
     * 需要在单例类中增加read
     */
    private static void serializableAttack() {
        System.out.println("序列化攻击单例对象-----------开始");
        EnumSingleton singleton = EnumSingleton.INSTANCE;
        System.out.println(singleton);
        try {
            Object obj;
            try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./EnumSingleton.out"))) {
                oos.writeObject(singleton);
            }
            try (ObjectInputStream ois = new ObjectInputStream((new FileInputStream("./EnumSingleton.out")))) {
                obj = ois.readObject();
            }
            System.out.println(obj);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println("序列化攻击单例对象-----------结束");
    }
}

分析

  • 避免反射创建单例对象(反射攻击)

    看看源码Constructor.java中的newInstance方法,会判断反射出来的对象类型,如果类型是枚举类直接抛出了异常

设计模式(一):单例模式

  • 避免通过序列化创建单例对象(如果单例类实现了Serializable)(序列化攻击)

    1. 进入 ois.readObject()方法

    设计模式(一):单例模式

    1. 进入readObject0方法,找到switch-case block中枚举那段,进入readEnum方法

    设计模式(一):单例模式

    1. 返回值是result,所以我们就需要分析什么地方给这个赋值

      设计模式(一):单例模式

    2. 核心就在这个Enum.valueOf方法中,参数是类型和名字。看了这段代码,说明一个枚举

      设计模式(一):单例模式

总结

实现方式 优点 缺点 实现
饿汉式 简单,线程安全 类加载的时候就会创建对象,如果该过程比较耗费资源但是又不一定被使用到,这种方式就会浪费内存 使用静态变量的方式
静态内部类 在饿汉式的基础上实现了延迟加载 仍然可以通过反序列化或反射创建出新的实例 将静态变量创建的创建过程放到内部类中,只有使用内部类的时候才会创建单例对象
懒汉式(锁方法) 简单,线程安全,延迟加载 每次获取对象的时候都要加锁,性能不好 利用Synchronized关键字锁住获取对象的方法
懒汉式(DCL) 线程安全,并发性能好,延迟加载 仍然可以通过反序列化或反射创建出新的实例 双重检查;一重解决并发读时不必要的加锁性能消耗;二重解决获取锁后对象已被其他线程创建;单例对象要用volatile修饰保证可见性。
枚举 写法简单,线程安全,安全等级最高,解决了反射和反序列化破坏单例的问题 利用枚举类自身的特点,构造函数本身就是私有的,而且可以*序列化、线程安全、保证单例