【问题标题】:Thread safety/publication线程安全/发布
【发布时间】:2013-11-14 04:12:49
【问题描述】:

我一直在阅读并发操作,并有几个问题。

public final class ThreeStooges {  

    private final Set<String> stooges = new HashSet<String>();  

    public ThreeStooges() {  
        stooges.add("Moe");  
        stooges.add("Larry");  
        stooges.add("Curly");  
    }  

    public boolean isStooge(String name) {  
        return stooges.contains(name);  
    }  
}  

这本书说,因为这个类是不可变的,所以它是线程安全的,因为没有修改状态(stooges)。 我很困惑的是这个。如果多个线程同时调用 isStooge(String name) 方法会怎样。发生什么了?

public class HolderObject{  

    private Holder holder;  

    public void initialize() {  
        holder = new Holder(42);  
    }  
}  

书上说这不是线程安全的?为什么?没有正确发布是什么意思?

public class Holder {  
    private int n;  

    public Holder(int n) { this.n = n; }  

    public void assertSanity() {  
        if (n != n)  
            throw new AssertionError("This statement is false.");  
    }  
}  

这个也一样。它出什么问题了?如果多个线程调用assertSanity() 会怎样。

谢谢大家

更新

假设 stooges 类更改为以下...

public class ThreeStooges {  

private List<String> stooges = new ArrayList<String>();  

public ThreeStooges() {  
    stooges.add("Moe");  
    stooges.add("Larry");  
    stooges.add("Curly");  
}  

public synchronized void addStoog(String stoog){
         stooges.add(stoog);
}

public boolean getStoog(int index){
   return stooges.get(index);
}

public boolean isStooge(String name) {  
    return stooges.contains(name);  
}  
} 

这里有线程问题吗?吸气剂的可见性问题? 如果线程 A 要 addStoog("Bobby") 并且线程 B 调用 getStoog(3),那么最终的 stoog 会在 getter 上可见吗?

【问题讨论】:

    标签: java multithreading thread-safety


    【解决方案1】:

    如果多个线程调用 isStooge(String name) 方法会怎样 同时地。会发生什么?

    在 Java 的内存模型中,如果两个线程同时访问相同的数据并且其中至少有一个是写入的,那么您就会遇到麻烦。这称为conflicting access。

    在此示例中,您有多个线程访问相同的数据(stooges 变量),但它们都没有修改数据,所以您很好。

    书上说这不是线程安全的?为什么?没有正确发布是什么意思?

    但是,在这种情况下,您为holder 分配了一个新值,这是一次写入。这是一场数据竞赛,如果两个线程同时调用initialize,而没有外部同步,就会发生不好的事情。

    “已发布”一词很可能是指在一个线程中完成的更改如何对其他线程可见。虽然我不承认这是一个常用术语,所以我想这本书应该在某个时候给出这个术语的确切定义。

    这个也一样。它出什么问题了?如果多个怎么办 线程调用 assertSanity()。

    正如您发布的那样,代码看起来不错。由于assertSanity 仅读取n,因此您没有数据竞争。

    但是,正如@EnnoShioji 的回答所指出的,可能存在线程观察到未初始化的n 值的问题,因此看似微不足道的检查可能会失败。

    【讨论】:

      【解决方案2】:

      我同意@ComicSansMS 的前两个答案,但我认为他的第三个答案没有解决本书的原点。

      本书的那部分(Java 并发实践)正在讨论安全发布。一个不可变的对象并不自动意味着它没有并发问题(在这种情况下,可见性问题)。即使Holder 的状态在其生命周期内没有改变,但如果它被不同的线程访问而不满足某些规则,则其他线程可能会看到一个对象在中间构造,因此会看到不正确的值。

      因为在构造对象时,字段首先填充为空/零,在第三个示例(Holder)中,线程可以看到n=0,然后是n=42,因此抛出AssertionError。

      这就是这本书所说的意思:

      更糟糕的是,其他线程可能会看到最新值 供持有人参考,但状态的陈旧值 持有人

      如果您将 int 字段声明为 final,但是 Holder 对象将成为正式的“不可变对象”,因此 JMM 将保证不会发生这种竞争条件(请参阅有关特殊保证的段落在不可变对象上)。本书还介绍了您可以遵循的其他规则来防止这种情况发生。

      【讨论】:

      • 你们俩都帮了大忙,但我不能同时勾选。我也不能投票,因为我的代表少于 15 个。
      • +1 感谢您解释第三个示例。这里有一个后续问题:如果对Holder 的构造函数的调用和对assertSanity 成员函数的调用之间存在发生之前的关系,那么这场竞赛是否还会发生?我问这个问题是因为一般来说,如果你在构造函数完成执行和调用成员函数之间存在竞争,你通常会遇到更大的问题,因为在执行成员函数时可能没有建立类不变量。
      • 我认为从 Java 5 规范开始,内存模型已经改变,允许构造函数在调用其他方法之前完全完成。我不知道我是否 100% 正确。这里需要专家。
      • @ComicSansMS:如果我理解正确的话,如果有发生之前的关系,这场比赛就不会发生。我猜这种竞争条件真的只会在你尝试进行 String#hashCode 类型优化时影响你(利用操作的幂等性并依赖于引用分配的原子性,而不是使用像 volatile 这样的先发生设施),或者换句话说,奇怪的东西。
      【解决方案3】:

      考虑 Java 并发性的一种简单方法如下:

      • 每个线程都拥有一个变量的副本。
      • 如果不使用 Java 同步(易失性、锁定、同步),线程可能永远不会“获取”变量的新副本,并且在“设置”的情况下,它可能永远不会将值刷新到主变量。

      在您的示例中,您强制“设置器”锁定对象并使用 synchronized 关键字将新值刷新到主变量:synchronized void addStoog

      另一方面,您并没有强迫“getter”获取stooges 的新副本,这将导致读取不一致的数据。

      【讨论】:

        猜你喜欢
        • 2014-08-29
        • 2010-12-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-04-20
        • 2021-05-18
        • 2013-09-01
        相关资源
        最近更新 更多