【问题标题】:Is this class Threadsafe?这个类是线程安全的吗?
【发布时间】:2011-08-03 20:24:31
【问题描述】:


下面的类是线程安全的吗?

class ImmutablePossiblyThreadsafeClass<K, V> {

    private final Map<K, V> map;

    public ImmutablePossiblyThreadsafeClass(final Map<K, V> map) {
        this.map = new HashMap<K, V>();

        for (Entry<K, V> entry : map.entrySet()) {
            this.map.put(entry.getKey(), entry.getValue());
        }
    }

    public V get(K key) {
        return this.map.get(key);
    }
}

【问题讨论】:

标签: java concurrency thread-safety immutability


【解决方案1】:

在声明字段时应使用ConcurrentHashMap 而不是HashMapConcurrentMap

【讨论】:

    【解决方案2】:

    是的,它是线程安全的。它是不可变的,因此没有线程可以进行任何会干扰另一个线程的结果的更改。

    注意:如果在构建过程中修改了map,你不会得到你以为通过的内容,但是之后访问map的时候,会是线程安全的。

    【讨论】:

    • 怎么样:V v = objOfTheClass.get(key); v.changesomething();... 那还是不可变的吗?
    • @fluring - 和?如果V 本身不是线程安全的——那就是另一回事了。
    • 它在构造过程中发生了变异,线程可以对映射参数进行更改,这将干扰构造线程的结果。不,它不是线程安全的。
    • @Ishtar - 这很明显,但没有办法阻止......事实上,这是调用者的责任,传递一个不可修改的地图
    【解决方案3】:

    如果这是整个类定义,即没有其他 setter/modifier 方法,则 map 本身的使用是线程安全的:

    • 通过声明 final 来保护其初始化和可见性,
    • 包含的类只有一个 getter,没有修饰方法,
    • 映射的内容是从构造函数参数映射复制的,因此不能有任何外部引用可以修改。

    (请注意,这仅指地图的结构 - 地图中的各个元素可能仍然不安全。)

    更新

    关于类在构造过程中没有被同时修改的构造函数参数的说法,我倾向于同意其他人(包括@Bozho)已经指出的观点

    1. 没有已知的方法来防止这种情况(甚至没有像@Grundlefleck 建议的那样使用ConcurrentHashMap - 我检查了源代码,它的构造函数只调用了putAll(),这也没有防范),李>
    2. 确保构造函数参数不被同时修改是调用者的责任,而不是被调用者的责任。事实上,任何人都可以定义处理同时修改的构造函数参数的“正确”行为吗?创建的对象应该包含参数映射中的原始元素还是最新元素?...如果开始考虑这一点,恕我直言,唯一一致的解决方案是禁止同时修改构造函数参数,而这只能由调用者来确保。

    【讨论】:

    • 复制过程中,另一个线程可以修改参数。 entrySetIf the map is modified while an iteration over the set is in progress, the results of the iteration are undefined. 使用是线程安全的,但构造不是。
    • @Péter Török - 没有什么可以阻止它,并不能保证它的安全。正如 Grundlefleck 解释的那样,它几乎是线程安全的。
    • @Péter Török 该类不是完全线程安全的,因为该类不是最终的。
    • @Grundlefleck,我猜他的意思是可以添加例如子类中的非同步设置方法,从而破坏了封装和线程安全。
    • @Grundlefleck - 我之所以认为类必须是 final 才能保证线程安全,主要是基于类的名称。 ImmutablePossiblyThreadsafeClass 意味着类本身需要是不可变的。正如我们所知,如果一个类能够被扩展,它就不能是不可变的,在我看来,这破坏了 OP 所追求的线程安全。
    【解决方案4】:

    类本身线程安全的。

    不是类包含的地图内容。客户端获取后可以更改条目。是否使整个类线程安全取决于这个类和V之间的逻辑关系。

    【讨论】:

      【解决方案5】:

      假设在构造之后没有任何改变 map 字段(因为你已经将它命名为“不可变”,我猜这就是想法)并且作为构造函数参数提供的 map 在构造期间没有改变,这是一个线程安全的类。

      请注意,将条目从一个映射复制到另一个映射比迭代更简单。您可能还想查看Collections 中的一些方法来创建不可变版本的集合。当然,您首先必须复制地图,因为基础地图仍然可以更改。 “不可修改*”方法仅返回集合的视图

      【讨论】:

        【解决方案6】:

        当您复制构造函数参数的内容时,它们可能会发生变化。但实际上你无法采取任何措施来防止这种情况发生。另一个潜在问题是可变键。

        另外,你可以用这个替换构造函数:

        public ImmutablePossiblyThreadsafeClass(final Map<K, V> map) {
            this.map = new HashMap<K, V>(map);
        }
        

        【讨论】:

        • 好点!但是,这不会导致ConcurrentModificationException 阻止对象的构造吗?
        • @Péter:经常但并非总是如此。来自 API 文档:“请注意,不能保证快速失败的行为,因为一般来说,在存在不同步的并发修改的情况下,不可能做出任何硬保证。”
        【解决方案7】:

        我认为它是线程安全的。您已经(有效地)创建了输入 map 的副本,并且只对其进行了只读操作。我一直坚信不可变类本质上是线程安全的。但请注意,除非您的 KV 类是不可变的,否则您仍然可能遇到线程安全问题。

        您也可以将其替换为:

        Map<K, V> m = Collections.unmodifiableMap(Collections.synchronizedMap(map));
        

        【讨论】:

          【解决方案8】:

          是和否,对于“线程安全”的不同值(对于迟钝的答案感到抱歉)。构造不是线程安全的,但是一旦构造函数完成,并且假设发生正确,它将是不可变的和线程安全的。

          在构造过程中,map 参数可能会在调用map.entrySet 和调用entry.getKeyentry.getValue 之间被另一个线程修改。来自Map.entrySet 的javadoc。这可能会导致未定义的行为。

          来自java.util.Map javadoc for entrySet

          返回映射的集合视图 包含在这张地图中。中的每个元素 返回的集合是一个 Map.Entry。这 set 由地图支持,因此更改 映射到集合中, 反之亦然。如果地图被修改 而对集合的迭代在 进度,迭代的结果 是未定义的。该套装支持 元素移除,即移除 地图中的对应映射, 通过 Iterator.remove、Set.remove、 removeAll、retainAll 和清除 操作。它不支持 add 或 addAll 操作。

          因此,根据谁对地图的引用,以及他们如何跨线程操作它,以及复制元素需要多长时间,您的行为未定义,并且可能在程序的不同执行中出现不同的行为。

          对于确保它在这个类中正确构造,您似乎无能为力(例如,ConcurrentHashMap 有类似的问题)。如果您的其余代码可以确保为构造函数提供一个线程安全的映射,那么它也是线程安全的,但这比您要求的范围更广。

          然而,一旦构造函数完成并安全发布[1],它将是不可变的和线程安全的 - 尽管只是在浅层的方式 - 如果键或条目是可变的,这污点 你的类,使其既不可变也不线程安全。所以,如果你的键是String,你的值是Integer,那么它将是不可变的,因此是线程安全的。但是,如果您的键是带有 setter 的类 bean 对象,而您的值是其他可变集合类型,那么您的类不是不可变的。我倾向于将其标记为“一路乌龟”。

          基本上,在您的问题未涵盖的某些超出范围的条件下,您的类可能是不可变且线程安全的。

          [1] 您的类目前将被安全发布,但如果,例如,它变成一个嵌套类,或者您在构造函数期间将 this 引用传递给另一个方法,这可能成为不安全发布。

          【讨论】:

          • 制作浅拷贝如何消除并发修改的风险?在浅拷贝过程中仍然可以修改源映射...
          • @Peter 我刚刚意识到疏忽,希望现在得到纠正。碰巧知道 ConcurrentHashMap 的信息是否正确?
          • +1 没有“线程安全”。 “你可能有未定义的行为”意味着无法定义行为,因此它是未定义的。 “可以”可以去掉。 :)
          • ConcurrentHashMap的构造函数也有同样的问题。
          • 强烈同意你的迟钝——“线程安全”是一个模棱两可的术语,即使没有模棱两可,也很难理解;重要的是要意识到它不是二进制标志。
          猜你喜欢
          • 1970-01-01
          • 2012-02-17
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多