【问题标题】:What is the difference between atomic / volatile / synchronized?原子/易失性/同步之间有什么区别?
【发布时间】:2012-04-02 17:35:12
【问题描述】:

atomic / volatile / synchronized 在内部如何工作?

以下代码块有什么区别?

代码 1

private int counter;

public int getNextUniqueIndex() {
    return counter++; 
}

代码 2

private AtomicInteger counter;

public int getNextUniqueIndex() {
    return counter.getAndIncrement();
}

代码 3

private volatile int counter;

public int getNextUniqueIndex() {
    return counter++; 
}

volatile 是否以下列方式工作?是

volatile int i = 0;
void incIBy5() {
    i += 5;
}

相当于

Integer i = 5;
void incIBy5() {
    int temp;
    synchronized(i) { temp = i }
    synchronized(i) { i = temp + 5 }
}

我认为两个线程不能同时进入一个同步块......我说的对吗?如果这是真的,那么在没有synchronized 的情况下,atomic.incrementAndGet() 如何工作?它是线程安全的吗?

内部读取和写入易失性变量/原子变量有什么区别?我在一些文章中读到该线程具有变量的本地副本 - 那是什么?

【问题讨论】:

  • @JBNizet 你是对的!!!我有那本书,它没有简单的原子概念,我没有得到一些概念。诅咒是我的错,不是作者的错。
  • 您不必真正关心它是如何实现的(并且它因操作系统而异)。你要理解的是契约:值是原子递增的,保证所有其他线程都能看到新的值。

标签: java multithreading synchronization atomic volatile


【解决方案1】:

您是专门询问他们在内部是如何工作的,所以您在这里:

不同步

private int counter;

public int getNextUniqueIndex() {
  return counter++; 
}

它基本上从内存中读取值,将其递增并放回内存。这适用于单线程,但现在,在多核、多 CPU、多级缓存的时代,它无法正常工作。首先它引入了竞争条件(多个线程可以同时读取该值),但也引入了可见性问题。该值可能仅存储在“local”CPU 内存(某些缓存)中,而对其他 CPU/核心(因此 - 线程)不可见。这就是为什么许多人在线程中引用变量的本地副本。这是非常不安全的。考虑一下这个流行但损坏的线程停止代码:

private boolean stopped;

public void run() {
    while(!stopped) {
        //do some work
    }
}

public void pleaseStop() {
    stopped = true;
}

volatile 添加到stopped 变量,它工作正常 - 如果任何其他线程通过pleaseStop() 方法修改stopped 变量,您可以保证在工作线程的while(!stopped) 循环中立即看到该更改。顺便说一句,这也不是中断线程的好方法,请参阅:How to stop a thread that is running forever without any useStopping a specific java thread

AtomicInteger

private AtomicInteger counter = new AtomicInteger();

public int getNextUniqueIndex() {
  return counter.getAndIncrement();
}

AtomicInteger 类使用 CAS (compare-and-swap) 低级 CPU 操作(不需要同步!)它们允许您仅在当前值等于其他值时修改特定变量(并且成功返回) .所以当你执行getAndIncrement()时它实际上是在一个循环中运行(简化的实际实现):

int current;
do {
  current = get();
} while(!compareAndSet(current, current + 1));

所以基本上:阅读;尝试存储增量值;如果不成功(值不再等于current),请阅读并重试。 compareAndSet() 以本机代码(程序集)实现。

volatile 没有同步

private volatile int counter;

public int getNextUniqueIndex() {
  return counter++; 
}

此代码不正确。它修复了可见性问题(volatile 确保其他线程可以看到对 counter 所做的更改)但仍然存在竞争条件。这已经explained 多次了:前/后增量不是原子的。

volatile 的唯一副作用是“flushing”缓存,以便所有其他方看到最新版本的数据。在大多数情况下,这太严格了;这就是为什么volatile 不是默认值的原因。

volatile不同步(2)

volatile int i = 0;
void incIBy5() {
  i += 5;
}

与上面的问题相同,但更糟糕的是因为i 不是private。竞争条件仍然存在。为什么会出现问题?例如,如果两个线程同时运行此代码,则输出可能是+ 5+ 10。但是,您一定会看到变化。

多个独立的synchronized

void incIBy5() {
  int temp;
  synchronized(i) { temp = i }
  synchronized(i) { i = temp + 5 }
}

惊奇,这个代码也是不正确的。事实上,这是完全错误的。首先,您正在同步i,它即将被更改(此外,i 是一个原语,所以我猜您正在同步通过自动装箱创建的临时Integer...)完全有缺陷。你也可以写:

synchronized(new Object()) {
  //thread-safe, SRSLy?
}

没有两个线程可以进入相同的synchronized具有相同的锁。在这种情况下(在您的代码中也是如此)锁定对象在每次执行时都会更改,因此synchronized 实际上没有任何效果。

即使您使用了最终变量(或this)进行同步,代码仍然不正确。两个线程可以先同步读取itemp(在temp 本地具有相同的值),然后第一个线程为i 分配一个新值(例如,从1 到6),另一个执行相同的东西(从 1 到 6)。

同步必须跨越从读取到赋值。您的第一次同步没有效果(读取int 是原子的),第二次也是如此。在我看来,这些是正确的形式:

void synchronized incIBy5() {
  i += 5 
}

void incIBy5() {
  synchronized(this) {
    i += 5 
  }
}

void incIBy5() {
  synchronized(this) {
    int temp = i;
    i = temp + 5;
  }
}

【讨论】:

  • 我唯一要补充的是 JVM 将变量值复制到寄存器中以对其进行操作。这意味着在单个 CPU/内核上运行的线程仍然可以看到非易失性变量的不同值。
  • @thomasz: compareAndSet(current, current + 1) 是否同步?如果不是,当两个线程同时执行此方法时会发生什么??
  • @Hardik: compareAndSet 只是 CAS 操作的一个薄包装。我在回答中详细介绍了一些细节。
  • @thomsasz:好的,我完成了这个link 问题并由 jon skeet 回答,他说“如果不检查是否有任何其他线程执行了写入,线程就无法读取 volatile 变量。”但是如果一个线程介于写入操作和第二个线程读取它之间会发生什么!我错了吗 ??这不是原子操作的竞争条件吗??
  • @Hardik:请创建另一个问题以获得有关您所问内容的更多回复,这里只有您和我,而 cmets 不适合提问。不要忘记在此处发布新问题的链接,以便我跟进。
【解决方案2】:

将变量声明为volatile意味着修改其值会立即影响变量的实际内存存储。编译器无法优化对变量所做的任何引用。这保证了当一个线程修改变量时,所有其他线程立即看到新值。 (对于非易失性变量,这是不保证的。)

声明一个原子变量保证了对变量进行的操作以原子方式发生,即操作的所有子步骤都在它们执行的线程内完成,并且不会被中断其他线程。例如,增量和测试操作要求变量递增,然后与另一个值进行比较;原子操作保证这两个步骤都将像单个不可分割/不可中断的操作一样完成。

同步对变量的所有访问一次只允许一个线程访问该变量,并强制所有其他线程等待该访问线程释放其对变量的访问。

同步访问类似于原子访问,但原子操作一般是在较低的编程层次上实现的。此外,完全可以只同步对变量的某些访问,而允许其他访问不同步(例如,同步对变量的所有写入,但不同步对变量的读取)。

原子性、同步性和易变性是独立的属性,但通常结合使用以强制执行适当的线程协作以访问变量。

附录 (2016 年 4 月)

对变量的同步访问通常使用 monitorsemaphore 来实现。这些是低级别的mutex(互斥)机制,允许线程独占地获取对变量或代码块的控制,如果所有其他线程也尝试获取相同的互斥体,则它们会强制等待。一旦拥有线程释放互斥锁,另一个线程可以依次获取互斥锁。

附录 (2016 年 7 月)

同步发生在一个对象上。这意味着调用类的同步方法将锁定调用的this 对象。静态同步方法将锁定 Class 对象本身。

同样,进入同步块需要锁定方法的this 对象。

这意味着一个同步方法(或块)可以同时在多个线程中执行,如果它们锁定在不同的对象上,但只有一个线程可以执行一个同步方法(或块) ) 对于任何给定的单个对象。

【讨论】:

    【解决方案3】:

    易失性:

    volatile 是一个关键字。 volatile 强制所有线程从主内存而不是缓存中获取变量的最新值。访问 volatile 变量不需要锁定。所有线程可以同时访问 volatile 变量值。

    使用volatile 变量可降低内存一致性错误的风险,因为对 volatile 变量的任何写入都会与后续读取同一变量建立起先发生关系。

    这意味着对volatile 变量的更改始终对其他线程可见。更重要的是,这也意味着当一个线程读取volatile 变量时,它不仅会看到 volatile 的最新更改,还会看到导致更改的代码的副作用

    何时使用:一个线程修改数据,其他线程必须读取数据的最新值。其他线程会采取一些行动,但不会更新数据

    AtomicXXX:

    AtomicXXX 类支持对单个变量进行无锁线程安全编程。这些AtomicXXX 类(如AtomicInteger)解决了内存不一致错误/修改易失性变量的副作用,这些变量已在多个线程中访问。

    什么时候使用:多线程可以读取和修改数据。

    同步:

    synchronized 是用于保护方法或代码块的关键字。通过使方法同步有两个效果:

    1. 首先,在同一对象上两次调用synchronized 方法是不可能的。当一个线程正在为一个对象执行synchronized 方法时,所有其他为同一对象块调用synchronized 方法的线程(暂停执行)直到第一个线程处理完该对象。

    2. 其次,当synchronized 方法退出时,它会自动与任何后续对同一对象的synchronized 方法调用建立起之前的关系。这保证了对象状态的更改对所有线程都是可见的。

    何时使用:多个线程可以读取和修改数据。您的业​​务逻辑不仅会更新数据,还会执行原子操作

    AtomicXXX 等效于 volatile + synchronized,即使实现不同。 AmtomicXXX 扩展 volatile 变量 + compareAndSet 方法,但不使用同步。

    相关的 SE 问题:

    Difference between volatile and synchronized in Java

    Volatile boolean vs AtomicBoolean

    值得阅读的好文章:(以上内容取自这些文档页面)

    https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html

    https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html

    https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/package-summary.html

    【讨论】:

    • 这是第一个真正提到所描述关键字/功能的发生前语义的答案,这对于理解它们如何实际影响代码执行非常重要。投票较高的答案错过了这方面。
    【解决方案4】:

    我知道两个线程不能同时进入Synchronize块

    两个线程不能两次进入同一个对象的同步块。这意味着两个线程可以进入不同对象的同一个块。这种混淆可能会导致这样的代码。

    private Integer i = 0;
    
    synchronized(i) {
       i++;
    }
    

    这不会像预期的那样运行,因为它可能每次都锁定在不同的对象上。

    如果这是真的,那么这个 atomic.incrementAndGet() 如何在没有 Synchronize 的情况下工作?线程安全吗??

    是的。它不使用锁定来实现线程安全。

    如果您想更详细地了解它们的工作原理,可以阅读它们的代码。

    内部读写易失变量/原子变量有什么区别??

    Atomic 类使用 volatile 字段。 字段没有区别。不同之处在于执行的操作。 Atomic 类使用 CompareAndSwap 或 CAS 操作。

    我在一些文章中读到线程有变量的本地副本那是什么??

    我只能假设它指的是每个 CPU 都有自己的内存缓存视图,这可能与其他所有 CPU 不同。为了确保您的 CPU 具有一致的数据视图,您需要使用线程安全技术。

    只有当内存共享至少一个线程更新它时,这才是一个问题。

    【讨论】:

      【解决方案5】:

      同步 Vs Atomic Vs Volatile:

      • Volatile 和 Atomic 仅适用于 variable ,而 Synchronized 适用于方法。
      • volatile 确保对象的可见性而不是原子性/一致性,而其他都确保可见性和原子性。
      • 易失性变量存储在 RAM 中,访问速度更快,但如果没有同步关键字,我们无法实现线程安全或同步。
      • 同步实现为同步块或同步方法,而两者都不是。我们可以在 synchronized 关键字的帮助下线程安全的多行代码,而两者都无法实现。
      • Synchronized 可以锁定同一个类对象或不同的类对象,而两者都不能。

      如果我遗漏了什么,请纠正我。

      【讨论】:

        【解决方案6】:

        易失性 + 同步是一种万无一失的解决方案,可让操作(语句)完全原子化,其中包括对 CPU 的多条指令。

        例如:volatile int i = 2; i++,就是 i = i + 1;这使得执行该语句后 i 在内存中的值为 3。 这包括从内存中读取 i 的现有值(即 2),加载到 CPU 累加器寄存器中并通过将现有值增加一个(累加器中的 2 + 1 = 3)来进行计算,然后写回增加的值回到记忆中。尽管 i 的值是易变的,但这些操作还不够原子。 i 是 volatile 只保证从内存中的 SINGLE 读/写是原子的,而不是 MULTIPLE。因此,我们还需要在 i++ 周围进行同步,以使其成为万无一失的原子语句。记住一个语句包含多个语句这一事实。

        希望解释足够清楚。

        【讨论】:

          【解决方案7】:

          Java volatile 修饰符是确保线程之间发生通信的特殊机制的一个示例。当一个线程写入 volatile 变量,而另一个线程看到该写入时,第一个线程会告诉第二个线程所有内存内容,直到它执行对该 volatile 变量的写入。

          原子操作在单个任务单元中执行,不受其他操作的干扰。原子操作在多线程环境中是必要的,以避免数据不一致。

          【讨论】:

            猜你喜欢
            • 2011-03-07
            • 2017-08-14
            • 1970-01-01
            • 2011-03-20
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多