从不可变对象设置副本是不是可以避免线程损坏? [关闭]

Posted

技术标签:

【中文标题】从不可变对象设置副本是不是可以避免线程损坏? [关闭]【英文标题】:Is setting of copies from immutables safe from thread corruption? [closed]从不可变对象设置副本是否可以避免线程损坏? [关闭] 【发布时间】:2014-01-10 13:32:32 【问题描述】:

这是一个类字段,

class someClass 
  Int someClassField = nil
  ...

(请,请(!)忽略可见性问题,这个问题与整体设计有关,而不是语言实现)如果我在网上看教程,我被告知这个字段可以安全地被多个线程使用。当教程说安全时,它们并不意味着一个线程不能干扰另一个线程可见的值。这种干扰可能是本意——场可能是反击。教程的意思是,当一个线程更改此字段时,该字段不会处于不安全状态。拿下这个领域,

class someClass 
  List<List> someClassField = new List<Int>()
  ...

据我了解,如果该字段是一个简单列表,则一个线程可能会使其处于不一致状态(即部分断开连接)。如果另一个线程使用该列表,它将失败——在像 C 这样的语言中,这将是一场灾难。连阅读都可能失败。

那么,可以要求在该领域使用的类复制它的状态(复制可以扩展到对不变性的全面辩护,但我保持讨论简单)。如果该类复制了它的状态,那么修改将在字段上的副本之外完成,在修改后的新副本中进行返回。这个新的、修改后的副本可以重新分配给该字段。但是这个赋值是线程安全的——从某种意义上说,字段的值不能处于不一致的状态——因为新对象对字段的引用分配是原子的?

我忽略了语言引擎是否可能重新排序、缓存等所有问题。请参阅下面的许多帖子(尤其是 Java,似乎),

c# question有提示 Rule of thumb answers 在 Scala 中,但似乎将线性同步与彻底的灾难混淆了? Dark information 关于 Java 的线程可见性问题。一篇文章表明,是的,参考写作是原子的 Java question 与此相关。可见性和未成形对象之间存在更多相同的 Java 混淆 immutable-objects-are-thread-safe-but-whyJava 问题。听起来是个正确的问题,但是什么样的线程安全? .net question 偏离轨道

我想在较小的范围内解决这个问题...

【问题讨论】:

如果不参考特定语言,我认为这个问题没有多大意义。 似乎达成了一致——从概念上讲,是的,但语言实现会存在易变性问题或无法保证原子性。感谢大家尝试对“未完成”的事情进行推理。 这个问题已被标记为“太宽泛”。我不知道这个标记。 我标记了几种语言 - 这就是原因,它不具体吗?我是否询问过设计模式的细节,例如访客,(也是跨语言的),这个问题会被标记吗?我对正在考虑的行动非常具体。我想我没能说“忽略可见性问题”。嗯嗯,收集到的信息不错,相信这里有一个重点。 任何对这个问题(或它的背景)感兴趣的人都可能想看看, 【参考方案1】:

在大多数语言中,对象分配是原子的。

在这种特定情况下,您需要小心,尽管在执行 x=new X() 时,不能保证在所有语言中 X 在分配之前完全初始化。我不确定 C# 的立场。

您还必须考虑可见性和原子性。例如,在 Java 中,您需要将变量设置为 volatile,否则在一个线程中所做的更改可能在另一个线程中根本不可见。

【讨论】:

【参考方案2】:

C++ 将数据竞争定义为两个或多个线程可能同时访问同一内存位置,其中至少一个是修改。具有数据竞争的程序的行为是未定义的。所以不,如果至少有一个线程可以修改它,那么多个线程访问该字段是不安全的。

【讨论】:

【参考方案3】:

在 Java 中编写引用是原子性的(仅当字段是 volatile 时才写入 long 或 double),但仅此一点对您没有任何帮助。

示例演示:

class Foo 
     int x;
     public Foo()  x = 5;

现在假设我们做了一个赋值,例如foo = new Foo()(没有 foo 的 final 或 volatile 修饰符!)。从低层次的角度来看,这意味着我们必须做到以下几点:

    分配内存 运行构造函数 为该字段分配内存地址。

但只要构造函数不读取我们分配给它的字段,编译器也可以执行以下操作:

    分配内存 为该字段分配内存地址。 运行构造函数

线程安全?当然不是(如果你不设置内存屏障,你永远不能保证真正看到更新)。当涉及 final 字段时,Java 提供了更多保证,因此创建一个新的不可变对象将是线程安全的(您永远不会看到 final 字段的未初始化值)。可变字段(我们在这里讨论的是赋值而不是对象中的字段)在 java 和 c# 中也避免了这个问题。不确定 C# 和 readonly。

【讨论】:

【参考方案4】:

在 Java 中,除了 64 位基本类型 longdouble 之外,对引用和原语的赋值都是原子的。对 Java longs 和 doubles 的赋值可以通过使用 volatile 修饰符声明它们来实现原子化。见:Are 64 bit assignments in Java atomic on a 32 bit machine?

之所以如此,是因为 Java VM 规范要求它才能使 VM 与 Java 兼容。

Scala 在标准 Java VM 之上运行,因此在分配方面也将提供与 Java 相同的保证,除非它们开始使用 JNI。

C/C++ 的一个问题(以及它的优势之一)是这两种语言都允许将数据结构非常精细地映射到内存地址。在这个级别上,对内存的写入是否是原子的在很大程度上取决于硬件平台。例如,CPU 通常无法自动读取,更不用说写入未正确对齐的变量。例如当 16 位变量未与偶数地址对齐时,或者当 32 位变量未与 4 的倍数地址对齐时,依此类推。当变量超出一个缓存行进入下一个缓存行,或者超出一个页面进入下一个时,情况会变得更糟。因此 C 不保证赋值是原子的。

【讨论】:

感谢您对 C 的注释。我希望可能有一些跨语言的贡献。

以上是关于从不可变对象设置副本是不是可以避免线程损坏? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

Exchange Server 2016管理系列课件53.DAG管理之设置滞后数据库副本

Java ThreadLocal 理解

Java中常用不可变类

在python中制作不可变对象的修改副本的最快方法

Java多线程编程之不可变对象模式

copy与mutableCopy