8

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

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

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关键字的类不是线程安全的,以下顺序也是可能的:

  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 Concurrency In Practice 一书的片段:

原子变量的读取和写入与 volatile 变量具有相同的内存语义。

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

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

4

5 回答 5

4

关键字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了。没有绝对的答案,有很多具体的案例和使用场景。

我的一些书签:

并发提示

核心 Java 并发

Java 并发

更新

来自此处提供的 Java 并发规范:

包 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.

这里

Java 编程语言volatile关键字:

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

于 2013-05-03T12:15:07.263 回答
1

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

private volatile int value;
...

public final int get() {
    return value;
}

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

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

于 2013-05-03T12:14:30.547 回答
0

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

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

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

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

于 2015-04-01T00:54:47.793 回答
0

您的带有 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 个动作没有任何顺序。

于 2016-11-20T19:07:40.760 回答
-1

这个问题对我来说并不容易,因为我(错误地)认为了解发生前关系的所有内容可以让人们全面了解 Java 内存模型以及volatile的语义。

我在这篇文档中找到了最好的解释: “JSR-133: JavaTM Memory Model and Thread Specification”

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

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

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

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

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

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

于 2013-05-03T19:15:06.570 回答