Skip to content

Latest commit

 

History

History
306 lines (205 loc) · 10.4 KB

设计模式—单例模式.md

File metadata and controls

306 lines (205 loc) · 10.4 KB

单例模式

✏️ 一思尚存,此志不懈。——胡居仁

什么是单例模式

一个类只允许创建一个对象(或者实例),那么这个类就是一个单例类。这种模式也叫单例模式。

如何实现一个单例

实现一个单例,需要关注以下几个点:

  • 构造函数需要是private访问权限的,避免外部通过new创建实例。
  • 考虑对象创建的线程安全问题。
  • 考虑是否支持延迟加载。
  • 考虑getInstance()性能是否高(是否加锁)。

单例模式的应用场景

  • 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少GC
  • 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用
  • 频繁访问数据库或文件的对象

实现单例的模式

饿汉式

饿汉式的单例,是指在类加载的时候,instance静态实例就已经创建并初始化好了。所以instance实例创建的过程是线程安全的。

不过这种方式不支持延迟加载。

饿汉单例具体实现如下:

public class HungrySingleton {

    private static final HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {
    }

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

因为饿汉单例是在类加载时创建,因此系统中存在大量的饿汉式单例的话,就会造成内存浪费的问题。因此,使用饿汉模式的话,需要根据实际情况来考虑。比如存在以下的情况,可以采用饿汉模式:

  • 如果单例初始化耗时长,则可以将这部分耗时提前到类加载时消耗。
  • 如果实例占用资源过多,根据fail-fast原则,这些实例也要使用饿汉模式。

!> 饿汉单例,是线程安全的。

懒汉式

线程不安全型

懒汉式的单例是与饿汉式单例相对的,懒汉式单例支持延迟加载,具体实现如下:

public class LazySingleton {

    private static LazySingleton instance;

    private LazySingleton() {
    }

    private LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

但是这种实现方式是线程不安全的,我们可以编写一个多线程的程序对单例进行测试,如下:

@Slf4j
class LazySingletonTest {

    @Test
    void createLazySingletonTest() {

        Thread thread1 = new Thread(new CreateLazySingletonRunnable());
        Thread thread2 = new Thread(new CreateLazySingletonRunnable());

        thread1.start();
        thread2.start();
    }

    static class CreateLazySingletonRunnable implements Runnable {

        @Override
        public void run() {
            LazySingleton instance = LazySingleton.getInstance();
            log.info("current Thread Name:{} current singleton:{}", Thread.currentThread().getName(), instance);
        }
    }
}

输出结果如下:

current Thread Name:Thread-2 current singleton:com.lvan.design_mode.singleton.LazySingleton@41ce6b13
current Thread Name:Thread-1 current singleton:com.lvan.design_mode.singleton.LazySingleton@50aa1827

通过以上结果,已经验证了多线程环境会对以上单例进行破坏。

全局加锁型

为了解决线程不安全的问题,我们使用synchronized来给getInstance加锁,代码如下:

public class LazySingleton {

    private static LazySingleton instance;

    private LazySingleton() {
    }

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

再运行以上多线程获取单例对象,通过结果就可以发现,此时懒汉式的单例已经是线程安全的了。

但是,使用synchronized加锁,会导致使用者每次调用getInstance()方法时,会导致频繁获取锁、释放锁等处理,在并发量高的情况下,会存在性能地瓶颈。实际上,多线程竞争的情况,只会在单例初次加载的时候,也就是说,单例加载完成后,再调用getInstance()方法,就不需要synchronized来加持了。

双锁检查型

由于synchronized目前是加在方法的级别上的,我们可以将其限制在单例初次加载时,代码设计如下:

public class LazySingleton {

    private static volatile LazySingleton instance;

    private LazySingleton() {
    }

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

这种实现单例模式的,也叫做Double-check。

但是这种方式,也是存在指令重排序的问题的,主要是因为new LazySingleton()这句代码的执行,并非一个原子性操作,它其实包括了三个操作,分别是分配对象的内存空间、初始化对象、设置instance指向刚分配的内存地址,也正是因为指令重排序的原因,初始化对象与设置instance指向为对象分配的内存地址这两个操作并非具有顺序性的,也就是说instance指向内存地址可以在初始化对象前执行,那么在多线程的情况下,可能会导致某些线程访问到未初始化的对象。

因此,为了解决指令重排序的问题,我在instance变量的声明时,使用volatile来解决了这个问题。

静态内部类单例

双重检查锁单例写法虽然解决了线程安全问题和性能问题,但是只要用到LazySingleton关键字就是还是利用到锁,对性能还是存在一定影响的。

静态内部类实现单例是一种比较简单的实现方式,也能实现单例的懒加载,代码设计如下:

public class StaticInnerSingleton {

    private StaticInnerSingleton() {
    }

    private static class SingletonHolder {
        private static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();
    }

    public static StaticInnerSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

通过静态内部类实现单例,主要是利用Java语法的特性。当外部类StaticInnerSingleton被加载的时候,并不会创建SingletonHolder实例对象,只有调用getInstance()方法时,SingletonHolder才会被加载,这个时候才会创建instanceinstance的唯一性、创建过程的线程安全性都有JVM来保证。

所以,这种方式既保证了线程安全,又能做到延迟加载。

枚举实现单例

不管是饿汉、懒汉、还是静态内部类实现的单例,我们都可以通过反射来修改构造函数的可见性,以此来破坏单例对象。

难道就没有比较安全的方法来实现单例吗?那就是通过枚举的特性,来实现单例。代码设计如下:

public enum SingletonEnum {
    INSTANCE;

    @Getter
    private Object Object;

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

通过枚举来是实现单例,保证了实例创建的线程安全性和实例的唯一性。

容器式单例

Spring框架中管理的对象基本都是单例对象,这些单例对象存储在Map数据结构中,因此这些单例也称之为容器式单例,比较适用于大量创建单例对象的场景。

破坏单例

反射破坏单例

通过反射,我们可以修改构造函数的访问控制,再通过构造函数来创建对象,这样就可以破坏单例了,作案代码如下:

@Test
void reflectDestroySingleton() throws Exception {

    Class<LazySingleton> lazySingletonClass = LazySingleton.class;

    Constructor<LazySingleton> declaredConstructor = lazySingletonClass.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true);

    LazySingleton lazySingleton = declaredConstructor.newInstance();
    LazySingleton lazySingleton1 = declaredConstructor.newInstance();

    assertThat(lazySingleton).isEqualTo(lazySingleton1);
}

输出结果如下:

Expected :com.lvan.design_mode.singleton.LazySingleton@7c417213
Actual   :com.lvan.design_mode.singleton.LazySingleton@6107227e

通过以上结果,可以看出我们创建了两个不同的实例,也就破坏了单例。

反序列化破坏单例

将单例进行序列化保存,再进行反序列化读取这种情况一般很少出现,除非这个单例在整个应用集群下是单例的(默认的单例是在Jvm进程下保持单例)。

但是,反序列化也是可以破坏单例的。作案代码如下:

!> LazySingleton需要实现Serializable接口

@Test
void serializableDestroySingleton() throws Exception {

    LazySingleton origObj = LazySingleton.getInstance();

    ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("singleton.obj"));
    outputStream.writeObject(origObj);
    outputStream.flush();
    outputStream.close();

    ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("singleton.obj"));
    LazySingleton obj = (LazySingleton) inputStream.readObject();
    assertThat(obj).isEqualTo(origObj);
}

输出结果如下:

Expected :com.lvan.design_mode.singleton.LazySingleton@5cdd8682
Actual   :com.lvan.design_mode.singleton.LazySingleton@1a0dcaa

通过以上结果,可以反序列化得到的实例对象与原对象不相等。

但是,这种方式,我们是可以避免的,序列化提供了这样的一个切入点,可以帮助我们在反序列化时,也能够保护单例对象。代码如下:

private Object readResolve() {
    return this.instance;
}

扩展

单例模式的优点

  • 单例可以保证内存里只有一个实例,减少内存的开销
  • 单例可以避免对资源的多重使用
  • 单例模式可以设置全局访问点,可以优化和共享资源访问

单例模式的缺点

  • 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则
  • 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象
  • 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则