单例模式的几种实现方式及细节

单例模式的应用场景

单例模式(Singelton Pattern) 是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点.单例模式是创建型模式.
一、饿汉式单例模式
1.常规写法
/**
 * Created by 于占峰 on 2020/2/27/027.
 * 饿汉单例模式
 */
public class HungrySingleton {

    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton() {
    }

    public HungrySingleton getInstans(){
        return hungrySingleton;
    }
}

饿汉式单例模式在类加载时便立即初始化,并创建单例对象.线程绝对安全,在线程没出现前便实例化,不存在访问安全问题,适用于单例对象较少的情况.

优点:

没有加任何锁、执行效率高

缺点:

类加载的时候就初始化,用与不用都占用内存空间.

2.静态代码块装逼写法:
/**
 * Created by 于占峰 on 2020/2/27/027.
 * 饿汉单例模式
 */
public class HungrySingleton {

/*    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton() {
    }

    public HungrySingleton getInstans(){
        return hungrySingleton;
    }*/

    //装逼写法

    private static final HungrySingleton hungrySingleton ;

    static {
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton(){};

    public HungrySingleton getInstans(){
        return hungrySingleton;
    }
}

两种没啥太大区别,主要是第二种更优雅,装逼必备奥铁汁~

二、懒汉式单例模式
1.常规懒汉模式
/**
 * Created by 于占峰 on 2020/2/27/027.
 * 懒汉模式
 */
public class LazySingleton {

    private static LazySingleton lazySingleton = null;

    private LazySingleton() {}

    public LazySingleton getInstans(){
        if (null == lazySingleton){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

懒汉式单例模式在被外部类调用时内部类才会加载,但存在线程安全问题.

2.多线程破坏问题

创建线程测试类进行测试:

/**
 * Created by 于占峰 on 2020/2/27/027.
 */
public class LazySingletonThread implements Runnable{

    @Override
    public void run() {
        LazySingleton lazySingleton = LazySingleton.getInstans();
        System.out.println("线程:"+Thread.currentThread().getName()+":"+lazySingleton);
    }
}

/**
 * Created by 于占峰 on 2020/2/27/027.
 */
public class LazySingletonTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new LazySingletonThread());
        Thread t2 = new Thread(new LazySingletonThread());
        t1.start();
        t2.start();
    }
}

第一次运行从对象地址来看似乎没什么问题

线程:Thread-1:yzf.test.LazySingleton.LazySingleton@494fccb
线程:Thread-2:yzf.test.LazySingleton.LazySingleton@494fccb

当多次运行后:

线程:Thread-2:yzf.test.LazySingleton.LazySingleton@489747c
线程:Thread-1:yzf.test.LazySingleton.LazySingleton@494fccb

能够看到受线程影响单例模式被破坏掉了,但是通过debug调试可以发现即使控制台看起来两次线程实例化的对象地址是一样,实际上有可能是被第一个线程实例化的对象被第二个线程覆盖掉了,就相当于

线程1:A 线程:B 最终的打印结果是 线程1:B 线程:B 还是破坏了单例模式

针对这种线程不安全的问题可以通过给getInstans()方法添加synchronized解决:

3.使用synchronized锁解决线程安全问题
public class LazySingleton {

    private static LazySingleton lazySingleton = null;

    private LazySingleton() {}

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

但是这样做在线程特别多的情况就会出现大批线程阻塞的情况,因此还可以在进一步采用双重检查锁来解决:

4.使用双重检查锁解决性能问题
public class LazySingleton {

    private static LazySingleton lazySingleton = null;

    private LazySingleton() {
    }

    //双重校验锁
    public static synchronized LazySingleton getInstans() {
        if (null == lazySingleton) {
            synchronized (LazySingleton.class) {
                if (null == lazySingleton) {
                    lazySingleton = new LazySingleton();
                }
            }
        }
        return lazySingleton;
    }
}

这样阻塞就只发生在getinstans方法内,能够极大解决性能问题,但是加锁必然还是会影响性能,接下来我们采取更好的方案.

5.用静态内部类单例模式解决性能和线程问题
/**
 * Created by 于占峰 on 2020/2/27/027.
 */
public class StaticInnerSingleton {

    private StaticInnerSingleton(){}

    private static final StaticInnerSingleton getInstance(){
        return InerClass.staticInnerSingleton;
    }

    private static class InerClass{
        private static final StaticInnerSingleton staticInnerSingleton = new StaticInnerSingleton();
    }
}

采用这种方式可以完美解决线程安全和性能浪费的问题,当StaticInnerSingleton被扫描加载时内部类InerClass并不会被扫描,只有当调用StaticInnerSingleton类时才会被实例化,相对以上来说已经比较完美,但也还是存在一些问题.

三、反射破坏单例

尽管以上几部我们已经做得很好,但在一些情况下还是会出现问题,比如运用反射机制.

/**
 * Created by 于占峰 on 2020/2/27/027.
 */
public class StaticInnerSingletonTest {

    public static void main(String[] args){
        try{
        Class<?> clazz = StaticInnerSingleton.class;
        //通过反射机制获得私有的构造方法
        Constructor constructor  = clazz.getDeclaredConstructor(null);
        //强制访问
        constructor.setAccessible(true);
        //创建两次实例 验证结果
        Object staticInnerSingleton1 = constructor.newInstance();
        Object staticInnerSingleton2 = constructor.newInstance();
        System.out.println(staticInnerSingleton1 == staticInnerSingleton2);
        }catch(Exception e){
            e.printStackTrace();
    }
    }
}

最终的运行结果是false,说明单例模式被破坏了,虽然很少有人这么干但无论如何还是有问题,所以就要解决这个问题,解决方法很简单直接在StaticInnerSingletonTest的构造方法内加入不为空判断即可,即当StaticInnerSingletonTest还从来没有被吊用过那么可以调用并实例化,如果已经实例化了,那么在此使用反射机制进行调用便抛出异常阻止.

/**
 * Created by 于占峰 on 2020/2/27/027.
 * 内部类单例模式
 */
public class StaticInnerSingleton {

    private StaticInnerSingleton(){
        //加入判断,重复创建抛出异常
        if(null != InerClass.staticInnerSingleton){
            throw new RuntimeException("小伙子不可乱搞哦~");
        }
    }
    private static final StaticInnerSingleton getInstance(){
        return InerClass.staticInnerSingleton;
    }
    private static class InerClass{
        private static final StaticInnerSingleton staticInnerSingleton = new StaticInnerSingleton();
    }
}

Caused by: java.lang.RuntimeException: 小伙子不可乱搞哦~

至此便可以说算是完美,但事实往往没有那么简单~

四、序列化破坏单例模式

首先有一个单例模式的对象

/**
 * Created by 于占峰 on 2020/2/27/027.
 * 反序列化导致破坏单例模式
 */
public class SeriableSingleton implements Serializable {
    //序列化就是把内存中的状态通过转换成字节码的形式
    //从而转换一个 I/O 流,写入其他地方(可以是磁盘、网络 I/O)
    //内存中的状态会永久保存下来
    //反序列化就是将已经持久化的字节码内容转换为 I/O 流
    //通过 I/O 流的读取,进而将读取的内容转换为 Java 对象
    //在转换过程中会重新创建对象 new
    public final static SeriableSingleton seriableSingleton = new SeriableSingleton();
    private SeriableSingleton(){}
    public static SeriableSingleton getInstance(){
        return seriableSingleton;
    }
}

使用序列化将内存中的对象写入磁盘在读取实例化

public class SeriableSingletonTest {
    public static void main(String[] args) {
        SeriableSingleton s1 = null;
        SeriableSingleton s2 =  SeriableSingleton.getInstance();
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();
            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SeriableSingleton)ois.readObject();
            ois.close();
            System.out.println(s1);
            System.out.println(s2);

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

最终的运行结果

yzf.test.SeriaBleSingleton.SeriableSingleton@568db2f2
yzf.test.SeriaBleSingleton.SeriableSingleton@7f31245a

从运行结果可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,破坏了单例模式,针对这种情况可以在类中增加一个readResolve()方法(大小写要完全一样).

/**
 * Created by 于占峰 on 2020/2/27/027.
 * 反序列化导致破坏单例模式
 */
public class SeriableSingleton implements Serializable {
    //序列化就是把内存中的状态通过转换成字节码的形式
    //从而转换一个 I/O 流,写入其他地方(可以是磁盘、网络 I/O)
    //内存中的状态会永久保存下来
    //反序列化就是将已经持久化的字节码内容转换为 I/O 流
    //通过 I/O 流的读取,进而将读取的内容转换为 Java 对象
    //在转换过程中会重新创建对象 new
    public final static SeriableSingleton seriableSingleton = new SeriableSingleton();
    private SeriableSingleton(){}
    public static SeriableSingleton getInstance(){
        return seriableSingleton;
    }
    //加入readResolve 防止序列化破坏单例模式
    public Object readResolve(){
        return seriableSingleton;
    }
}

原理是ObjectInputStream会反射判断读取的对象是否有readResolve方法如果有则不返回新创建的对象,实际上还是创建了两次,如果创建对象的动作发生频率加快内存分配开销也会随之增大.

五、注册单例模式

注册式单例模式又称为登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识
获取实例。注册式单例模式有两种:一种为枚举式单例模式,另一种为容器式单例模式。

1.枚举类单例模式

创建一个枚举类

/**
 * Created by 于占峰 on 2020/2/27/027.
 * 枚举注册单例模式
 */
public enum EnumSingleton {
    INSTANCE;
    private Object data;
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }

测试类

/**
 * Created by 于占峰 on 2020/2/27/027.
 */
public class EnumSingletonTest {
    public static void main(String[] args) {
        try {
            EnumSingleton instance1 = null;
            EnumSingleton instance2 = EnumSingleton.getInstance();
            instance2.setData(new Object());
            FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(instance2);
            oos.flush();
            oos.close();
            FileInputStream fis = new FileInputStream("EnumSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            instance1 = (EnumSingleton) ois.readObject();
            ois.close();
            System.out.println(instance1.getData());
            System.out.println(instance2.getData());
            System.out.println(instance1.getData() == instance2.getData());
        }catch (Exception e){
            e.printStackTrace();
        }
    }

}

最终的运行结果是true

枚举单例模式是一种推荐的实现方式,枚举类单例模式是饿汉模式的实现,通过类名和类对象找到唯一的枚举对象,不可能被类加载器加载多次,反射机制的newInstance方法中做了强制判断,如果修饰符Modifier.ENUM枚举类型,则直接抛出异常。JDK 枚举的语法特殊性及反射也为枚举保驾护航,让枚举式单例模式成为一种比较优雅的实现

2.IOC容器式单例
public static Object getBean(String className){
synchronized (ioc) {
if (!ioc.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className).newInstance();
ioc.put(className, obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
} else {
return ioc.get(className);
}
}
}
解决IOC容器式单例线程不安全问题
/**
 * Created by 于占峰 on 2020/2/29/029.
 * 加入双重校验锁 防止线程安全问题
 */
public class IocSingletonThread {
    private static Map<String, Object> iocMap = new ConcurrentHashMap<>();

    public IocSingletonThread() {
    }

    public static Object getBean(String className) {
        Object bean = null;
        //双层校验锁
        if (!iocMap.containsKey(className)) {
            synchronized (IocSingletonThread.class) {
                if (!iocMap.containsKey(className)) {
                    try {
                        bean = Class.forName(className).newInstance();
                        iocMap.put(className, bean);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    return bean;
                } else {
                    return iocMap.get(className);
                }
            }
        }
        return iocMap.get(className);
    }
}
六、线程单例实现 ThreadLocal

ThreadLocal 不能保证其创建的对象是全局唯一的,但是能保证在单个线程中是唯一的,天生是线程安全的。

/**
 * Created by 于占峰 on 2020/2/27/028.
 */
public class ThreadLocalSingleton {
    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
            new ThreadLocal<ThreadLocalSingleton>(){
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };
    private ThreadLocalSingleton(){}
    public static ThreadLocalSingleton getInstance(){
        return threadLocalInstance.get();
    }
}

单例模式为了达到线程安全的目的,会给方法上锁,以时间换空间。ThreadLocal 将所有的对象全部放在 ThreadLocalMap 中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程隔离的