Java 我应该同步一个静态可变变量吗?

Java 我应该同步一个静态可变变量吗?,java,thread-safety,volatile,Java,Thread Safety,Volatile,关于这个问题有几个问题,但大多数都回避这个问题,因为这不是问题的本意 如果我的类中有静态volatile: private static volatile MyObj obj = null; 我采用以下方法: public MyObj getMyObj() { if (obj == null) { obj = new MyObj();// costly initialisation } return obj; } 我是否需要同步以确保只有一个线程写入该

关于这个问题有几个问题,但大多数都回避这个问题,因为这不是问题的本意

如果我的类中有静态volatile:

private static volatile MyObj obj = null;
我采用以下方法:

public MyObj getMyObj() {
    if (obj == null) {
        obj = new MyObj();// costly initialisation
    }
    return obj;
}
我是否需要同步以确保只有一个线程写入该字段,或者任何写入都会立即对其他线程可见,以评估
obj==null
conditional


换言之:volatile是否让您不必对静态变量上的写操作进行同步访问?

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

考虑这样一系列事件:

  • 线程A进入
    getMyObj()
    并看到
    obj==null
  • 线程B进入
    getMyObj()
    并看到
    obj==null
  • 线程A构造了一个
    新的MyObj()
    -我们称它为
    objA
  • 线程B构造了一个
    新的MyObj()
    -我们称它为
    objB
  • 线程A将
    objA
    分配给
    obj
  • 线程B将
    objB
    分配给
    obj
    (此时不再是
    null
    ,因此线程A分配的对
    objA
    的引用被覆盖)
  • 线程A退出
    getMyObj()
    并开始使用
    objA
  • 线程B退出
    getMyObj()
    并开始使用
    objB
  • 这种情况可以发生在任意数量的线程上。请注意,尽管为了简单起见,我在这里假设事件的顺序是严格的,但在真实的多线程环境中,事件1-2、3-4和/或7-8可以在时间上部分或完全重叠,而不会改变最终结果

    holder成语的一个例子:

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

    这保证是安全的,因为
    实例
    最终的
    。Java内存模型保证在加载包含类时,
    final
    字段被初始化,并对任意数量的线程正确可见。由于
    LazyHolder
    private
    并且仅由
    getInstance()
    引用,因此只有在第一次调用
    getInstance()
    时才会加载它。此时,
    实例
    在后台初始化,并由JVM安全发布。

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

    基本问题是该块代码:

    if (obj == null) {
         obj = new MyObj();// costly initialisation
    }
    
    它不是原子的。事实上,它离原子化还有很长的路要走。

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

    就我个人而言,我会选择三种选择之一:

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

    • 使用无条件锁定:

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

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

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

  • obj初始为null
  • T1:if(obj==null):是
  • T2:if(obj==null):是
  • T1:创建MyObj的新实例并将其分配给obj
  • T2:还创建MyObj的新实例并将其分配给obj

  • 创建两次预期只创建一次的对象。这并不是最糟糕的情况。您可能会返回一个不再分配给变量的对象。

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


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

    像这样的双重检查锁定需要JDK5才能正常工作:抱歉,这里有正确的链接:@DavidHeffernan:感谢链接,我不知道“JDK5或更新版本”的要求。我将编辑答案以包含此信息。我可以将字段设置为静态,并使用静态初始值设定项或静态方法进行创建,允许同步和try/catch逻辑。这样,它的可读性更高+我不必锁定
    getMyObj()
    调用,因为静态初始值设定项将在类可用之前完成。@atc,您的描述不明确,因此没有代码示例,我无法评估它的线程安全性和可行性。请注意,holder习惯用法也没有使用锁(JVM在构建
    LazyHolder
    时会有一个锁,但是
    getInstance()
    本身是不同步的)。我不能走holder路线:它将单例强制到我的外部类上,这需要对我正在开发的整个框架进行更多的思考和更改。我需要解决的问题是静态实例的原子创建。我使用了一个静态方法,并将其声明为(
    obj
    )static final,以允许原子分配和并发安全。@atc:不,holder路由不强制外部类为单例。现在还不清楚您在这里的意思……为了清楚起见:这个
    静态volatile
    变量最初是在一个对象工厂中,在运行时将它注入到它负责创建的实例中。我是杜
    public static MyObj getMyObj() {
        return MyObjHolder.obj;
    }
    
    private static class MyObjHolder {
        static final MyObj obj = new MyObj();
    }
    
    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;
        }
    }