Java的安全初始化

不安全的发布

错误的延迟初始化将导致不正确的发布,如下面的程序所示。初看起来,在程序中存在的问题只有竞态条件问题。在某些特定条件下,例如当Resource的所有实例都相同时,你或许会忽略这些问题(以及在多次创建Resource实例时存在的低效率问题)。然而,即使不考虑这些问题,UnsafeLazyInitialization仍然是不安全的,因为另一个线程可能看到对部分构造的Resource实例的引用。

public classUnsafeLazyInitialization {
    private static Resource resource;
   
    public static ResourcegetInstance()
    {
        if(resource == null)
        {
            resource = new Resource();
        }
        return resource;
    }
}

假设线程A是第一个调用getInstance的线程,它将看到resource为null,并且初始化一个新的Resource,然后将resource设置为这个新实例。当线程B随后调用getInstance,它可能看到resource的值为非空,因此使用这个已经构造好的Resource。最初这看不出任何问题,但线程A写入resouce的操作与线程B读取resource的操作之间不存在Happens-Before关系。在发布对象时存在数据竞争问题,因此B并不一定能看到Resource的正确状态。

 

当新分配一个Resource时,Resource的构造函数将把新实例中的各个域由默认值修改为它们的初始值。由于在两个线程中都没有使用同步,因此线程B看到的线程A中的操作顺序,可能与线程A执行这些操作时的顺序并不相同。因此,即使线程A初始化Resource实例之后再将resource设置为指向它,线程B仍可能看到对resource的写入操作将在对Resource各个域的写入操作之前发生。因此,线程B就可能看到一个不部分构造的Resource实例。

 

注:除了不可变对象外,使用被另一个线程初始化的对象通常都是不安全的,除非对象的发布操作是在使用该对象的线程开始使用之前执行。

 

 

安全的发布

(1)      使用synchronized

public classSafeLazyInitialization {
    private static Resource resource;
   
    public synchronized static ResourcegetInstance()
    {
        if(resource == null)
        {
            resource = new Resource();
        }
        return resource;
    }
}

(2)      静态初始化器

在初始化器中采用了特殊的方式来处理静态域(或者在静态初始化代码块中初始化的值),并提供了额外的线程安全性保证。静态初始化器是由JVM在类的初始化阶段执行,即在类被加载后并且被线程使用之前。由于JVM将在初始化期间获得一个锁,并且每个线程都至少获取一次这个锁以确保这个类已经加载,因此在静态初始化期间,内存写入操作将自动对所有线程可见。因此无论是在被构造期间还是被引用时,静态初始化对象都不需要显示的同步。然而,这个规则仅适用于在构造时的状态,如果对象时可变的,那么在读线程和写线程之间,仍然需要通过同步来确保随后的修改操作是可见的,以及避免数据的破坏。

如下代码,通过使用提前初始化,避免了每次调用safeLazyInitialization中的getInstance时所产生的同步开销。通过将这项技术和JVM的延迟加载机制结合起来,可以形成一种延迟初始化技术,从而在常见的代码中不需要同步。      

public classEagerInitialization{
    private static Resource resource = new Resource();
   
    public synchronized static ResourcegetResource()
    {
        return resource;
    }
}

(3)      延迟初始化占位

使用一个专门的类来初始化Resource。JVM将推迟ResourceHolder的初始化操作,直到开始使用这个类时才初始化,并且由于通过一个静态初始化来初始化Resource,因此不需要额外的同步。当任何一个线程第一次调用getResouce时,都会使ResouceHolder被加载和被初始化,此时静态初始化器将执行Resource的初始化操作。

 

public classResourceFactory{
    private static class ResourceHolder{
        private static Resource resource = new Resource();
    }
    public synchronized static ResourcegetResource()
    {
        return ResourceHolder.resource;
    }
}


双重检查加锁

在任何一本介绍并发的书中都会讨论声名狼藉的双重检查加锁(DCL)。

DCL的真正问题在于:当在没有同步的情况下读取一个共享对象时,可能发生的最糟糕的事情只是看到一个失效值(在这种情况下是一个空值),此时DCL方法将通过在持有锁的情况下在此尝试来避免这种风险。然而,实际情况远比这种情况糟糕——线程可能看到引用的当前值,但对象的状态值却是失效的,这意味着线程可以看到对象处于无效或错误的状态。

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


在JVM的后续版本(Java5.0以及更高的版本)中,如果把resource声明为volatile类型,那么就能启用DCL。DCL方法已经被广泛地废弃了,延迟初始化占位类模式能带来同样的优势,并且更容易理解。

 

发布了8 篇原创文章 · 获赞 5 · 访问量 1万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览