【问题标题】:How can one break this (non?) thread safe object?如何破坏这个(非?)线程安全对象?
【发布时间】:2012-03-26 21:17:20
【问题描述】:

我之前回复了question 关于线程安全的问题,但没有得到明确的答案(我认为)。

所以我一直试图通过让数千个线程读取和写入该对象来说服自己设计被破坏(可见性) - 但我无法得到任何意想不到的东西。这显然不能证明它是线程安全的,可能只是证明我自己的局限性!

我了解重新排序的风险,但我看不出它在这种情况下如何应用,因为bar() 方法中的clone 实例是本地的,并且其字段的更改是在执行之前完成的使用return 向外界发布,之后实例实际上是不可变的。因此,查看返回对象的线程会看到它的 bar 字段已设置为正确的值...

所以我的问题是: 什么样的代码你能否展示一段使用IsItSafe的代码,它可能导致2个线程看到不同的值IsItSafe 的给定实例的bar 字段?

为了参考和阅读方便,我将代码复制到这里:

public class IsItSafe implements Cloneable {

    private int foo;
    private int bar;

    public IsItSafe foo(int foo) {
        IsItSafe clone = clone();
        clone.foo = foo;
        return clone;
    }

    public IsItSafe bar(int bar) {
        IsItSafe clone = clone();
        clone.bar = bar;
        return clone;
    }

    public int getFoo() {
        return foo;
    }

    public int getBar() {
        return bar;
    }

    protected IsItSafe clone() {
        try {
            return (IsItSafe) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new Error(e);
        }
    }
}

【问题讨论】:

  • 该代码描述了一个不可变对象。不可变的东西总是线程安全的。
  • 实际上它只是有效的不可变,必须安全发布以避免可见性问题。

标签: java multithreading thread-safety


【解决方案1】:

我会更担心错误,比如你想让 foo 在你设置 bar 时变成 0 吗?

您的字段实际上是最终的。使它们成为最终的并使用构造函数创建新副本将使它们成为线程安全的。当您更改 bar 和反之亦然时,这也会使您在重置 foo 时更加明显。

您遇到的一个问题是您更改了 foo 或 bar 的值(从构造函数中设置的 0 开始)。这意味着如果您在另一个线程中查看此对象,您可能会看到您想要的值,或者您可能会看到 0。

private IsItSafe iis = null;

// in thread A
iis = new IsItSafe().foo(123);

// in thread B
if (iis != null) {
   int foo = iis.getFoo();
   // foo could be 123, 
   // or it could be the initial value of the field foo which is 0.
}

【讨论】:

  • 我同意 1 和 2 但这不是重点。问题是关于你的第三点,我仍然很难弄清楚什么代码会做到这一点(另一个线程看到 0 而不是 xxx)。
  • iis 是不稳定的,所以实例是安全发布的,线程 B 会看到 foo=123
  • iis 是安全发布的参考。它的字段不是易变的,也不能保证安全发布。
  • 我不这么认为,根据 JCIP 关于 volatile 变量(3.1.4):“volatile 变量的可见性影响超出了 volatile 变量本身的值。当线程 A 写入volatile 变量,随后线程 B 读取同一个变量,在写入 volatile 变量之前对 A 可见的所有变量的值在读取 volatile 变量后对 B 可见"
  • @EmmanuelBourg 你说得对,我在这里删除了volatile
【解决方案2】:
当您的程序写入变量的值被缓存在 cpu 缓存 中而不是立即写入 RAM 时,可能会出现

可见性问题。因此,如果在 cpu A 上运行的线程 A 在没有正确同步的情况下写入一个值,而线程 B 从 cpu B 读取该值,他可能会在 RAM 中看到一个陈旧的值,而不是最新的值(仅存在于处理器 A 的 cpu 缓存中)。

在给出的示例中,您没有使用 Java 提供的任何机制来确保安全发布。即构造函数中设置的同步volatilefinal fields

因此可以想象,在您的示例中,对创建 clone 对象的引用变为可用,但写入克隆字段的值仍保留在 cpu 缓存中。在这种情况下,其他线程将无法读取最新值。

提供一些参考。看这个例子

类 FinalFieldExample { 最终诠释 x; 整数y; 静态 FinalField 示例 f; 公共 FinalFieldExample() { x = 3; y = 4; } 静态无效作家(){ f = 新的 FinalFieldExample(); } 静态无效读者(){ 如果(f!= null){ 诠释 i = f.x; 诠释 j = f.y; } } }

上面的类是如何使用 final 字段的示例。保证执行 reader 的线程看到 f.x 的值 3,因为它是最终的。不能保证看到 y 的值 4,因为它不是最终值。

你提出的论点也适用于这个例子,不是吗?创建实例,在构造函数中设置字段等。但是它不是线程安全的,因为写入y 的值不需要对其他线程可见。 (引用的例子来自JSR 133 (Java Memory Model) FAQhttp://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#reordering

更新:您要求提供演示问题的代码。我曾经问过一个类似的(更开放的)问题:How to demonstrate java multithreading visibility problems? 给出的代码示例的有趣之处在于,它会在不同的 Java 次要版本、不同的操作系统以及使用客户端或服务器 jvm 时表现不同。在这方面,我发现这个样本很有趣。 需要注意的重要一点是,今天实际创建导致代码可见性问题的示例代码很可能是不可能的。然而,明年 cpu 生成可能会实施不同的缓存策略,问题突然出现。如果您遵循 Java 语言规范的指导方针,您的保存。

【讨论】:

  • 一个非常好的答案!我已经删除了我的,以便引起更多关注:)
  • @pst 哇,真是太好了。谢谢!
【解决方案3】:

您的代码无关紧要。

问题很简单:如果bar 不是volatile 并且访问不同步,如果一个线程更改了它的值,则不能保证另一个线程看到更改。

【讨论】:

    猜你喜欢
    • 2012-09-10
    • 2016-05-15
    • 2010-12-09
    • 2011-07-18
    • 2011-08-02
    • 1970-01-01
    • 1970-01-01
    • 2017-06-29
    • 1970-01-01
    相关资源
    最近更新 更多