【问题标题】:Is the following thread-safe? 4 threads writing to same data structure以下线程安全吗? 4个线程写入相同的数据结构
【发布时间】:2014-08-15 16:46:20
【问题描述】:

假设我有 4 个线程,它们都循环遍历具有 100 个索引的数组,翻转每个索引中的信息位并写回该索引...

arr[];

Thread 1:

for (int i = 0; i< 100; i+=4) { flip bits of arr[i]}

Thread 2:

for (int j = 1; j< 100; j+=4) { flip bits of arr[j]}

Thread 3:

for (int k = 2; k< 100; k+=4) { flip bits of arr[k]}

Thread 4:

for (int l = 3; l< 100; l+=4) { flip bits of arr[l]}

我在并发方面完全是个菜鸟,所以我想知道这是否是一种好的做法,或者是否有其他方法可以做到这一点?

更新:为了清楚起见——如果“arr[i] 的翻转位”和“arr[j] 的翻转位”由于某种原因触及同一个对象/成员,那么“非线程安全”的答案是显而易见的(并且与数组或实际问题无关),因此假设这些操作不会直接或在某些更深层次的对象中接触不同 i 和 j 对的相同内存。

【问题讨论】:

  • 很难说如果你甚至没有告诉我们你正在学习哪种编程语言......
  • 如果它是 Java,你会遇到可见性问题
  • @Skorpius 我建议您进一步解释“翻转每个索引中的信息位并写回该索引”的含义

标签: java arrays multithreading thread-safety


【解决方案1】:

您没有提供足够的细节,因此无法给出明确的答案,所以我会做一些额外的假设。


线程安全

如果你有:

  1. 主线程:int[] arr = new int[100]; //populate array
  2. 为主线程中的每个 T1、T2、T3、T4 调用 t.start(),然后在这些线程中运行循环,确保每个线程在不同的索引子集上运行(原始代码中就是这种情况由于 for 循环中的第 4 步)
  3. 在主线程中为每个 T1、T2、T2、T4 调用 t.join()

那么您有以下保证:

  • 在第 2 步,每个 T1..T4 都将访问同一个正确构造的数组 - 这是因为启动线程会在数组初始化和循环之间创建发生前的关系
  • 每个线程对不同的索引进行操作,每个索引可以被视为一个单独的int,因此它们不会相互干扰
  • 此时(步骤 2 结束),您没有任何可见性保证:例如,仅保证 T1 所做的更改在 T1 中可见 - 特别是主线程仍然可以在其初始状态
  • 因此,您需要通过同步操作“发布”更改 - 一种方法是加入主线程中的线程

良好做法

这里有几个通用的cmets。

正如另一个答案中所指出的,如果性能是一个问题,您可能应该尝试以对缓存更友好的方式编写代码。

如果每个线程所做的只是对 25 个整数进行一些简单的位操作,那么启动一个线程可能会比操作本身花费更多的时间,并且顺序运行整个事情会更快。如果您有一个已经在运行的线程池(例如 ExecutorService),那么提交任务的开销可能小到足以提供与顺序执行相比的收益。

如果您愿意支付一些性能开销,您还可以使用 java 8 的并行流,它可以让您编写更少的代码:

IntStream.range(0, arr.length).parallel()
         .forEach(i -> flip(arr[i]));

在任何情况下,您都应该衡量您正在做的事情在性能方面是否有意义...


参考资料:

  • Thread.start()/Thread.join() 创建发生在关系之前:JLS #17.4.5

    • 对线程上的start() 的调用发生在已启动线程中的任何操作之前。
    • 线程中的所有操作都发生在任何其他线程从该线程上的join() 成功返回之前。
  • 每个数组元素都是独立的:JLS #17.6

    特别是,分别更新字节数组的相邻元素的两个线程不得干扰或交互,并且不需要同步以确保顺序一致性。

    更准确地说,这是在JLS #17.4.1 中定义的,它表示每个数组元素都被视为一个单独的变量,以实现多线程:

    在本章中,我们使用术语变量来指代字段和数组元素。

【讨论】:

  • 我相信第 2 步和第二轮子弹才是问题的本质所在。
【解决方案2】:

由于其他线程没有触及数组索引,这应该没问题。不过,你有很多虚假的分享。更好的方法是将索引 0-24 分配给线程 A,将 25-49 分配给线程 B,等等。这样对缓存更加友好。

【讨论】:

    【解决方案3】:

    我假设您的代码只是伪代码。 (它不是标记的 Java 代码,所以也许您最好为我们提供真实的示例代码)。但我会接受你的想法。

    你有一个数据结构arr

    你有四个线程同时修改arr

    您的代码不是线程安全的。

    为了使其线程安全,您可以使用互斥锁(处理并发的技术之一)。

    围绕arr 包裹一个互斥锁。这将锁定arr,并且只有拥有互斥锁的线程才能访问arr

    互斥量应该在for循环开始时获取,在for循环结束时释放。

    循环将竞争互斥体,顺序通常是不可预测的。


    P.S.:如果您可以断言没有 arr 元素引用一个对象,而该对象被另一个 arr 元素引用,那么您可能不需要太关心线程安全。不过最好还是写线程安全的代码吧。

    【讨论】:

    • “你的代码不是线程安全的” - 这似乎是一个与其他答案相矛盾的断言,所以你真的应该用事实来支持它(并且就个人而言,作为一个发布赏金的人 - 但不是OP -,我假设数组引用的所有对象都没有相互数据 - 否则“不是线程安全”的答案变得太明显了,一开始就不用问了)
    • 我认为您的回答可能过于简单了 - 请注意,循环的起始索引相差 1,步长为 4,因此每个线程不会与数组的相同成员交互任何其他(即它们在数组的不同子集上运行)。
    • @JRichardSnape 我不确定您的确切意思。可能是我在 P.S. 中提到的吗? ?
    • @DVK 阅读附言。 - 那里提到了。
    • 当然 - 你的一般观点是正确的,我同意这个问题远不清楚它是对特定情况感兴趣,还是对从不同线程访问同一数组时的线程安全性感兴趣。
    猜你喜欢
    • 2011-01-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多