8

关于这个主题有几个问题,但大多数都绕开了这个问题,因为这不是问题的意图。

如果我的班级中有一个静态易失性:

private static volatile MyObj obj = null;

在下面的方法中我这样做:

public MyObj getMyObj() {
    if (obj == null) {
        obj = new MyObj();// costly initialisation
    }
    return obj;
}

我需要同步以确保只有一个线程写入该字段,还是任何写入对评估obj == null条件的其他线程立即可见?

换句话说: volatile 是否让您不必同步对静态变量的写入访问?

4

5 回答 5

8

您肯定需要某种锁定来确保只有一个线程写入该字段。无论波动如何,两个线程都可以“看到”obj为空,然后都开始使用您当前的代码进行初始化。

就个人而言,我会采取以下三种选择之一:

  • 在类加载时初始化(知道这将是懒惰的,但不像getMyObj第一次调用 wait until 那样懒惰):

    private static final MyObj obj = new MyObj();
    
  • 使用无条件锁定:

    private static MyObj obj;
    private static final Object objLock = new Object();
    
    public static MyObj getMyObj() {
        synchronized(objLock) {
            if (obj == null) {
                obj = new MyObj();
            }
            return obj;
        }
    }
    
  • 以这种方式使用嵌套类来实现懒惰:

    public static MyObj getMyObj() {
        return MyObjHolder.obj;
    }
    
    private static class MyObjHolder {
        static final MyObj obj = new MyObj();
    }
    
于 2012-05-22T07:22:30.260 回答
3

是的,您应该绝对同步(或使用更好的成语,如Singleton Holder 成语)。否则,您将面临多个线程多次初始化您的对象的风险(然后使用不同的实例)。

考虑这样的一系列事件:

  1. 线程 A 进入getMyObj()并看到obj == null
  2. 线程 B 进入getMyObj()并看到obj == null
  3. 线程 A 构造 a new MyObj()- 我们称之为objA
  4. 线程 B 构造一个new MyObj()- 我们称之为objB
  5. 线程 A 分配objAobj
  6. 线程 B 分配objBobjnull此时不再存在,因此由线程 A 分配的对 的引用objA被覆盖)
  7. 线程A退出getMyObj()并开始使用objA
  8. 线程 B 退出getMyObj()并开始使用objB

这种情况可能发生在任意数量的线程上。请注意,虽然在这里,为了简单起见,我假设了事件的严格排序,但在真实的多线程环境中,事件 1-2、3-4 和/或 7-8 可以在时间上部分或全部重叠,而不会改变结尾结果。

持有人成语的一个例子:

public class Something {
    private Something() {
    }

    private static class LazyHolder {
        public static final Something INSTANCE = new Something();
    }

    public static Something getInstance() {
        return LazyHolder.INSTANCE;
    }
}

这保证是安全的,INSTANCE就像final. Java 内存模型保证final在加载包含类时,字段被初始化并正确地对任意数量的线程可见。由于LazyHolderisprivate并且仅由 引用,因此仅在第一次调用getInstance()时才会加载。getInstance()并且此时,INSTANCE在后台初始化并由 JVM 安全发布。

于 2012-05-22T07:21:51.663 回答
1

不,您仍然需要同步访问。volatile允许其他线程看到一个线程对变量所做的更改。

想象以下执行流程(假设有两个线程 T1 和 T2):

  1. obj 最初为 null。
  2. T1:如果(obj == null):是
  3. T2:如果(obj == null):是
  4. T1:创建一个新的 MyObj 实例并将其分配给 obj
  5. T2:还创建一个新的 MyObj 实例并将其分配给 obj

您创建了两次您希望只创建一次的对象。这不是更糟糕的情况。您最终可能会返回一个不再分配给您的变量的对象。

于 2012-05-22T07:23:43.523 回答
0

该代码不是线程安全的。如果多个线程执行该函数,则可以创建多个 MyObj 实例。您需要某种形式的同步。

根本问题是这个块代码:

if (obj == null) {
     obj = new MyObj();// costly initialisation
}

不是原子的。事实上,它距离原子化还有很长的路要走。

于 2012-05-22T07:22:23.193 回答
0

处理此问题的另一种方法是双重检查(注意:仅适用于 Java 5 或更新版本,请参阅此处了解详细信息):

public class DoubleCheckingSingletonExample
{
    private static final Object lockObj = new Object();

    private static volatile DoubleCheckingSingletonExample instance;

    private DoubleCheckingSingletonExample()
    {
        //Initialization
    }

    public static DoubleCheckingSingletonExample getInstance()
    {
        if(instance == null)
        {
            synchronized(lockObj)
            {
                if(instance == null)
                {
                    instance = new DoubleCheckingSingletonExample();
                }
            }
        }

        return instance;
    }
}

当同时从两个线程调用 getInstance 时,两者都会首先将实例视为 null,另一个进入同步块并实例化对象。另一个将看到实例不再为空,并且不会尝试实例化它。对 getInstance 的进一步调用将看到该实例不为空,并且根本不会尝试锁定。

于 2012-05-22T07:29:47.450 回答