1. 单例模式

a. 饿汉式

// final 不允许被继承
public final class Singleton {

    // 实例变量
    private byte[] data = new byte[1024];
    
    // 在定义实例对象的时候直接初始化
    private static final Singleton instance = new Singleton();

    // 私有构造函数,不允许外部new.
    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }

}

饿汉式的关键在于 instance 作为类变量并且直接得到了初始化. 如果主动使用了 Singleton 类,那么 instance 实例将会直接完成创建,包括其中的实例变量都会得到初始化,比如 1K 空间的 data 将会同时被创建.

instance 作为类变量在类初始化的过程中会被收集进 <clinit>() 方法中,该方法能够百分百地保证同步,也就是说 instance 在多线程的情况下不可能被实例化两次,但是 instance 在多线程的情况下不可能被实例化两次,但是 instance 被 ClassLoader 加载后可能很长一段时间才被使用,那就意味着 instance 实例所开辟的堆内存会驻留更久的时间.

如果一个类中的成员属性比较少,且占用的内存资源不多,饿汉的方式也未尝不可,相反,如果一个类中的成员都是比较重的资源,那么这种方式就会有写不妥.

总结起来,饿汉式的单例设计模式可以保证多个线程下的唯一实例, getInstance 方法性能也比较高,但是无法进行懒加载.

b. 懒汉式

所谓懒汉式就是在使用类实例的时候再去创建(用时创建),这样就可以避免类在初始化时提前创建.

// fianl 不允许被继承
public final class Singleton {
	
    // 实例变量
    private byte[] data = new byte[1024];
    
    // 定义实例,但是不直接初始化
    private static Singleton instance = null;

    private Singleton() {}

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

        return instance;
    }

}

Singleton 的类变量 instance=null ,因此 Singleton.class 被初始化的时候 instance 并不会被实例化,在 getInstance 方法中会判断 instance 实例是否被实例化,看起来没有什么问题,但是将 getInstance 方法中会判断 instance 实例是否被实例化,看起来没有什么问题,但是将 getInstance 方法放在多线程环境下进行分析,则会导致 instance 被实例化一次以上,并不能保证单例的唯一性.
在这里插入图片描述

如图所示,两个线程同时看到 instance==null ,那么 instance 将无法保证单例的唯一性.

c. 懒汉式 + 同步方法

懒汉式的方式可以保证实例的懒加载,但无法保证实例的唯一性. 在多线程的情况下, instance 又称为共享资源(数据),当多个线程对其访问使用时,需要保证数据的同步性.

// fianl 不允许被继承
public final class Singleton {
	
    // 实例变量
    private byte[] data = new byte[1024];
    
    // 定义实例,但是不直接初始化
    private static Singleton instance = null;

    private Singleton() {}

    // 向 getInstance 方法加入同步控制,每次只能有一个线程能够进入.
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }

        return instance;
    }

}

采用 懒汉式+数据同步 的方式既满足了懒加载又能够百分之百地保证 instance 实例的唯一性,但是 synchronized 关键字天生的排他性导致了 getInstance 方法只能在同一时刻被一个线程所访问,性能低下.

d. Double-Check

Double-Check 是一种比较聪明的设计方式,他提供了一种高效的数据同步策略,那就是首次初始化时加锁,之后则允许多个线程同时进行 getInstance 方法的调用来获得类的实例.

// final 不允许被继承
public final class Singleton {

    // 实例变量
    private byte[] data = new byte[1024];

    private static Singleton instance = null;
    
    Connection conn;
    Socket socket;

    private Singleton() {
        this.connection // 初始化 conn
        this.socket // 初始化socket
    }

    public static Singleton getInstance() {
        // 当 instance 为 null 时,进入同步代码块,同时该判断避免了每次需要进入同步代码块,可以提高效率
        if (instance == null) {
            // 只有一个线程能够获得 Singleton.class 关联的 monitor
            synchronized (Singleton.class) {
                // 判断如果 instance 为 null 则创建
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
    
}

当两个线程发现 instance==null 成立时,只有一个线程有资格进入同步代码块,完成对 instance 的实例化,随后的线程发现 instance==null 不成立则无须进行任何动作,以后对 getInstance 的访问就不需要数据同步的保护了.

这种方式看起来是那么的完美和巧妙,即满足了懒加载,又保证了 instance 实例的唯一性, Double-Check 的方式提供了高效的数据同步策略,可以允许多个线程同时对 getInstance 进行访问,但是这种方式在多线程的情况下有可能会引起空指针异常,下面我们来分析一下引发异常的原因.

在 Singleton 的构造函数,需要分别实例化 conn 和 socket 两个资源,还有 Singleton 自身,根据 JVM 运行时指令重排序和 Happens-Before 规则,这三者之间的实例化顺序并无前后关系的约束,那么极可能是 instance 最先被实例化,而 conn 和 socket 并未完成实例化,未完成初始化的实例调用其方法将会抛出空指针异常.
在这里插入图片描述

e. Volatile + Double-Check

Double-Check 虽然是一种巧妙的程序设计,但是有可能会引起类成员变量的实例化 conn 和 socket 发生在 instance 实例化之后,这一切均是由于 JVM 在运行时指令重排序所导致的,而 volatile 关键字则可以防止这种重排序的发生,因此代码稍作修改即可满足多线程下的单例、懒加载以及获取实例的高效性,代码修改如下:

private volatile static Singleton instance = null;

f. Holder 方式

Holder 的方式完全是借助了类加载的特点,下面我们对整个单例模式进行重构,然后结合类加载器的知识点分析这样做的好处在哪里.

// final 不允许被继承
public final class Singleton {

    // 实例变量
    private byte[] data = new byte[1024];

    private Singleton() {}
    
    // 在静态内部类中持有 Singleton 的实例,并且可被直接初始化
    private static class Holder {
        private  static Singleton instance = new Singleton();
    }

    // 调用 getInstance 方法,事实上是获得 Holder 的 instance 静态属性
    public static Singleton getInstance() {
        return Holder.instance;
    }

}

在 Singleton 类中并没有 instance 的静态成员,而是将其放到了静态内部类 Holder 之中,因此在 Singleton 的静态变量,并且直接进行了实例化,当 Holder 被主动引用的时候则会创建 Singleton 的实例, Singleton 实例的创建过程在 Java 程序编译时期收集至 <clinit>() 方法中,该方法又是同步方法,同步方法可以保证内存的可见性、JVM 指令的顺序性和原子性、Holder 方式的单例设计是最好的设计之一,也是目前使用比较广的设计之一.

g. 枚举方式

枚举的方式实现单例模式是《Effective Java》作者力推的方式,在很多优秀的开源代码中经常可以看到使用枚举方式实现单例模式的(身影),枚举类型不允许被继承,同样是线程安全的且只能被实例化一次,但是枚举类型不能够懒加载,对 Singleton 主动使用,比如调用其中的静态方法则 INSTANCE 会立即得到实例化.

// 枚举类型本身就是final的,不允许被继承
public enum Singleton {

    INSTANCE;

    // 实例变量
    private byte[] data = new byte[1024];

    Singleton() {
        System.out.println("INSTANCE 将立即初始化");
    }

    public static void method() {
        // 调用该方法则会主动使用 Singleton , INSTANCE 将会被实例化
    }

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

但是也可以对其进行改造,增加懒加载的特性,类似于 Holder 的方式.

public class Singleton {

    // 实例变量
    private byte[] data = new byte[1024];

    private Singleton() {}

    // 使用枚举充当 holder
    private enum EnumHolder {
        INSTANCE;

        private Singleton instance;

        EnumHolder() {
            this.instance = new Singleton();
        }

        private Singleton getSingleton() {
            return instance;
        }
    }

    public static Singleton getInstance() {
        return EnumHolder.INSTANCE.getSingleton();
    }

}

参考

  • 《Java高并发编程详解》
发布了14 篇原创文章 · 获赞 6 · 访问量 273

猜你喜欢

转载自blog.csdn.net/qq_42493970/article/details/102864787