【问题标题】:Is accessing two AtomicIntegers as one operation thread safe?作为一个操作线程访问两个 AtomicInteger 是否安全?
【发布时间】:2018-09-17 23:13:50
【问题描述】:

在 Brian Goetz 的Java Concurrency in Practice中有一个例子如下:

public class NumberRange {
  // INVARIANT: lower <= upper
  private final AtomicInteger lower = new AtomicInteger(0);
  private final AtomicInteger upper = new AtomicInteger(0);

  public void setLower(int i) {
    // Warning -- unsafe check-then-act
    if (i > upper.get())
    throw new IllegalArgumentException(
    "can't set lower to " + i + " > upper");
    lower.set(i);
  }

  public void setUpper(int i) {
    // Warning -- unsafe check-then-act
    if (i < lower.get())
      throw new IllegalArgumentException(
      "can't set upper to " + i + " < lower");
    upper.set(i);
  }

  public boolean isInRange(int i) {
    return (i >= lower.get() && i <= upper.get());
  }
}

我了解上述代码容易出现竞争条件。

然后他解释如下:

像这样的多变量不变量会产生原子性要求:必须在单个原子操作中获取或更新相关变量。您不能更新一个、释放并重新获取锁,然后再更新其他的,因为这可能涉及在释放锁时使对象处于无效状态。

我从这一段中了解到,如果我们将setUppersetLower 函数设为synchronized,那么也会出现对象可能达到无效状态的情况。但是,我认为如果两个函数都是同步的,那么只有一个线程可以执行其中一个函数,并且每个函数都对不变量进行必要的检查。我们怎么能处于无效状态。任何人都可以举例说明。我在这里错过了什么?

如果我理解正确,那么这行的意义是什么:

您不能更新一个、释放并重新获取锁,然后再更新其他的,因为这可能涉及在释放锁时使对象处于无效状态。

【问题讨论】:

  • 如果它们是同步的,我看不出有任何问题。使用AtomicInteger 毫无意义。
  • 无需花费大量时间运行不同的测试,i 的检查和i 的设置之间,lowerupper 的值可以通过以下方式更改一个不同的线程,使检查无效。重点是,在getset之间,需要防止任何其他线程修改值的状态
  • @MadProgrammer 我知道这可能会发生。但是作者在我提到的段落中试图解释什么。
  • @JotWaraich 跟我说的差不多。 get 保证方法返回时的值,但是,在它和 set 之间,状态可以改变,你不能保证在调用 set 和调用 get 时状态相同.您需要生成一个原子状态,当您可以推断在调用 set 时,自调用 get 以来状态没有被修改。如果getset在同一个锁的上下文中被调用,那么继续应该是安全的
  • 我不认为他在处理这个特殊的 sn-p。他的观点是,当您拥有多变量不变量时,单独保护每个字段是不够的。

标签: java multithreading thread-safety


【解决方案1】:

来自“Java 并发实践”一书:

NumberRange 可以通过使用锁定来保持其线程安全 不变量,例如用一个共同的锁保护下部和上部。它 还必须避免发布上下限以防止客户 颠覆它的不变量。

这意味着下面的代码是线程安全的:

@ThreadSafe
public class NumberRange {

    @GuardedBy("this") private int lower, upper;

    public synchronized void setLower(int i) {
        if (i > upper) {
            throw new IllegalArgumentException("can't set lower to " + i + " > upper");
        }
        lower = i;
    }

    public synchronized void setUpper(int i) {
        if (i < lower) {
            throw new IllegalArgumentException("can't set upper to " + i + " < lower");
        }
        upper = i;
    }

    public synchronized boolean isInRange(int i) {
        return (i >= lower && i <= upper);
    }
}

在这种情况下,NumberRange 提供自己的锁定以确保复合操作是原子的。

【讨论】:

  • 这个 sn-p 不安全。打破它的例子: setup: x.setLower(0); x.setUpper(100);线程 1: x.setLower(80);线程 2: x.setUpper(50);使用正确的交错,您最终会得到 80 的下限和 50 的上限,这会破坏不变量。
  • 不安全,因为设置范围不是原子的。你需要一个setRange(int, int) 方法。
  • @JotWaraich 1) 范围的状态为 5..7,线程 1 想要将范围更改为 1..3,线程 2 想要 11..13,2) 线程 1 调用 setLower(1) - state = 1..7 已经是一个问题,3) 线程 2 调用 setLower(11) - state = 11..7 这毫无意义,4) 线程 2 调用 setUpper(13) - state = 11..13, 5) 线程1 调用 setUpper(3)。最终状态 = 11..3 哎呀!看看您如何将状态更改变成一个原子操作 - 即在一个同步方法中设置所有状态。同步方法意味着线程安全!!!
  • @Bohemian 我开始明白你在说什么。再澄清一点。状态 1..7 怎么已经是个问题了?我们如何才能达到状态 11..7,我认为它会抛出异常。同样11..3也会导致异常?
  • @Bohemian 我认为您误解了书中和我的回答中这些小例子的要点。这不是关于设置范围,而是关于设置一个特定变量 - lowerupper(检查 lower,更新 upper,反之亦然 - 复合操作)。此外,您的示例将生成 Exception,您无法设置无效范围。你甚至没有测试代码...
猜你喜欢
  • 1970-01-01
  • 2021-08-25
  • 1970-01-01
  • 2010-12-03
  • 1970-01-01
  • 2019-10-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多