【问题标题】:How to write a simple thread-safe class using a volatile variable?如何使用 volatile 变量编写简单的线程安全类?
【发布时间】:2013-05-03 12:09:06
【问题描述】:

我想编写一个简单的线程安全类,可以用来设置或获取整数值。

最简单的方法是使用同步关键字:

public class MyIntegerHolder {

    private Integer value;

    synchronized public Integer getValue() {
        return value;
    }

    synchronized public void setValue(Integer value) {
        this.value = value;
    }

}

我也可以尝试使用 volatile

public class MyIntegerHolder {

    private volatile Integer value;

    public Integer getValue() {
        return value;
    }

    public void setValue(Integer value) {
        this.value = value;
    }

}

带有volatile关键字的类线程安全吗?

考虑以下事件序列:

  1. 线程 A 将值设置为 5。
  2. 线程 B 将值设置为 7。
  3. 线程 C 读取值。

它遵循 Java 语言规范

  • “1”发生在之前“3”
  • “2”发生在之前“3”

但我看不出它如何从规范中得出“1”发生在“2”之前,所以我怀疑“1”不会 发生在“2”。

我怀疑线程 C 可能读取 7 或 5。我认为带有 volatile 关键字的类不是 thread-safe 并且以下顺序也是可能的:

  1. 线程 A 将值设置为 5。
  2. 线程 B 将值设置为 7。
  3. 线程 C 读取 7。
  4. 线程 D 读取 5。
  5. 线程 C 读取 7。
  6. 线程 D 读取 5。
  7. ...

我是否正确假设带有 volatile 的 MyIntegerHolder 不是 线程安全

是否可以使用 AtomicInteger 制作线程安全的整数持有者:

public class MyIntegerHolder {

    private AtomicInteger atomicInteger = new AtomicInteger();

    public Integer getValue() {
        return atomicInteger.get();
    }

    public void setValue(Integer value) {
        atomicInteger.set(value);
    }

}

?

这是 Java 并发实践一书的片段:

"原子变量的读写具有相同的内存语义 作为 volatile 变量。"

编写线程安全 MyIntegerHolder 的最佳(最好是非阻塞)方式是什么?

如果你知道答案,我想知道你为什么认为它是正确的。它是否遵循规范?如果有,怎么做?

【问题讨论】:

  • 我还在学习,如果有人能向我澄清一件事,那就太好了。 synchronizedvolatile 关键字如何使它更加线程安全?由于整数赋值和读取本身是原子的,它会改变什么吗?这种方法无论如何都不允许原子增量或获取和设置。谢谢
  • @Sebi 线程安全是一个在两个或多个线程中运行的程序,没有一个线程干扰另一个线程。您可以通过不同的方式/技术解决这个问题,一个线程干扰另一个线程。一个简单的 int x = x + x 似乎是原子的,但不是(更多关于这个,搜索 volatile 和 atomic)! “让它更加线程安全”,只是解决问题的一种方法。这不会使更多线程安全,只是解决相同问题的不同实现。
  • @ThufirHawat 我知道什么是线程安全程序,或者添加涉及在写入之前进行多次读取,但我确信这门课有一些我不知道的东西。我无法理解如何使用暴露整数的 getter 和 setter 的类来执行任何需要线程安全而不被外部锁定的事情(Integer 就足够了)。

标签: java multithreading concurrency java-memory-model


【解决方案1】:

关键字synchronized 表示如果Thread A and Thread B 想要访问Integer,他们不能同时访问。 A 告诉 B 等我完成。

另一方面,volatile 使线程更加“友好”。他们开始互相交谈并一起工作以执行任务。因此,当 B 尝试访问时,A 会通知 B 它在那之前所做的一切。 B 现在知道这些变化,可以从 A 离开的地方继续工作。

在 Java 中,Atomic 正是出于这个原因,它在幕后使用了 volatile 关键字,所以它们做的事情几乎相同,但它们可以节省您的时间和精力。

你要找的是AtomicInteger,你是对的。对于您尝试执行的操作,这是最佳选择。

There are two main uses of `AtomicInteger`:

 * As an atomic counter (incrementAndGet(), etc) that can be used by many threads concurrently

 * As a primitive that supports compare-and-swap instruction (compareAndSet()) to implement non-blocking algorithms. 

一般性地回答您的问题

这取决于你需要什么。我并不是说synchronized 是错误的而volatile 是好的,否则好心的Java 人早就会删除synchronized。没有绝对的答案,有很多具体的案例和使用场景。

我的一些书签:

Concurrency tips

Core Java Concurrency

Java concurrency

更新

来自可用的 Java 并发规范here

包 java.util.concurrent.atomic

支持无锁线程安全的类的小工具包 对单个变量进行编程。

Instances of classes `AtomicBoolean`, `AtomicInteger`, `AtomicLong`, and `AtomicReference` each provide access and updates to a single variable of the corresponding type.
Each class also provides appropriate utility methods for that type.
For example, classes `AtomicLong` and AtomicInteger provide atomic increment methods.

The memory effects for accesses and updates of atomics generally follow the rules for volatiles:

get has the memory effects of reading a volatile variable.
set has the memory effects of writing (assigning) a volatile variable.

来自Here

Java 编程语言volatile 关键字:

(在所有 Java 版本中)对 volatile 变量的读取和写入有一个全局顺序。这意味着访问 volatile 字段的每个线程将在继续之前读取其当前值,而不是(可能)使用缓存值。 (但是,不能保证 volatile 读写与常规读写的相对顺序,这意味着它通常不是有用的线程构造。)

【讨论】:

  • 我理解你的回答,但问题没那么简单。我认为带有 volatile 的 MyIntegerHolder 不是线程安全的,我提出了我的推理。在我看来,正确的答案必须参考规范和happens-before关系的定义。也许我错了, MyIntegerHolder 是线程安全的 - 但在这种情况下,答案应该解释什么是理解发生前关系定义的正确方法。或者问题可能不在于代码(也许它是正确的),而在于规范(也许它不够清楚)。
  • "A 将通知 B 它在那之前所做的一切" - 你确定吗?它是否遵循规范?在happens-before关系的定义中,您可以看到写发生在读之前,但我没有看到片段说写发生在另一个写之前。
  • 我知道 AtomicInteger 类似于 volatile Integer。但问题实际上是关于正确理解 Java Language Specification 中定义的发生之前的关系
【解决方案2】:

如果您只需要在变量上获取/设置,则像您一样将其声明为 volatile 就足够了。如果您检查 AtomicInteger 如何设置/获取工作,您将看到相同的实现

private volatile int value;
...

public final int get() {
    return value;
}

public final void set(int newValue) {
    value = newValue;
}

但是你不能这么简单地原子地增加一个 volatile 字段。这是我们使用 AtomicInteger.incrementAndGet 或 getAndIncrement 方法的地方。

【讨论】:

    【解决方案3】:

    Java 语言规范的第 17 章定义了内存操作(例如共享变量的读取和写入)的发生前关系。只有当写操作发生在读操作之前,一个线程写的结果才能保证对另一个线程的读可见。

    1. synchronized 和 volatile 构造以及 Thread.start() 和 Thread.join() 方法可以形成happens-before 关系。特别是:线程中的每个动作都发生在之前 该线程中的每个操作都按程序顺序进行。
    2. 监视器的解锁(同步块或方法退出)发生在每个后续锁定(同步块或方法)之前 同一个监视器的条目)。因为happens-before关系 是传递的,线程在解锁之前的所有动作 发生在任何线程锁定之后的所有操作之前 监控。
    3. 对易失性字段的写入发生在对同一字段的每次后续读取之前。 volatile 字段的写入和读取具有相似的 内存一致性效果作为进入和退出监视器,但确实 不需要互斥锁定。
    4. 在线程上启动的调用发生在已启动线程中的任何操作之前。
    5. 线程中的所有操作都发生在任何其他线程从该线程的连接成功返回之前。

    参考:http://developer.android.com/reference/java/util/concurrent/package-summary.html

    根据我的理解 3 意味着:如果你写(不是基于读取结果)/读取就可以了。如果您写入(基于读取结果,例如增量)/读取不正常。由于 volatile “不需要互斥锁定”

    【讨论】:

      【解决方案4】:

      带有 volatile 的 MyIntegerHolder 是线程安全的。但是如果你在做并发程序,AtomicInteger 是首选,因为它也提供了很多原子操作。

      考虑以下事件序列:

      1. 线程 A 将值设置为 5。
      2. 线程 B 将值设置为 7。
      3. 线程 C 读取值。

      它遵循 Java 语言规范

      • “1”发生在“3”之前
      • “2”发生在“3”之前

      但我看不出它如何从规范中得出“1” 发生在“2”之前,所以我怀疑“1” 不会发生在“2”之前。

      我怀疑线程 C 可能是 7 或 5。我认为带有 volatile 关键字不是线程安全的

      您就在这里,“1”发生在“3”之前,“2”发生在“3”之前。 “1”不会发生在“2”之前,但这并不意味着它不是线程安全的。问题是您提供的示例模棱两可。如果您说“将值设置为5”,“将值设置为7”,“读取值”顺序发生,您始终可以读取7的值。将它们放在不同的线程中是无稽之谈。但是如果你说 3 个线程没有顺序地同时执行,你甚至可以得到 0 的值,因为“读取值”可能首先发生。但这与线程安全无关,这 3 个动作没有任何顺序。

      【讨论】:

        【解决方案5】:

        这个问题对我来说并不容易,因为我(错误地)认为了解关于 happens-before 关系的所有内容可以让人们全面了解 Java 内存模型 - 以及 易变

        我在本文档中找到了最好的解释: "JSR-133: JavaTM Memory Model and Thread Specification"

        上述文档中最相关的片段是“7.3 格式良好的执行”部分。

        Java 内存模型保证程序的所有执行格式正确。一个执行是格式良好的只有当它

        • 服从在一致性之前发生
        • 遵守同步顺序一致性
        • ...(其他一些条件也必须为真)

        Happens-before 一致性 通常足以得出关于程序行为的结论 - 但在这种情况下并非如此,因为 volatile 写入不会happen-before另一个 volatile 写入。

        带有volatile的MyIntegerHolder是线程安全的,但它的安全性来自于同步顺序的一致性

        在我看来,当线程 B 即将将该值设置为 7 时,A 直到那一刻才通知 B 它所做的一切(正如其他答案之一所建议的那样) - 它只通知 B 的值易失性变量。如果线程 B 采取的操作是读取而不是写入(在这种情况下,将存在 happens-before em> 这两个线程所采取的动作之间的关系)。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-09-23
          • 1970-01-01
          • 2019-03-22
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多