代码之家  ›  专栏  ›  技术社区  ›  to StackOverflow

这条线安全吗?

  •  0
  • to StackOverflow  · 技术社区  · 15 年前

    据我所知,下面的代码是线程安全的,但需要注意的是,我不寻找单例模式(我不在乎多个线程是否获得不同的myObject实例,因为myObject是不可变的。在创建多个实例的并发线程之间的潜在争用之后,后续线程都将获得相同的维护)。

    private static MyObject myObject;
    ...
    public static MyObject GetMyObject()
    {
        if (myObject == null)
        {
            myObject = new MyObject(...);
        }
        return myObject;
    }
    

    以下显然不是线程安全的,因为多个线程可能会在完全初始化之前尝试访问myObject字段:

    private static MyObject myObject;
    ...
    public static MyObject GetMyObject()
    {
        if (myObject == null)
        {
            myObject = new MyObject();
            myObject.Initialize(...);
        }
        return myObject;
    }
    

    以下线程是安全的,还是myObject字段上需要volatile关键字?它 尽管如此,它是安全的,因为在对象完全初始化之前,不会分配myObject字段。但我担心,抖动可能能够内联loadmoObject方法,并实质上将其重新实现为上述代码,这不是线程安全的。

    private static MyObject myObject;
    ...
    private static MyObject LoadMyObject()
    {
        MyObject myObject = new MyObject();
        myObject.Initialize(...);
        return myObject;
    }
    
    public static MyObject GetMyObject()
    {
        if (myObject == null)
        {
            myObject = LoadMyObject();
        }
        return myObject;
    }
    

    以上的背景是,我想重构一些多线程代码,并通过调用工厂方法来替换对构造函数的调用。我想知道这样做是否能破坏线程安全。

    编辑

    只是重申一下,我不关心是否创建了MyObject的多个实例,只关心无法访问未完全初始化的实例。作为一个具体的例子,可以考虑使用配置文件的内容加载的只读XML文档。我希望静态引用内存中可用的XML文档,以避免每次访问时从磁盘加载的开销。但我不关心两个线程是否同时运行,并且都从磁盘加载文档。

    编辑2

    如果我理解正确,下面来自C语言规范的语句似乎意味着抖动不会对代码重新排序,因此我的最后一个示例应该是线程安全的。我理解正确吗?

    3.10执行令 … 数据依赖性保存在 执行线程。也就是说, 每个变量的值计算为 如果线程中的所有语句 按原程序顺序执行

    9 回复  |  直到 15 年前
        1
  •  1
  •   Guffa    15 年前

    从某种意义上说,您将得到一个正确初始化的对象是安全的,但是几个线程可能同时创建对象,而一个线程可能返回一个刚创建的不同线程的对象。

    如果编译器输入该方法,它仍然不会与第一个代码相同。由于对对象的引用保存在局部变量中,直到初始化,所以它仍然是安全的。

    您可以自己内联该方法,无需将代码放在单独的方法中以确保其安全:

    public static MyObject GetMyObject() {
       if (myObject == null) {
          MyObject newObject = new MyObject();
          newObject.Initialize(...);
          myObject = newObject;
       }
       return myObject;
    }
    

    如果要防止多个对象由单独的线程创建,则需要使用锁。

        2
  •  2
  •   Rob Parker    15 年前

    在某些情况下,volatile可能是一个性能选项,以避免使用锁的额外冲击,但它不解决争用条件的线程安全问题,这是您的第三个示例(有问题的例子)所具有的。

    为了确保另一个线程没有检索到未初始化的实例,在拥有完全初始化的实例之前,不应将其分配给静态变量。一旦他们看到一个非空值,他们就会知道它是好的。 编辑 :(更正)虽然我认为避免冗余工作更理想(见下文),但对于您的用例,允许重新生成和替换静态引用(如您所拥有的,或Guffa的版本)仍然是“安全的”,因为每个引用 GetMyObject() 将在此时将单个对象抓取到位。

    我建议这样做:

    private static object s_Lock = new object();
    private static volatile MyObject s_MyObject;
    private static MyObject LoadMyObject()
    {
        MyObject myObject = new MyObject();
        myObject.Initialize();
        return myObject;
    }
    public static MyObject GetMyObject()
    {
        if (s_MyObject == null)
        {
            lock (s_Lock)
            {
                if (s_MyObject == null) // Check again now that we're in the lock.
                    s_MyObject = LoadMyObject(); // Only one access does this work.
            }
        }
        return s_MyObject; // Once it's set, it's never cleared or assigned again.
    }
    

    这样做的好处是只做一次init工作,同时也避免了锁争用开销,除非实际上需要这样做…但仍然是安全的。(如果您想确定volatile是否传播出去,可以如上所述使用volatile,但是如果没有它,这也应该“有效”;在看到 s_MyObject ,取决于体系结构。因此,我认为volatile有助于这种方法。)

    我一直守护着你 LoadMyObject GetMyObject 方法,但也可以将逻辑合并到单个工厂方法的内部if的then子句中。

        3
  •  1
  •   cjk    15 年前

    要回答第二次编辑,如果您的初始化方法需要一段时间,则另一个线程可能会像k一样,如果对象不是空的,则拉入其未初始化状态。我建议您使用一个用于空检查和初始化的锁来确保只有在完全初始化时才检索对象。

    如果您希望它是完全线程安全的,这可以帮助:

    private static MyObject myObject;
    private object lockObj = new object();
    ...
    public static MyObject GetMyObject()
    {
        lock(lockObj)
        {
            if (myObject == null)
            {
                myObject = new MyObject(...);
            }
        }
        return myObject;
    }
    
        4
  •  1
  •   Pop Catalin    15 年前

    但我担心的是 可能能够将 加载对象方法 将其重新实现为上述代码, 这不是线程安全的。

    即使方法是内联的,本地也不会被优化掉,对字段的写入也只会发生一次。

    所以最后一个例子 线程安全 .

    通常,当变量有多个访问权限时,局部变量不会被优化并替换为全局变量。

        5
  •  0
  •   1800 INFORMATION    15 年前

    如果我能在clippy上发布一分钟的话:“看起来你正在尝试实现singleton模式。”请看 Jon Skeet's canonical article on the subject .

        6
  •  0
  •   Alex Reitbort    15 年前

    为什么不将initialize方法移动到myObject的tor中,并用一个调用初始化它?

        7
  •  0
  •   Matt    15 年前

    即使没有编译器优化,它仍然是不安全的。在这种情况下,volatile没有帮助——它只是设计用来保护变量不被修改为“在封面下”。假设两个线程在完全相同的时间通过了空检查——您最终会两次调用“LoadMyObject”,在这种情况下,这可能是正常的,也可能不是。这是一个toctou错误(检查时间/使用时间)。基本上,您必须使整个getMyObject方法的主体安全,包括检查(即使该调用同步)。

        8
  •  0
  •   TheVillageIdiot    15 年前

    这是我的看法:

    public class TestClass
    {
        private static TestClass instance = LoadObject();
    
        private TestClass(){ }
    
        private static TestClass LoadObject()
        {
            var t = new TestClass();
            //do init
    
            return t;
        }
    
        public static  TestClass GetObject()
        {
            return instance;
        }
    }
    
        9
  •  0
  •   Christian Hayter    15 年前

    在我看来,这似乎过于乐观了。与其深入了解编译器所做工作的细微之处,不如选择最适合您的方法:

    • 一个实例(单例)
    • 每个线程一个实例(线程静态)
    • 每个调用方一个实例(完全没有特殊代码)

    这完全取决于你想付多少罚金。对于一个单身汉,你要付表演罚金。对于其他两个选项,您需要支付内存罚金。

    要使其线程静态,只需执行以下操作:

    [ThreadStatic]
    private static MyObject myObject;
    ...
    public static MyObject GetMyObject()
    {
        if (myObject == null)
        {
            myObject = new MyObject(...);
        }
        return myObject;
    }