java并发中的延迟初始化

在《java并发编程实战》这本书的第十六章中讲到不安全的发布时,给了一个不安全的延迟初始化示例:

 

public class UnsafeLazyInitialization {
    private static Resource resource;

    public static Resource getInstance() {
        if (resource == null)
            resource = new Resource(); // unsafe publication
        return resource;
    }

    static class Resource {
    }
}

  一般情况下,估计大家都会这么写代码,在非并发环境中,这个getInstance方法会工作的很好,但是放到并发环境中,问题就来了,比如竞态条件,但这里还存在另外一个问题,即另一个线程可能会看到对部分构造的Resource实例的引用。

简单点来说,就是线程A在访问getInstance方法时,发现resource为null,于是就resource设置为一个新实例,在这个过程中,线程B也调用getInstance方法,发现resource不为空,因此就直接使用这个resource实例了。看起来貌似没有问题,但是因为线程A实例化resource的操作和线程B读取resource实例的操作之间不存在Happens-Before关系,所以,在线程B使用resource实例时,resource实例也许还未构造完成,这就导致了线程B看到的resource实例不正确的状态。

 

解决这个问题的一个简单办法就是使用同步,这也是我们经常会使用的办法:

 

public class SafeLazyInitialization {
    private static Resource resource;

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

    static class Resource {
    }
}

  这种办法够简单直接,但是在getInstance方法被频繁调用的时候,还是会存在激烈的竞争。书中还给出了另外一种办法,就是不采用延迟初始化,也就是提前初始化,在定义resource时候,就实例化它:

 

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

    public static Resource getResource() {
        return resource;
    }

    static class Resource {
    }
}

 

  这种办法在日常开发中也会采用到。但是这种办法为什么是线程安全的呢?这涉及到JVM在类的初始化阶段给出的线程安全性保证。因为JVM在类初始化阶段,会获取一个锁,并且每个线程都会至少获取一次这个锁以确保这个类已经加载,在静态初始化期间,内存的写入操作自动对所有线程可见,而resource的初始化就是属于静态初始化。因此,在构造期间或者被引用时,静态初始化的对象都不需要显式的同步,但是这个规则只适用于在构造时的状态,如果对象可变,那么在其它地方对该对象的访问还是需要使用同步来确保对对象的修改操作是可见的。

 

下面再来看看另一种解决办法,书中称之为延迟初始化占位类模式,我认为这个方法很巧妙:

 

 

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

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

    static class Resource {
    }
}

 

  这种方法就是基于上述JVM在类的初始化阶段给出的线程安全性保证,将resource的实例化操作放置到一个静态内部类中,在第一次调用getResource方法时,JVM才会去加载ResourceHelper类,同时初始化resource实例,因此,即使我们不采取任何同步策略,getResource方法也是线程安全的。

 

后面还有讲到基于双重检查锁(DCL)的方式来实现,但是这种方法属于糟糕的方法,这里就不过多描述了,示例代码如下:

 

public class DoubleCheckedLocking {
    private static Resource resource;

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

    static class Resource {

    }
}
 

 

你可能感兴趣的:(java)