详谈单例、饿汉、和懒汉模式

1、基本概念

单例模式属于建立型设计模式。设计模式

确保一个类只有一个实例,并提供该实例的全局访问点。安全

实现: 使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现bash

2、结构

类图:多线程



私有构造函数保证了不能经过构造函数来建立对象实例,只能经过公有静态函数返回惟一的私有静态变量并发

3、几类经典单例模式实现

一、懒汉式-线程不安全

下面的实现中,私有静态变量 uniqueInstance 被延迟实例化,这样作的好处是,若是没有用到该类,那么就不会实例化 uniqueInstance,从而节约资源。jvm

这个实如今多线程环境下是不安全的,若是多个线程可以同时进入 if (uniqueInstance == null) ,而且此时 uniqueInstance == null,那么会有多个线程执行 uniqueInstance = new Singleton(); 语句,这将致使实例化屡次 uniqueInstance函数

// 懒汉式: 线程不安全
// 有延迟加载: 不是在类加载的时候就建立了,而是在调用newStance()的时候才会建立
public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton(){

    }

    public static Singleton newInstance(){
        if(uniqueInstance == null)
            uniqueInstance = new Singleton();
        return uniqueInstance;
    }
}复制代码

二、懒汉式-线程安全-性能很差

为了解决上面的问题,咱们能够直接在newInstance()方法上面直接加上一把synchronized同步锁。那么在一个时间点只能有一个线程可以进入该方法,从而避免了实例化屡次 uniqueInstance性能

可是当一个线程进入该方法以后,其它试图进入该方法的线程都必须等待,即便 uniqueInstance已经被实例化了。这会让线程阻塞时间过长,所以该方法有性能问题,不推荐使用测试

public static synchronized Singleton newInstance(){//在上面的基础上加了synchronized
    if(uniqueInstance == null)
        uniqueInstance = new Singleton();
    return uniqueInstance;
}复制代码

三、饿汉式-线程安全-无延迟加载

饿汉式就是 : 采起直接实例化 uniqueInstance 的方式,这样就不会产生线程不安全问题。ui

这种方式比较经常使用,但容易产生垃圾对象(丢失了延迟实例化(lazy loading)带来的节约资源的好处)。

它基于 classloader机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然致使类装载的缘由有不少种,在单例模式中大多数都是调用 getInstance 方法, 可是也不能肯定有其余的方式(或者其余的静态方法)致使类装载,这时候初始化 instance 显然没有达到 lazyloading 的效果

public class Singleton {

    // 急切的建立了uniqueInstance, 因此叫饿汉式
    private static Singleton uniqueInstance = new Singleton();

    private Singleton(){
    }

    public static Singleton newInstance(){
        return uniqueInstance;
    }

    // 瞎写一个静态方法。这里想说的是,若是咱们只是要调用 Singleton.getStr(...),
    // 原本是不想要生成 Singleton 实例的,不过没办法,已经生成了
    public static String getStr(String str) {return "hello" + str;}
}复制代码

四、双重校验锁-线程安全

uniqueInstance 只须要被实例化一次,以后就能够直接使用了。加锁操做只须要对实例化那部分的代码进行,只有当uniqueInstance 没有被实例化时,才须要进行加锁。

双重校验锁先判断 uniqueInstance 是否已经被实例化,若是没有被实例化,那么才对实例化语句进行加锁

// 双重加锁
public class Singleton {

    // 和饿汉模式相比,这边不须要先实例化出来
    // 注意这里的 volatile,使用 volatile 能够禁止 JVM 的指令重排,保证在多线程环境下也能正常运行
    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton newInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                // 这一次判断也是必须的,否则会有并发问题
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}复制代码
注意,内层的第二次 if (uniqueInstance == null) {也是必须的,若是不加: 也就是只使用了一个 if 语句。在 uniqueInstance == null 的状况下,若是两个线程都执行了 if 语句,那么两个线程都会进入 if 语句块内。虽然在 if 语句块内有加锁操做,可是两个线程都会执行 uniqueInstance = new Singleton();这条语句,只是前后的问题,那么就会进行两次实例化。所以必须使用双重校验锁,也就是须要使用两个 if 语句。

volatile 关键字修饰也是颇有必要的, uniqueInstance = new Singleton(); 这段代码实际上是分为三步执行:

  • 1)、为 uniqueInstance 分配内存空间;
  • 2)、初始化 uniqueInstance
  • 3)、将 uniqueInstance 指向分配的内存地址;

可是因为 JVM 具备指令重排的特性,执行顺序有可能变成 1>3>2。指令重排在单线程环境下不会出现问题,可是在多线程环境下会致使一个线程得到尚未初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 newInstance() 后发现 uniqueInstance 不为空,所以返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

使用 volatile 能够禁止 JVM 的指令重排,保证在多线程环境下也能正常运行

五、静态内部类实现

Singleton 类加载时,静态内部类 Holder 没有被加载进内存。只有当调用 newInstance()方法从而触发 Holder.uniqueInstanceHolder 才会被加载,此时初始化 uniqueInstance实例,而且 JVM 能确保 uniqueInstance只被实例化一次。

这种方式不只具备延迟初始化的好处,并且由 JVM 提供了对线程安全的支持。

这种方式是 Singleton 类被装载了, uniqueInstance 不必定被初始化。由于 Holders 类没有被主动使用,只有经过显式调用 newInstance() 方法时,才会显式装载 Holder 类,从而实例化 uniqueInstance
public class Singleton {

    private Singleton() {
    }

    // 主要是使用了 嵌套类能够访问外部类的静态属性和静态方法 的特性
    // 不少人都会把这个嵌套类说成是静态内部类,严格地说,内部类和嵌套类是不同的,它们能访问的外部类权限也是不同的。
    private static class Holder {
        private static final Singleton uniqueInstance = new Singleton();
    }
    public static Singleton newInstance() {
        return Holder.uniqueInstance;
    }
}复制代码

六、枚举实现

这种实现方式尚未被普遍采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止屡次实例化。

该实如今屡次序列化再进行反序列化以后,不会获得多个实例。而其它实现须要使用 transient修饰全部字段,而且实现序列化和反序列化的方法。

枚举实现单例 (+测试):

public class Singleton {

    private Singleton() {

    }

    public static Singleton newInstance() {
        return Sing.INSTANCE.newInstance();
    }

    private enum Sing {

        INSTANCE;

        private Singleton singleton;

        //jvm guarantee only run once
        Sing() {
            singleton = new Singleton();
        }

        public Singleton newInstance() {
            return singleton;
        }
    }

    public static int clientTotal = 1000;

    public static int threadTotal = 200;

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();

        Semaphore semaphore = new Semaphore(threadTotal);
        CountDownLatch countDownLatch = new CountDownLatch(clientTotal);

        Set<Singleton>set = Collections.synchronizedSet(new HashSet<>());//注意set也要加锁

        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();

                    set.add(Singleton.newInstance());

                    semaphore.release();
                } catch (Exception e) {

                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();

        System.out.println(set.size());//1
    }
}复制代码

关于序列化和反序列化:

public enum Singleton {

    INSTANCE;

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}复制代码

测试:

public class Test {

    public static void main(String[] args){
        // 单例测试
        Singleton s1 = Singleton.INSTANCE;
        s1.setName("firstName");
        System.out.println("s1.getName(): " + s1.getName());

        Singleton s2 = Singleton.INSTANCE;
        s2.setName("secondName");

        //注意我这里输出s1 ,可是已经变成了 secondName
        System.out.println("s1.getName(): " + s1.getName());
        System.out.println("s2.getName(): " + s2.getName());

        System.out.println("-----------------");

        // 反射获取实例测试
        Singleton[] enumConstants = Singleton.class.getEnumConstants();
        for (Singleton enumConstant : enumConstants)
            System.out.println(enumConstant.getName());
    }
}复制代码

输出:

s1.getName(): firstName
s1.getName(): secondName
s2.getName(): secondName
-----------------
secondName复制代码
该实现能够防止反射攻击。在其它实现中,经过 setAccessible()(反射中的强制访问私有属性方法) 方法能够将私有构造函数的访问级别设置为 public,而后调用构造函数从而实例化对象,若是要防止这种攻击,须要在构造函数中添加防止屡次实例化的代码。该实现是由 JVM 保证只会实例化一次,所以不会出现上述的反射攻击。

4、总结

通常状况下,不建议使用懒汉方式,建议使用饿汉方式。

只有在要明确实现 lazy loading 效果时,才会使用静态内部类方式

若是涉及到反序列化建立对象时,能够尝试使用枚举方式。

若是有其余特殊的需求,能够考虑使用双检锁方式。

相关文章
相关标签/搜索