我应该同步静态volatile变量吗?

时间:2012-05-22 07:17:24

标签: java thread-safety volatile

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

如果我班上有静态不稳定因素:

private static volatile MyObj obj = null;

在下面的方法中我做了:

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

我需要同步以确保只有一个线程写入字段,或者任何写入是否会立即显示给评估obj == null条件的其他线程?

换句话说:volatile是否会让你不得不同步访问静态变量上的写入?

5 个答案:

答案 0 :(得分:8)

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

我个人会选择以下三个选项之一:

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

    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();
    }
    

答案 1 :(得分:3)

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

考虑一系列这样的事件:

  1. 主题A输入getMyObj()并看到obj == null
  2. 主题B输入getMyObj()并看到obj == null
  3. 线程A构造new MyObj() - 让我们称之为objA
  4. 主题B构建new MyObj() - 让我们称之为objB
  5. 主题A将objA分配给obj
  6. 主题B将objB分配给obj(此时此时不再是null,因此会覆盖由线程A分配的objA的引用<) / LI>
  7. 主题A退出getMyObj()并开始使用objA
  8. 主题B退出getMyObj()并开始使用objB
  9. 这种情况可能发生在任意数量的线程中。请注意,尽管在这里,为了简单起见,我假设事件的严格排序,在真实的多线程环境中,事件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;
        }
    }
    

    这保证是安全的,因为INSTANCEfinal。 Java内存模型保证在加载包含类时,final字段被初始化并且可以正确地显示给任意数量的线程。由于LazyHolderprivate且仅由getInstance()引用,因此只有在首次调用getInstance()时才会加载它。此时,INSTANCE在后​​台初始化并由JVM安全发布。

答案 2 :(得分:1)

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

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

  1. obj最初为null。
  2. T1:if(obj == null):是
  3. T2:if(obj == null):是
  4. T1:创建MyObj的新实例并将其分配给obj
  5. T2:还创建一个MyObj的新实例并将其分配给obj
  6. 您创建两次您希望仅创建一次的对象。这不是更糟糕的情况。您最终可能会返回一个不再分配给您的变量的对象。

答案 3 :(得分:0)

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

根本问题是这个区块代码:

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

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

答案 4 :(得分:0)

另一种处理此问题的方法是仔细检查(注意:仅适用于Java 5或更高版本,有关详细信息,请参阅here):

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,另一个进入synchronized-block,并实例化该对象。另一个将看到实例不再为null,并且不会尝试实例化它。对getInstance的进一步调用将看到该实例不为null,并且根本不会尝试锁定。