【问题标题】:Volatile or Synchronized in javajava中的易失或同步
【发布时间】:2021-10-24 14:57:05
【问题描述】:

我们有一个类 CalcStrategySet,它在并发环境中被多个线程访问。我想知道你们是否可以从下面列出的 2 中建议哪种实现更好。在第一个中,我将变量声明为 volatile 并创建了翻转方法,这是同步的原子操作。其他实现我没有将变量声明为 volatile,但将所有其他方法设为同步。

这些方法是否达到目的并且不会导致竞争条件,如果是,那么哪个更好?

采用易失性和同步原子方法

public class CalcStrategySet {
   private volatile CalcStrategy current;
   private volatile CalcStrategy backup;
   private volatile boolean isBackup;

   public CalcStrategySet(CalcStrategy current, CalcStrategy backup) {
       this.current = current;
       this.backup = backup;
   }

   public void isStandard() {
       return !isBackup;
   }
  
   public void merge(CalcStrategy other) {
      current.merge(other);
   }

   public synchronized void flip() {
      if(!isBackup) {
          current = backup;
          backup = null;
          isBackup = true;
      } else {
          throw new IllegalStateException("Already in backup mode");
      }
   }
}

完全同步

public class CalcStrategySet {
   private CalcStrategy current;
   private CalcStrategy backup;
   private boolean isBackup;

   public CalcStrategySet(CalcStrategy current, CalcStrategy backup) {
       this.current = current;
       this.backup = backup;
   }

   public synchronized void isStandard() {
       return !isBackup;
   }
  
   public synchronized void merge(CalcStrategy other) {
      current.merge(other);
   }

   public synchronized void flip() {
      if(!isBackup) {
          current = backup;
          backup = null;
          isBackup = true;
      } else {
          throw new IllegalStateException("Already in backup mode");
      }
   }
}

【问题讨论】:

  • 在您的第一个实现中,没有什么能阻止线程 1 (T1) 在 flip 方法中,发现 isBackup 为假,并且在 T2 进入 merge 方法并修改 @ 之后的情况987654326@。有时 T2 会在 current = backup 之前执行此操作,有时在之后执行此操作。对于您的用例,这可能是也可能不是问题。此外,如果 CalcStrategy 不是线程安全的,则 merge 方法如果不同步可能会成为问题。
  • @assylias 我会说第一个实现成功地满足了两个(隐式)不变量(1)没有并发翻转(2)在替换为备份之前使用当前策略。如果这是所有 OP 的需要,那么第一个实现就可以了。

标签: java multithreading concurrency synchronized volatile


【解决方案1】:

这些方法是否达到目的并且不会导致竞争条件,如果是,那么哪个更好?

如果不完全了解类的使用方式以及CalcStrategy.merge() 方法的作用,就很难确定这一点。如果merge() 已经是可重入的,那么第一种机制似乎就足够了。肯定存在竞争条件,这可能意味着电流在 flip(...) 方法到达开关之前得到更新。

担心的是flip(...) 方法可能会转移到备份,但尚未将isBackup 切换为true。例如,一个线程可能在flip(...) 的中间,当前已设置为备份,但另一个线程可能仍看到isStandard() 返回true,因为尚未设置isBackup。多个volatile 字段加剧了这种情况。

改善这种情况的一种方法是将AtomicReference 用于current。这将减少这种竞争条件,并意味着flip() 不需要是synchronized。在下面的代码中,standardbackup 是最终的,而不是 volatile。不确定这是否会破坏某些东西。 isStandard() 仍有可能在 flip() 进行更改之前被调用,但这是一种改进。

类似:

private final AtomicReference<CalcStrategy> current = new AtomicReference<>();
private final CalcStrategy standard;
private final CalcStrategy backup;
// no boolean

public CalcStrategySet(CalcStrategy standard, CalcStrategy backup) {
    this.current.set(standard);
    this.standard = standard;
    this.backup = backup;
}

public void isStandard() {
    // no race with isBackup set in flip()
    return (current.get() == standard);
}

// no need for synchronized because one single atomic operation
public void flip() {
   // update to the backup atomically
   if(!current.compareAndSet(standard, backup)) {
       throw new IllegalStateException("Already in backup mode");
   }
}

【讨论】:

  • 您的示例代码还必须将构造参数standard 存储在单独的字段中,例如您在方法实现中访问的standard 字段。
  • flip() 方法仍然可以改变currentafter isStandard() 获取了它的值,before 它与standard 比较,导致isStandard() 返回“错误”值。这意味着您最大的担忧仍然会发生,尽管可能性较小。幸运的是,担心永远不会代表CalcStrategySet 对象本身的不一致状态:它不是竞争条件。虽然我更喜欢您的原子解决方案,但如果 isStandard 的调用站点知道它可以返回过时的图片,那么使用 volatiles 的原始第一个实现就足够了。
  • @Emmef 仍然存在竞争条件,但这里没有双字段问题。要么标准在测试时已经更新,要么没有。只要在current.compareAndSet(...) 之后调用current.get()isStandard() 就永远不会返回错误的值。
  • 添加了 standard 字段。谢谢。
猜你喜欢
  • 2016-04-04
  • 1970-01-01
  • 2013-01-12
  • 2020-09-11
  • 2017-02-17
  • 2016-07-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多