单例模式

https://www.jianshu.com/u/4b8ac8f11b7d

定义:确保某一个类只有一个实例,自行实例化并且向整个系统提供这个实例。

单例模式的优点:

  1. 提高效率
  2. 避免对资源的多重占用
  3. 在系统设置全局访问点,优化和共享资源访问。

单例模式的缺点:

  1. 单例模式一般没有接口,扩展困难。
  2. 单例测试对于测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的。没有接口,也不能使用mock的方式虚拟一个对象。
  3. 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心是否是单例的。单例模式把单例和业务逻辑融合在一个类中。

单例模式的应用场景

在一个系统中,要求一个类有且仅有一个实例,如果出现多个实例就会出现副作用,可以采用单例模式。具体如下:

  1. 要求生成唯一序列号的环境。
  2. 在整个项目中需要共享一个访问点或者数据。
  3. 创建和销毁一个对象需要消耗的资源过多,但是又经常用到。如,要访问IO和数据库连接等。
  4. 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(也可以直接声明为static)。
  5. 需要频繁的进行创建和销毁的对象。

单例模式的最佳实践

单例模式比较简单,也应用广泛。在Spring中,每个Bean默认是单例的,优点是Spring容器可以管理这些Bean的生命周期,决定对象的创建和销毁时机,以及创建和销毁对象时的处理。

单例模式常见的实现方式

单例模式的实现可以分为两类:饿汉式(饥汉式)和懒汉式。
饿汉式:在程序启动或单例模式类被加载的时候,单例模式实例就已经被创建。
懒汉式:当程序第一次访问单例模式实例的时候才进行创建。
以上两种方式各有优点。

  • 如果单例模式实例在系统中会被频繁用到,饿汉式比较好
    优点:程序启动的时候已经进行了实例化,调用时直接返回实例,速度快,效率高。
    缺点:如果实例使用频率不高或者几乎不用,启动的时候就进行实例化,浪费内存资源。
  • 如果单例模式实例在系统中很少用到或者几乎不会用到,懒汉式较好
    优点:如果实例使用频率不高或者几乎不用,启动的时候就不进行实例化,第一次调用的时候进行实例化(lazy-loading),节约内存资源。
    缺点:单例模式的实例如果被频繁调用,影响效率。

代码实现(较为实用)


双重检查:

  
public class Singleton {
    private static volatile Singleton singleton;
    private Singleton() {}

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

设置volatile是为了保证可见性,避免重排序。

 singleton = new Singleton();包含三个动作:1:分配内存给这个对象;2:初始化对象;3:设置了引用指向刚分配内存的对象。2、3有可能重排序
lazy-load的单例可能被破坏:
序列化和反序列化会破坏单例:可通过重写readResolve方法返回当前对象加以保护
public class SerializableDemo1 {
    //为了便于理解,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记
    //Exception直接抛出
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //Write Obj to file
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
        oos.writeObject(Singleton.getSingleton());
        //Read Obj from file
        File file = new File("tempFile");
        ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));
        Singleton newInstance = (Singleton) ois.readObject();
        //判断是否是同一个对象
        System.out.println(newInstance == Singleton.getSingleton());
    }
}

通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性。

 

防止序列化破坏单例模式

先给出解决方案,然后再具体分析原理:

只要在Singleton类中定义readResolve就可以解决该问题:

public class Singleton implements Serializable{
    private volatile static Singleton singleton;
    private Singleton (){}
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    private Object readResolve() {
        return singleton;
    }
}

反射攻击

什么是反射攻击呢?

  1. 在Java中,由于反射的功能实在是太强了,通过动态访问类并设置Access使得可以访问对象的私有属性方法等。
  2. 在单例模式中,我们使用private 修饰构造方法对外隐藏,防止外部new 对象,但是在反射的存在下,private的存在形同虚设,通过反射设置Access即可访问构造方法,这时的单例就不是单例了。
  try {
            Constructor<CommonUtil> constructor = CommonUtil.class.getDeclaredConstructor(Class.class);
            constructor.setAccessible(true);//将构造方法的私有属性放开
            CommonUtil commonUtil = constructor.newInstance();

        } catch (Exception e) {
            e.printStackTrace();
        }

预防反射攻击

要避免通过反射来调用私有构造器这是行不通的,那么该如何做呢,这里有两种做法。

  • 当尝试使用构造方法new 对象时,直接抛出异常
  • 使用枚举,枚举类jvm底层保证了不可new。

第一种:

package com.fine.reflect.enhance;

/**
 * 单例
 * volatile 双重校验
 *
 * @author finefine at: 2019-05-02 22:22
 */
public class Singleton {
    private volatile static Singleton INSTANCE;

    private Singleton() {
        //如果已存在,直接抛出异常,保证只会被new 一次
        if (INSTANCE != null) {
            throw new RuntimeException("对象已存在不可重复创建");
        }
    }

    public static Singleton getInstance() {

        if (INSTANCE==null){

            //同步代码块
            synchronized (Singleton.class){
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

第二种:

/**
 * 单例
 * 枚举
 *
 * @author finefine at: 2019-05-02 22:22
 */
public enum  SingletonEnum {
    INSTANCE;
}

package com.fine.reflect.enhance;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;


public class SingletonEnumTest {

    public static void main(String[] args) {
        SingletonEnum singletonEnum = SingletonEnum.INSTANCE;

        Class clazz = singletonEnum.getClass();
        try {
            Constructor<Singleton> constructor = clazz.getDeclaredConstructors()[0];

            //设置允许访问私有的构造器
            constructor.setAccessible(true);
            Singleton singleton1 = constructor.newInstance();

            if (singleton1 != null && singleton1.getClass().equals(singletonEnum.getClass())) {
                System.out.println("通过反射构造除了对象");
            } else {
                return;
            }

        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

    }
}

静态内部类

public class Singleton {
    private Singleton() {}

    public static Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }

    private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }
}
这种方式跟饿汉式方式采用的机制类似,但又有不同。
  两者都是采用了类装载的机制来保证初始化实例时只有一个线程。
  不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
  类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
优点:避免了线程不安全,延迟加载,效率高。

容器单例

public class ContainerSingleton {

    private ContainerSingleton(){

    }
    private static Map<String,Object> singletonMap = new HashMap<String,Object>();

    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);
    }
}

这种方式实现的单例是线程不安全的。如果需要线程安全的可以使用HashTable但是HashTable每次存取都会加上同步锁,性能损耗比较严重。无法规避反射攻击和序列化破坏

ThreadLocal “单例“

这个单例严格意义上讲并不完全算是单例,它只能算在单个线程中的单例,也就是在同一个线程中的它是单例的。在全局上是非单例的;

public class ThreadLocalInstance {
    private static final ThreadLocal<ThreadLocalInstance> threadLocalInstanceThreadLocal
             = new ThreadLocal<ThreadLocalInstance>(){
        @Override
        protected ThreadLocalInstance initialValue() {
            return new ThreadLocalInstance();
        }
    };
    private ThreadLocalInstance(){

    }
    public static ThreadLocalInstance getInstance(){
        return threadLocalInstanceThreadLocal.get();
    }

}

 运行结果:

 同步锁:以时间换空间

threadLocal: 空间换时间

 枚举单例
/**
 * @ClassName SingleThreadLazy
 * @Description 懒加载 枚举实现单例
 * @Author xinsen.liao
 * @Date 2020/7/1  10:27
 */
public class SingleThreadEnumLazy {


    private SingleThreadEnumLazy() {
    }

    private enum InnerEnumInstance {
        INSTANCE;
        private SingleThreadEnumLazy singleThreadEnumLazy;

        private InnerEnumInstance() {
            singleThreadEnumLazy = new thread.SingleThreadEnumLazy();
        }

        private SingleThreadEnumLazy getInstance() {
            return singleThreadEnumLazy;
        }
    }

    public static SingleThreadEnumLazy getInstance() {
        return SingleThreadEnumLazy.InnerEnumInstance.INSTANCE.getInstance();
    }

    public static void main(String[] args) {
        IntStream.rangeClosed(1, 100).forEach(i -> new Thread(i + "-thread") {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "..." + SingleThreadEnumLazy.getInstance());
            }
        }.start());
    }

}
 

参考:

JAVA设计模式总结之23种设计模式
链接1:https://www.jianshu.com/p/2b45a08839d0
链接2:https://www.jianshu.com/p/dd238dc20e22
 
 
原文地址:https://www.cnblogs.com/personsiglewine/p/12971030.html