如何打破这个(非?)线程安全对象?

时间:2012-03-09 12:04:05

标签: java multithreading thread-safety

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

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

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

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

为了参考和便于阅读,我在这里复制代码:

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);
        }
    }
}

3 个答案:

答案 0 :(得分:4)

当程序写入变量的值缓存在 cpu cache 中而不立即写入RAM时,可能会出现

可见性问题。因此,如果在cpu A上运行的线程A在没有正确同步的情况下写入值而线程B从cpu B读取该值,则可能在RAM中看到过时的值而不是最近的值(仅出现在处理器A的cpu缓存中) )。

在给出的示例中,您没有使用Java提供的任何机制来确保安全发布。这是在构造函数中设置的同步易失性最终字段

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

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

class FinalFieldExample {
  final int x;
  int y;
  static FinalFieldExample f;
  public FinalFieldExample() {
    x = 3;
    y = 4;
  }

  static void writer() {
    f = new FinalFieldExample();
  }

  static void reader() {
    if (f != null) {
      int i = f.x;
      int j = f.y;
    }
  }
}
  

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

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

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

答案 1 :(得分:3)

您的代码无关紧要。

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

答案 2 :(得分:1)

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

你的领域实际上是最终的。使它们成为最终并使用构造函数来创建新副本将使它们成为线程安全的。当你改变标准时,你也会更明显地重置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.
}