对饿汉和懒汉的提升——双重校验&Initialization-on-demand holder idiom(登记式/静态内部类)

说明

都知道饿汉有内存内存浪费的问题,而懒汉有线程安全问题。所以这两个平时都不敢用,但是它们的优化方式我经常说不明白。今天好好总结总结。

双重校验

是否 Lazy 初始化:是

是否多线程安全:是

描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

public class Singleton{
	private volatile static Singleton s;
	private Singleton(){}
	public static Singleton getSingleton(){
		if(s==null){
			synchronized(Singleton.class){
				if(s==null){
				s=new Singleton();
				}

			}
		}

		return s;
	}


}

性能优化:只在实例尚未被创建时同步,减少了每次调用getSingleton()方法时的同步开销。一旦实例创建,获取实例的操作就不再需要同步,这对于频繁调用单例实例的场景是一个重要的性能优化。

线程安全:通过双重校验锁的方式,可以确保在多线程环境中单例的唯一性和线程安全性。第一重校验确保只有首次访问单例时才进行同步,第二重校验则是为了确保在进入同步块后,如果有其他线程已经初始化了实例,就避免再次初始化。

volatile关键字的作用:在singleton变量上使用volatile关键字是关键,它确保实例的初始化完整性和可见性。没有volatile,可能出现部分初始化的对象被其他线程使用的情况,因为singleton= new Singleton();这个操作不是原子的,它包括了分配内存、初始化对象、将singleton变量指向分配的内存空间这几个步骤。使用volatile关键字可以防止指令重排,确保这些步骤的执行顺序。

Initialization-on-demand holder idiom(登记式/静态内部类)

是否 Lazy 初始化:是

是否多线程安全:是

实现难度:一般

该模式利用了Java语言规范中保证类的初始化阶段是线程安全的原理。在这种模式中,单例类本身并不直接实例化单例,而是在内部定义一个静态内部类,这个内部类包含有单例对象的实例。当外部类的静态方法(如getInstance())被调用时,内部类才会被加载和初始化,从而创建单例对象。

public class Singleton {

    // 私有构造函数,防止外部实例化
    private Singleton() {
    }

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

    // 提供给外部的获取实例的静态方法
    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

而它关键的一点是,类在初始化时是线程安全的。

类加载过程:Java类的加载分为加载(Loading)、链接(Linking)和初始化(Initialization)三个主要阶段。其中初始化阶段是关键,它发生在类首次被使用时。

初始化阶段的线程安全:在初始化阶段,Java虚拟机(JVM)负责处理静态变量的赋值和静态块的执行。JLS规定,这个阶段必须是线程安全的。这意味着如果多个线程同时尝试初始化一个类,JVM会确保该类在任何时候只被一个线程初始化。直到初始化完成,其他线程都会阻塞等待。

静态内部类的特性:静态内部类只有在被使用的时候才会被加载和初始化。这是因为类的初始化是触发在某个类首次被使用时,比如引用静态字段、调用静态方法或者创建类的实例。因此,静态内部类提供了一种延迟初始化对象的方法,同时保持了JVM在类初始化阶段的线程安全性。
如果使用static来修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象。因此使用static修饰的内部类被称为类内部类,有的地方也称为静态内部类。

static的扩展:
static关键字的作用是把类的成员变成类相关,而不是实例相关,即static修饰的成员属于整个类,而不属于单个对象。外部类的上一级程序单元是包,所以不可使用static修饰;而内部类的上一级程序单元是外部类,使用static修饰可以将内部类变成外部类相关,而不是外部类实例相关。因此static关键字不可修饰外部类,但可修饰内部类。

静态内部类需满足如下规则:

静态内部类可以包含静态成员,也可以包含非静态成员;

静态内部类不能访问外部类的实例成员,只能访问它的静态成员;

外部类的所有方法、初始化块都能访问其内部定义的静态内部类;

在外部类的外部,也可以实例化静态内部类,语法如下:

外部类.内部类 变量名 = new 外部类.内部类构造方法();

单例模式的应用:在单例模式中,这种特性被用于保证单例对象的唯一性和线程安全性。单例类的私有静态内部类持有单例对象的实例。该内部类不会在单例类被加载时立即初始化,而是在首次调用getInstance()方法时,触发内部类的加载和初始化,从而创建单例对象。由于类的初始化是线程安全的,这种方法自然地保证了单例实例的线程安全性,无需额外的同步机制。

在双重校验锁模式中,volatile关键字的两个主要作用

  1. 保证可见性:

    在多线程环境中,一个线程对volatile修饰的变量的修改,对其他线程是立即可见的。这意味着当一个线程修改了单例对象的引用,这个修改对于其他访问该对象的线程是可见的,从而确保了线程之间对单例实例的正确共享。
    防止指令重排序:

  2. 在Java内存模型中,编译器和处理器可能会对指令进行重排序,以提高程序的执行效率。但是,在某些情况下,这种重排序可能会导致程序逻辑上的错误。volatile修饰的变量可以禁止指令重排序。
    在双重校验锁模式下,禁止指令重排序是非常重要的。考虑单例对象的初始化过程,这个过程不是原子的,它可以分为几个步骤:分配内存空间、初始化对象、将对象的引用赋值给变量。如果没有volatile,就可能出现指令重排序,导致其他线程可能访问到一个未完全初始化的对象。
    例如,在双重校验锁的单例模式中,使用volatile可以避免这样的情况:一个线程A执行了单例实例的初始化,但实际上只是分配了内存空间并将地址赋给了引用变量,而对象的构造函数还没有被执行。此时,另一个线程B检查到单例引用不为空,直接返回了这个半初始化的对象,导致出现错误。

静态内部类不适用volatile

类的初始化锁定:当Java类进行初始化时,JVM会对类进行加锁,这个过程是线程安全的。当一个线程正在初始化一个类时,其他线程对该类的首次使用将会阻塞,直到活动线程完成初始化。

没有指令重排序的问题:在Initialization-on-demand
holder模式中,单例的实例是在静态内部类中静态成员的形式创建的。类的初始化阶段会执行静态变量的赋值和静态代码块,这在Java内存模型中是严格定义的,没有指令重排序发生在静态初始化阶段。

性能优化:由于JVM的类初始化机制,该模式本身就是线程安全的,所以没有必要引入volatile关键字。volatile主要是用于确保变量修改的可见性以及禁止指令重排序,但在这个模式中,这些特性是不需要的,因为JVM已经保证了初始化的正确性。

扩展static的使用

  1. 静态变量(Static Variables):

    静态变量是在类加载时初始化的,具体来说是在类被首次使用时,不是在类文件被加载时。类的使用包括创建类的实例、访问类的静态变量或方法。
    静态变量只初始化一次,即在类加载的时候,之后不会再次初始化。

  2. 静态方法(Static Methods):

    静态方法不依赖于类的实例,可以通过类名直接调用。 静态方法可以在类加载后的任何时间被调用。

  3. 静态代码块(Static Blocks):

    静态代码块也是在类加载的时候执行的,且只执行一次。 静态代码块通常用于初始化静态变量或执行仅需进行一次的静态初始化操作。
    如果一个类有多个静态代码块,它们将按照它们在类中出现的顺序被执行。

  4. 静态内部类(Static Inner Classes):

    静态内部类是与外部类关联的一种特殊的内部类,它不依赖于外部类的实例。 静态内部类是在首次使用时加载的,比如创建其实例、访问其静态成员。

你可能感兴趣的:(Java,单例模式,java,开发语言)