也说一下DCL问题

Double-checked Locking是一个经典的Java并发问题。根据网上看到的资料加上同别人的讨论,这里把我的理解尝试用一种更易懂的方法整理一下。

这个问题来自怎么线程安全的发布单例,先看下面这段code,它在需要的时候才会初始化一个单例资源:

class Foo {
    private Resource resource;
    public Resource getResource() {
        if (resource == null) {
            resource = new Resource();
        }
        return resource;
    }
}

在多线程环境下面显然是有问题的,多个线程可能会重复初始化Resource。解决方法很简单,加同步锁:

class Foo {
    private Resource resource;
    public synchronized Resource getResource() {
        if (resource == null) {
            resource = new Resource();
        }
        return resource;
    }
}

哈,问题解决了。但每次获取资源时都要synchronized一把,对性能上很有影响。于是有人发明了双重检查锁定,先检查下Resource是否为空,如果不是就不用进行synchronized了;在synchronized块里还要再检查一遍是否为空,因为在Resource初始化之前可能有两个线程同时检测到Resource为空并尝试进入同步块。

class Foo {
    private Resource resource;
    public Resource getResource() {
        if (resource == null) {
            synchronized(this) {
                if(resource == null) {
                    resource = new Resource();
                }
            }
        }
        return resource;
    }
}

这样就没问题了吗?重点来了,由于new Resource()不是一个原子操作,实际执行的时候由于指令优化可能是这样

  1. 初始化Resource对象;

  2. 把Resource对象地址赋给resource变量;

也可能是这样

  1. 初始化一半Resource对象;

  2. 把Resource对象地址赋给resource变量;

  3. 初始化剩下的Resource对象;

如果是第一种情况还好;如果是第二种情况,我们假设线程A刚走到第2步的时候就被操作系统挂起了,线程B执行了一下getResource方法,由于resource变量已经被赋了一个地址所以不为null,然后线程B就直接把这个地址傻乎乎的返回出去了。这样在线程B后面的代码里实际上使用的就是一个残缺的Resource对象。

问题清楚了,解决的思路其实很简单,确保resource的赋值操作在对象初始化后执行。这里可以用volatile和final两个关键字来解决。

volatile

class Foo {
    private volatile Resource resource;
    public Resource getResource() {
        if (resource == null) {
            synchronized(this) {
                if(resource == null) {
                    resource = new Resource();
                }
            }
        }
        return resource;
    }
}

由于有了volatile的存在,resource的赋值指令不会被优化到new Resource()中间去,这就保证了另外一个线程如果看到了resource被赋值的时候,其指向的对象一定是被初始化完成的。

这里有一个性能上可以做的优化是,先把resource赋给本地变量,然后操作和返回这个本地变量。这样就不用访问多次resource变量了,据说可以提高25%的性能。

final

public class FinalHolder<T> {
    public final T value;
    public FinalHolder(T value) {
        this.value = value;
    }
}
 
class Foo {
    private FinalHolder<Resource> resourceHolder;
 
    public Resource getResource() {
        FinalHolder<Resource> holder = resourceHolder;
        if (holder == null) {
            synchronized(this) {
                if(resourceHolder == null) {
                    resourceHolder = new FinalHolder<Resource>(new Resource());
                }
                holder = resourceHolder;
            }
        }
        return holder.value;
    }
}

对一个final的字段的初始化,JVM保证了从别的线程看起来,拿到对象引用后final字段一定被初始化好了。也就是resourceHolder的赋值一定是在new Resource()后面。请注意这里的本地变量不是多余的。如果没有的话,第二个resourceHolder == null可能会被代码优化干掉。

if (resourceHolder == null) {
    synchronized(this) {
        if(resourceHolder == null) {
            resourceHolder = new FinalHolder<Resource>(new Resource());
        }

static

对于DCL,还有一种利用static的更优雅的写法

class Foo {
    private static class ResourceHolder{
        private static final Resource resource = new Resource();
    }
 
    public static Resource getResource() {
        return ResourceHolder.resource;
    }
}

这段代码在初次执行到return ResourceHolder.resource这一行的时候,JVM才会对ResourceHolder类进行类加载和初始化。而这个过程由JVM保证是线程安全的。