【问题标题】:How do you ensure multiple threads can safely access a class field?你如何确保多个线程可以安全地访问一个类字段?
【发布时间】:2008-09-23 00:17:29
【问题描述】:

当多个线程通过getter方法访问一个类字段时,如何维护线程安全? synchronized 关键字是否足够?

这样安全吗:

public class SomeClass {
    private int val;

    public synchronized int getVal() {
        return val;
    }

    private void setVal(int val) {
        this.val = val;
    }
}

或者二传手是否引入了更多的复杂性?

【问题讨论】:

  • tl;dr:这不是线程安全的。

标签: java multithreading synchronized


【解决方案1】:

如果您在此处的设置器上也使用“同步”,则此代码是线程安全的。然而,它可能不够精细;如果你有 20 个 getter 和 setter 并且它们都是同步的,那么你可能会造成同步瓶颈。

在这个特定实例中,使用单个 int 变量,然后消除 'synchronized' 并将 int 字段标记为 'volatile' 也将确保可见性(每个线程在调用 getter 时将看到最新的 'val' 值)但是它可能不够同步以满足您的需要。例如,期待

 int old = someThing.getVal();
 if (old == 1) {
    someThing.setVal(2);
 }

当且仅当它已经是 1 时才将 val 设置为 2 是不正确的。为此,您需要一个外部锁,或一些原子比较和设置方法。

我强烈建议您阅读 Brian Goetz 的Java Concurrency In Practice,它对 Java 的并发结构有最好的覆盖。

【讨论】:

  • 查看我的答案以了解更多进行比较和存储的方法
【解决方案2】:

除了Cowan's comment,您还可以执行以下操作进行比较和存储:

synchronized(someThing) {
    int old = someThing.getVal();
    if (old == 1) {
        someThing.setVal(2);
    }
}

之所以可行,是因为通过同步方法定义的锁与对象的锁 (see java language spec) 隐式相同。

【讨论】:

  • 但它完全依赖于客户端代码的并发意识 - 不太建议(-1)
【解决方案3】:

据我了解,您应该在 getter 和 setter 方法上都使用 synchronized,这就足够了。

编辑:这里是link 以获取有关同步的更多信息以及其他信息。

【讨论】:

    【解决方案4】:

    如果您的类只包含一个变量,那么实现线程安全的另一种方法是使用现有的 AtomicInteger 对象。

    public class ThreadSafeSomeClass {
    
        private final AtomicInteger value = new AtomicInteger(0);
    
        public void setValue(int x){
             value.set(x);
        }
    
        public int getValue(){
             return value.get();
        }
    
    }
    

    但是,如果您添加其他变量以使它们相互依赖(一个变量的状态取决于另一个变量的状态),那么 AtomicInteger 将不起作用。

    赞同阅读“Java 并发实践”的建议。

    【讨论】:

    • 你正在用锤子杀死一只蚂蚁。这是 AtomicInteger 的一个非常糟糕的用例。只需标记value 字段volatile 即可获得等效代码。
    【解决方案5】:

    对于简单的对象,这可能就足够了。在大多数情况下,您应该避免使用 synchronized 关键字,因为您可能会遇到同步死锁。

    例子:

    public class SomeClass {
    
      private Object mutex = new Object();
      private int    val   = -1; // TODO: Adjust initialization to a reasonable start
                                 //       value
      public int getVal() {
        synchronized ( mutex ) {
          return val;
        }
      }
    
      private void setVal( int val ) {
        synchronized ( mutex ) {
          this.val = val;
        }
      }
    }
    

    确保只有一个线程读取或写入本地实例成员。

    阅读《Concurrent Programming in Java(tm): Design Principles and Patterns (Java (Addison-Wesley))》一书,或许http://java.sun.com/docs/books/tutorial/essential/concurrency/index.html也有帮助……

    【讨论】:

      【解决方案6】:

      存在同步是为了防止线程干扰和内存一致性错误。通过在 getVal() 上同步,代码保证 SomeClass 上的其他同步方法不会同时执行。由于没有其他同步方法,因此它没有提供太多价值。另请注意,对原语的读取和写入具有原子访问权限。这意味着通过仔细编程,不需要同步对字段的访问。

      阅读Sychronization

      不太清楚为什么会降到 -3。我只是在总结 Sun 的同步教程所说的(以及我自己的经验)。

      使用简单的原子变量访问是 比访问这些更有效 通过同步代码的变量, 但需要更多的照顾 程序员避免内存一致性 错误。额外的努力是否 是否值得取决于大小和 应用程序的复杂性。

      【讨论】:

      • “对原语的读写具有原子访问权限”——并非如此。对 long 和 double 的读取和写入只有在标记为 volatile 时才是原子的。
      • 哦,是的,保证只适用于变量,不适用于原始类型数组中的项目。
      • 对字段的读取和写入(已说明警告)是原子的,但不足以保证发布。因此,对 int 字段的写入将安全地写入该字段,但不保证其他线程会看到写入。除非你使用 synchronized 或 volatile。
      • 我认为这被否决是因为它不完整。 “通过仔细编程,不需要同步对字段的访问”是过于简单化了。如果没有 synchronized 或 volatile,对变量的任何更改可能永远不会被其他线程看到,这是很少需要的。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-06-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多