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

时间:2013-05-03 12:09:06

标签: java multithreading concurrency java-memory-model

我想编写一个简单的线程安全类,可用于设置或获取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;
    }

}

该类是否具有易失性关键字线程安全

考虑以下事件序列:

  1. 线程A将值设置为5。
  2. 线程B将值设置为7.
  3. 线程C读取值。
  4. 遵循Java语言规范

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

    但我不知道它是如何遵循规范“1”发生在“2”之前所以我怀疑“1” 发生在“2”之前。

    我怀疑线程C可能读取7或5.我认为带有 volatile 关键字的类不是线程安全的,并且以下序列也是可能的:< / p>

    1. 线程A将值设置为5。
    2. 线程B将值设置为7.
    3. 线程C读取7。
    4. 线程D读取5。
    5. 线程C读取7。
    6. 线程D读取5。
    7. ...
    8. 假设MyIntegerHolder volatile 不是线程安全的,我是否正确?

      是否可以使用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一书的一个片段:

        

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

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

      如果你知道答案,我想知道为什么你认为这是正确的。它符合规范吗?如果是这样,怎么样?

5 个答案:

答案 0 :(得分:4)

关键字synchronized表示如果Thread A and Thread B想要访问Integer,则他们无法同时访问volatile。 A告诉B等到我完成它。

另一方面,Atomic使线程更“友好”。他们开始互相交谈,共同完成任务。因此,当B试图访问时,A将告知B它在那一刻所做的一切。 B现在知道这些变化,并且可以从A的左边继续其工作。

在Java中,由于这个原因,你有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是错误的,synchronized是好的,否则好的Java人很久以前就会删除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. 。没有绝对的答案,有很多具体案例和使用场景。

我的一些书签:

Concurrency tips

Core Java Concurrency

Java concurrency

<强>更新

来自可用的Java并发规范here

  

包java.util.concurrent.atomic

     

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

volatile
来自Here

Java编程语言{{1}}关键字:

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

答案 1 :(得分:1)

如果你只需要获取/设置一个变量就足以像你那样声明它是不稳定的。如果您检查AtomicInteger如何设置/获取工作,您将看到相同的实现

private volatile int value;
...

public final int get() {
    return value;
}

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

但是你无法通过原子方式增加volatile字段。这是我们使用AtomicInteger.incrementAndGet或getAndIncrement方法的地方。

答案 2 :(得分:0)

  

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

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

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

从我的理解3意味着:如果你写(不是基于阅读结果)/阅读就好了。如果你写(基于读取结果,例如增量)/读取不好。由于不稳定“不需要互斥锁定”

答案 3 :(得分:0)

带有volatile的MyIntegerHolder是线程安全的。但是如果要进行并发程序,AtomicInteger是首选,因为它还提供了大量的原子操作。

  

考虑以下事件序列:

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

    遵循Java语言规范

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

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

         

    我怀疑线程C可能会读取7或5.我认为该类与   volatile关键字不是线程安全的

你就在这里“1”发生 - 在“3”和“2”发生之前 - 在“3”之前。 “1”在“2”之前不会发生,但并不意味着它不是线程安全的。问题是你提供的例子含糊不清。如果你说“将值设置为5”,“将值设置为7”,“读取值”按顺序发生,则可以始终读取值7.并且将它们放在不同的线程中是无稽之谈。但是如果你说3个线程没有序列同时执行,你甚至可以获得值0,因为“读取值”可能首先发生。但这对于Thread-safe来说并不算什么,没有3个动作的命令。

答案 4 :(得分:-1)

这个问题对我来说并不容易,因为我(错误地)认为知道关于的所有事情 - 在关系之前发生了对Java内存模型的全面理解 - 以及易失性

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

上述文件中最相关的部分是“7.3 Well-Formed Executions”部分。

Java内存模型保证程序的所有执行都是格式正确的。执行格式正确仅在

时执行
  • Obeys 发生在一致性
  • 之前
  • Obeys 同步订单一致性
  • ... (其他一些条件也必须如此)

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

带有易失性的MyIntegerHolder是线程安全的,但它的安全性来自同步 - 订单一致性

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