Java安全发布对象(单例)的几种方式

459 阅读3分钟

基础知识

类的初始化

JVM在类被加载后,并且被线程使用之前,会进行类的初始化。在初始化期间,JVM将会获取一个锁,以同步多个线程对类的初始化。

根据Java语言规范,在首次发生下列任意一种情况时,一个类或接口类型T将被立即初始化:

  1. T是一个类,而且一个T类型的实例被创建。
  2. T是一个类,且T中声明的一个静态方法被调用。
  3. T中声明的一个静态字段被赋值。
  4. T中声明的一个静态字段被使用,而且这个字段不是一个常量字段。
  5. T是一个顶级类(Top Level Class,见Java语言规范的§7.6),而且一个断言语句嵌套在T内部被执行。

volatile

volatile可以理解为是轻量级的synchronized。

保证共享变量的“可见性”,当一个线程修改了一个共享变量时,另一个线程能读到这个修改的值。

如果一个变量声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

提前初始化

提前初始化,是指在类初始化期间对资源进行初始化,即在线程调用之前已完成好初始化。

public class EagerInitialization {
	private static Resource resource = new Resource();

	public static Resource getResource() {
		return resource;
	}

	static class Resource {
	}
}

该方案的优点是不需要每次调用都进行同步,并且避免第1次调用时初始化的开销。

延迟初始化

有时候,我们需要推迟一些高开销的对象初始化操作,并且只有当使用这些对象时才进行初始化。

下面是常见的三种延迟初始化方案。

基于同步

public class SafeLazyInitialization {
    private static Resource resource;

    public synchronized static Resource getInstance() {
        if (resource == null)
            resource = new Resource();
        return resource;
    }

    static class Resource {
    }
}

该方案由于使用了synchronized,在多线程频繁调用的情况下,将会导致程序执行性能的下降。因此,该方案通常很少使用。

基于类初始化

利用类初始化的安全性,我们可以在无锁的情况下,实现资源的安全初始化。

public class ResourceFactory {
    private static class ResourceHolder {
        public static Resource resource = new Resource();
    }

    public static Resource getResource() {
        return ResourceHolder.resource;
    }

    static class Resource {
    }
}

其中,return ResourceHolder.resource将促使ResourceHolder进行初始化,从而初始化resource = new Resource()。

双重检测锁

public class SafeDoubleCheckedLocking {
    private volatile static Instance instance; // 这里必须使用volatile

    public static Instance getInstance() {
        if (instance == null) {
            synchronized (SafeDoubleCheckedLocking.class) {
                if (instance == null)
                    instance = new Instance(); // 这里可能会出现重排序
            }
        }
        return instance;
    }

    static class Instance {
    }
}

其中,instance = new Instance();可简单的分解为3步:

  1. 分配对象内存空间
  2. 初始化对象
  3. 设置instance指向对象的内存地址

特别需要注意的是,如果instance变量不声明为volatile,那么由于重排序,分解后的3步可能是:

  1. 分配对象内存空间
  2. 设置instance指向对象的内存地址
  3. 初始化对象

第2和第3步出现了重排序,使得其他线程可能在看到instance不为null的时候,事实上instance还没初始化完成。

因此,对于双重检测锁,需要通过声明volatile来防止初始化对象时的重排序。(这个解决方案需要JDK5或更高版本的支持,因为volatile的语义得到了增强)

对于延迟初始化,由于基于类初始化的方案高效且易于理解,通常来说更加推荐使用该方案。


题图:www.journaldev.com

参考:

《Java并发编程的艺术》

《Java并发编程实战》

关于我

公众号:二进制之路

教程:996geek.com

博客:binarylife.icu