繁体   English   中英

如何使用volatile变量编写简单的线程安全类?

[英]How to write a simple thread-safe class using a volatile variable?

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

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

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来创建线程安全的Integer持有人:

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》一书的一部分:

原子变量的读写与易失性变量具有相同的内存语义。

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

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

关键字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字段的线程都将在继续之前读取其当前值 ,而不是(潜在地)使用缓存的值。 (但是,不能保证易失性读写与常规读写之间的相对顺序,这意味着它通常不是有用的线程构造。)

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

private volatile int value;
...

public final int get() {
    return value;
}

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

但是您不能像这样简单地自动增加一个可变字段。 这是我们使用AtomicInteger.incrementAndGet或getAndIncrement方法的地方。

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

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

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

从我的理解3的意思是:如果您写(不是基于读取结果)/读取就可以了。 如果您写(基于读取结果,例如增量)/读取不正确。 由于易失性“不需要互斥锁定”

带有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值,因为“读取值”可能首先发生。 但这对于线程安全而言是一无是处,三个动作都没有期望的顺序。

这个问题对我来说并不容易,因为我认为(错误地)知道事前关系的所有知识可以使人们对Java内存模型和volatile的语义有一个完整的了解。

我在此文档中找到了最好的解释: “ JSR-133:JavaTM内存模型和线程规范”

以上文档中最相关的片段是“ 7.3格式正确的执行”部分。

Java内存模型可确保程序的所有执行都具有良好的格式 执行只有在执行时格式正确

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

发生在一致性之前通常足以得出有关程序行为的结论-但是在这种情况下,这是不足够的,因为不会发生易失性写操作-在另一个易失性写操作之前

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM