设计模式-单例模式(Singleton )(7种)

575 阅读8分钟

Github 源码地址

23种设计模式总览

创建型模式

结构型模式

行为型模式

一.模式介绍

模式的定义

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

模式的使用场景

确保某个类有且只有一个对象的场景,例如创建一个对象需要消耗的资源过多,如要访问 IO 和数据库等资源。

二.7种单例例模式实现

单例例模式的实现方式比较多,主要在实现上是否支持懒汉模式、是否线程安全中运用各项技巧。当然也有一些场景不需要考虑懒加载也就是懒汉模式的情况,会直接使用 static 静态类或属性和方法的方式进行处理,供外部调用。

1. 懒汉模式(线程不安全)

public class SingletonLanHan {

    private static SingletonLanHan singletonLanHan;

    private SingletonLanHan() {
    }

    public static SingletonLanHan getInstance() {
        if (singletonLanHan == null) { // 这里线程是不安全的,可能得到两个不同的实例
            singletonLanHan = new SingletonLanHan();
        }
        return singletonLanHan;
    }
}
  • 单例模式有⼀个特点就是不允许外部直接创建,也就是 new Singleton() ,因此这里在默认的构造函数上添加了私有属性private 。
  • 目前此种方式的单例确实满足了懒加载,但是如果有多个访问者同时去获取对象实例 你可以想象成⼀堆⼈在抢厕所 ,就会造成多个同样的实例并存,从而没有达到单例的要求。

2. 懒汉模式(线程安全 效率低)

public class SingletonLanHan {

    private static SingletonLanHan singletonLanHan;

    private SingletonLanHan() {
    }

    public static synchronized SingletonLanHan getInstance() {
        if (singletonLanHan == null) { 
            singletonLanHan = new SingletonLanHan();
        }
        return singletonLanHan;
    }
}

getInstance()方法增加了synchronized关键字,也就是getInstance()是一个同步方法

  • 缺点:效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。 而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低要改进。

3. 懒汉模式-双重校验锁DCL(线程安全 推荐使用)

public class SingletonLanHan {

    private static volatile SingletonLanHan singletonLanHan;

    private SingletonLanHan() {
    }

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

为什么是双重校验锁实现单例模式呢?

第一次校验:也就是第一个if(singleton==null),这个是为了代码提高代码执行效率,由于单例模式只要一次创建实例即可,所以当创建了一个实例之后,再次调用getInstance方法就不必要进入同步代码块,不用竞争锁。直接返回前面创建的实例即可。

第二次校验:也就是第二个if(singleton==null),这个校验是防止二次创建实例,假如有一种情况,当singleton还未被创建时,线程t1调用getInstance方法,由于第一次判断singleton==null,此时线程t1准备继续执行,但是由于资源被线程t2抢占了,此时t2页调用getInstance方法,同样的,由于singleton并没有实例化,t2同样可以通过第一个if,然后继续往下执行,同步代码块,第二个if也通过,然后t2线程创建了一个实例singleton。此时t2线程完成任务,资源又回到t1线程,t1此时也进入同步代码块,如果没有这个第二个if,那么,t1就也会创建一个singleton实例,那么,就会出现创建多个实例的情况,但是加上第二个if,就可以完全避免这个多线程导致多次创建实例的问题。

所以说:两次校验都必不可少

还有,这里的private static volatile Singleton singleton 中的volatile也必不可少

因为 singleton = new Singleton() 这句话可以分为三步:
1. 为 singleton 分配内存空间;
2. 初始化 singleton;
3. 将 singleton 指向分配的内存空间。

但是由于JVM具有指令重排的特性,执行顺序有可能变成 1-3-2。 指令重排在单线程下不会出现问题,但是在多线程下会导致一个线程获得一个未初始化的实例。例如:线程T1执行了1和3,此时T2调用 getInstance() 后发现 singleton 不为空,因此返回 singleton, 但是此时的 singleton 还没有被初始化。

使用 volatile 会禁止JVM指令重排,从而保证在多线程下也能正常执行。

4. 饿汉模式(线程安全)

public class SingletonEHan {

    private SingletonEHan() {}
  
    private static final SingletonEHan singletonEHan = new SingletonEHan();

    public static SingletonEHan getInstance() {
        return singletonEHan;
    }
}
  • 优点:从它的实现中我们可以看到,这种方式的实现比较简单,在类加载的时候就完成了实例化,避免了线程的同步问题。
  • 缺点:由于在类加载的时候就实例化了,所以没有达到Lazy Loading(懒加载)的效果,也就是说可能我没有用到这个实例,但是它也会加载,会造成内存的浪费(但是这个浪费可以忽略,所以这种方式也是推荐使用的)。

5. 内部类(线程安全)

public class SingletonIn {

    private SingletonIn() {
    }

    private static class SingletonInHodler {
        private static SingletonIn singletonIn = new SingletonIn();
    }

    public static SingletonIn getSingletonIn() {
        return SingletonInHodler.singletonIn;
    }
}

这种方式跟饿汉式方式采用的机制类似,但又有不同。 两者都是采用了类装载的机制来保证初始化实例时只有一个线程。

不同的地方:

  • 在饿汉式方式是只要Singleton类被装载就会实例化,
  • 内部类是在需要实例化时,调用getInstance方法,才会装载SingletonHolder类

优点:避免了线程不安全,延迟加载,效率高

6.CAS「AtomicReference」(线程安全)

在计算机科学中,比较和交换(Conmpare And Swap)是用于实现多线程同步的原子指令。它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成。

public class SingletonCAS {
    private static final AtomicReference<SingletonCAS> INSTANCE = new AtomicReference<SingletonCAS>();
    private static SingletonCAS instance;

    private SingletonCAS() {
    }

    public static final SingletonCAS getInstance() {
        for (; ; ) {
            SingletonCAS instance = INSTANCE.get();
            if (null != instance) return instance;
            INSTANCE.compareAndSet(null, new SingletonCAS());
            return INSTANCE.get();
        }
    }
}
  • java并发库提供了了很多原⼦子类来⽀支持并发访问的数据安全性; AtomicInteger 、 AtomicBoolean 、 AtomicLong 、AtomicReference 。
  • AtomicReference 可以封装引⽤用⼀一个V实例例,⽀支持并发访问如上的单例例⽅方式就是使⽤用了了这样的⼀一个特点。
  • 使⽤用CAS的好处就是不不需要使⽤用传统的加锁⽅方式保证线程安全,⽽而是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了了额外的开销,并且可以⽀支持较⼤大的并发性。
  • 当然CAS也有⼀一个缺点就是忙等,如果⼀一直没有获取到将会处于死循环中。

7.Effective Java作者推荐的枚举单例例(线程安全)

public enum SingletonEnum {

    instance;

    public void test() {

    }
}

使用极其简单

SingletonEnum.instance.test();

这种写法在功能上与共有域方法相近,但是它更简洁,无偿地提供了了串行化机制,绝对防止对此实例化,即使是在面对复杂的串行化或者反射攻击的时候。虽然这中方法还没有广泛采用,但是单元素的枚举类型已经成为实现 Singleton 的最佳方法。

但也要知道此种方式在存在继承场景下是不可用的。

三.总结

  • 虽然只是⼀一个很平常的单例模式,但在各种的实现上真的可以看到java的基本功的体现,这里包括了;懒汉、饿汉、线程是否安全、静态类、内部类、加锁、串行化等等。
  • 在平时的开发中如果可以确保此类是全局可用不需要做懒加载,那么直接创建并给外部调用即可。但如果是很多的类,有些需要在用户触发一定的条件后(游戏关卡)才显示,那么一定要用懒加载。线程的安全上可以按需选择。

代码地址: