设计模式-单例模式学习之旅

604 阅读7分钟

“这是我参与8月更文挑战的第14天,活动详情查看:8月更文挑战

上一篇:设计模式-工厂模式学习之旅

一、单例模式定义

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛,例如,公司CEO、部门经理等。J2EE标准中的ServletContext、ServletContextConfig等,Spring框架应用中的ApplicationContext、数据库的连接池等也都是单例模式。

二、饿汉式单例模式

饿汉式单例模式在类加载的时候就立即初始化,并且创建单例对象。它绝对线程安全,在线程还没出现以前就实例化了,不可能存在访问安全问题。

接下来我们看下饿汉式单例的标准代码:

public class HungrySingleton {

    /**
     * 加载机制:
     * 先静态,后动态
     * 先属性,后方法
     * 从上而下
     */

    private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return HUNGRY_SINGLETON;
    }
}

还有另外一种写法,利用静态代码块的机制:

public class HungryStaticSingleton {

    private static final HungryStaticSingleton HUNGRY_STATIC_SINGLETON;

    static {
        HUNGRY_STATIC_SINGLETON = new HungryStaticSingleton();
    }

    private HungryStaticSingleton() {
    }

    public static HungryStaticSingleton getInstance() {
        return HUNGRY_STATIC_SINGLETON;
    }
}

这两种写法都非常简单,也非常好理解,饿汉式单例模式适用于单例对象较少的情况。这样写可以保证绝对线程安全、执行效率比较高。但是它的缺点也很明显,就是所有对象类加载的时候就实例化。这样一来,如果系统中有大批量的单例对象存在,那系统初始化时就会导致大量的内存浪费。也就是说,不管对象用与不用都占着空间,浪费了内存,有可能 “站着茅坑不拉屎”。

三、懒汉式单例模式

为了解决饿汉式单例可能带来的内存浪费问题,于是就出现了懒汉式单例的写法,懒汉式单例模式的特点是,单例对象是在被使用时才会初始化,下面看懒汉式单例模式的简单实现。

public class LazySimpleSingleton {

    private LazySimpleSingleton() {

    }

    private static LazySimpleSingleton instance;

    public static LazySimpleSingleton getInstance() {
        if (instance == null) {
            instance = new LazySimpleSingleton();
        }
        return instance;
    }
}

但这样写又带来一个新的问题,如果在多线程环境下,就会出现线程安全问题(不唯一)。简单模拟下:

public class LazySimpleSingletonTest {

    @Test
    public void test() {
        new Thread(new MyThread()).start();
        new Thread(new MyThread()).start();
        System.out.println("end");
    }

    class MyThread implements Runnable {

        @Override
        public void run() {
            LazySimpleSingleton singleton = LazySimpleSingleton.getInstance();
            System.out.println(Thread.currentThread().getName() + ": " + singleton);
        }
    }
}

可以利用idea的线程调试模式,干预多线程的执行流程:

image.png

image.png

那么,我们如何来优化代码,使得如何来优化代码,使得懒汉式单例模式在多线程环境下安全昵?来看下面的代码,给getInstance()方法上加synchronized关键字,使这个方法变成线程同步方法:

public class LazySimpleSingleton {

    private LazySimpleSingleton() {

    }

    private static LazySimpleSingleton instance;

    public static synchronized LazySimpleSingleton getInstance() {
        if (instance == null) {
            instance = new LazySimpleSingleton();
        }
        return instance;
    }
}

线程安全的问题解决了。但是,用synchronized加锁时,在线程数量比较多的情况下,则会导致大批线程阻塞,从而导致程序性能大幅下降。那么,有没有一种更好的方式,既能兼顾线程安全又能提升程序性能昵?答案是肯定的,我们来看双重检查锁(DCL)的单例模式:

public class LazyDoubleCheckSingleton {

    private LazyDoubleCheckSingleton() {

    }

    //使用volatile禁止指令重排序
    private static volatile LazyDoubleCheckSingleton instance;

    public static LazyDoubleCheckSingleton getInstance() {
        if (instance == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (instance == null) {
                    instance = new LazyDoubleCheckSingleton();
                    /**
                     * 1。分配内存给这个对象
                     * 2。初始化对象
                     * 3。设置lazy指向刚分配的内存地址
                     * 4。初始化访问对象
                     */
                }
            }
        }
        return instance;
    }
}

当第一个线程调用getInstance()方法时,第二个线程也可以调用。当第一个线程执行到synchronized时会上锁,第二个线程就会出现阻塞。此时阻塞是在getInstance()方法内部的阻塞,只要逻辑不太复杂,对于调用者而言感知不到。

DCL单例模式在好多框架源码里都被使用过,看过源码的同学知道哈!!!

但是,用到synchronized关键字总归要上锁,对程序性能还是存在一定影响的。难道就真的没有更好的方案妈?当然后,我们可以从类初始化的角度来考虑,采用静态内部类的方式。

四、静态内部类单例模式

/**
 * 这种形式兼顾饿汉式单例模式的内存浪费问题和懒汉式synchronized加锁的性能问题,完美的屏蔽了这两个缺点
 */
public class LazyInnerClassSingleton {

    /**
     * 使用LazyInnerClassSingleton的时候,默认会初始化内部类LazyHolder,如果没使用,则内部类是不加载的
     */
    private LazyInnerClassSingleton() {
    }

    /**
     * 每一个关键字都不是多余的,static是为了使单例的空间共享,final保证这个方法不会被覆盖,重载
     */
    public static final LazyInnerClassSingleton getInstance() {
        return LazyHolder.LAZY_INNER_CLASS_SINGLETON;
    }

    //默认不加载
    private static class LazyHolder {
        private static final LazyInnerClassSingleton LAZY_INNER_CLASS_SINGLETON = new LazyInnerClassSingleton();
    }
}

这种方法兼顾了饿汉式单例模式的内存浪费问题和懒汉式单例synchronized加锁的性能问题。内部类在方法调用时才会初始化,巧妙了避免了线程安全问题。

五、破坏单例行为

1. 反射破坏单例

静态内部类单例模式由于这种方式比较简单,但是,金无足赤,人无完人。这种写法真的完美了吗?

public class LazyInnerClassSingletonTest {

    @Test
    public void test() {
        try {
            //在很无聊的情况下,进行破坏
            Class clazz = LazyInnerClassSingleton.class;

            //通过反射获取私有构造方法
            Constructor constructor = clazz.getDeclaredConstructor(null);
            constructor.setAccessible(true);

            //暴力初始化
            //调用了两次构造方法,相当于new了两次,犯了原则性错误
            Object o1 = constructor.newInstance();
            Object o2 = constructor.newInstance();

            System.out.println(o1 == o2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

答案:false

显然,创建了两个不同的实例,那怎么办昵?我们来做一次优化,现在我们在其构造方法中做一些限制,一旦出现多次重复创建,则直接抛出异常,来看优化后的代码:

private LazyInnerClassSingleton() {
    if (LazyHolder.LAZY_INNER_CLASS_SINGLETON != null) {
        throw new RuntimeException("不允许创建多个实例~~~");
    }
}

image.png

至此,自认为史上最牛的单例模式的实现方式便大功告成。

2. 序列化破坏单例

静态内部类单例模式构造函数限制虽然能限制住反射,但又被序列化给破了!!!

AF963E9F.jpg

一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。如果序列化的目标对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例,来看一段代码:

@Test
public void test2() {
    try {
        LazyInnerClassSingleton s1 = LazyInnerClassSingleton.getInstance();
        FileOutputStream fos = new FileOutputStream("serializableSingleton.obj");

        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(s1);
        oos.flush();
        oos.close();

        FileInputStream fis = new FileInputStream("serializableSingleton.obj");
        ObjectInputStream ois = new ObjectInputStream(fis);
        LazyInnerClassSingleton s2 = (LazyInnerClassSingleton) ois.readObject();
        ois.close();

        System.out.println("s1=" + s1);
        System.out.println("s2=" + s2);
        System.out.println(s1 == s2);

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

image.png

从运行结果可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例模式的设计初衷。那如何保证在序列化情况下也能够实现单例模式昵?其实很简单,只需要增加readResole()方法即可。来看优化后的代码:

public class LazyInnerClassSingleton implements Serializable {

    private static final long serialVersionUID = -8484501356898167924L;

    private LazyInnerClassSingleton() {
        if (LazyHolder.LAZY_INNER_CLASS_SINGLETON != null) {
            throw new RuntimeException("不允许创建多个实例~~~");
        }
    }

    public static final LazyInnerClassSingleton getInstance() {
        return LazyHolder.LAZY_INNER_CLASS_SINGLETON;
    }

    private static class LazyHolder {
        private static final LazyInnerClassSingleton LAZY_INNER_CLASS_SINGLETON = new LazyInnerClassSingleton();
    }

    // 重点代码!!!
    private Object readResolve() {
        return LazyHolder.LAZY_INNER_CLASS_SINGLETON;
    }
}

image.png

总算解决了,为了单例不被破坏,真是煞费苦心啊!!!

AF9DFCFE.gif

想知道具体原因,大家可以阅读ObjectInputStream的readObject()方法。

六、枚举式单例模式

public enum EnumSingleton {

    INSTANCE;

    public static EnumSingleton getInstance() {
        return INSTANCE;
    }
}

其实枚举式单例,虽然写法优雅,但是也会有一些问题。因为它在类加载之时就将所有的对象初始化放在类内存中,这其实和饿汉式并无差异,不适合大量创建单例对象的场景。

七、总结

单例模式可以保证内存里只有一个实例,减少了内存的开销,可以避免对资源的重复占用。

欢迎大家关注微信公众号(MarkZoe)互相学习、互相交流。