atomic / volatile / synchronized有什么区别?


297

原子/易失性/同步在内部如何工作?

以下代码块之间有什么区别?

代码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 }
}

我认为两个线程不能同时进入同步块...对吗?如果这是真的,那么atomic.incrementAndGet()没有它synchronized怎么办?而且它是线程安全的吗?

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


5
这就产生了很多问题,甚至没有编译的代码。也许您应该读一本好书,例如《 Java Concurrency in Practice》。
JB Nizet 2012年

4
@JBNizet你是对的!我有那本书,它没有简短的原子概念,我也没有得到一些概念。当然是我的错,不是作者的错。
hardik 2012年

4
您实际上不必关心它的实现方式(并且随操作系统的不同而不同)。您需要了解的是合约:该值以原子方式递增,并且保证所有其他线程都可以看到新值。
JB Nizet 2012年

Answers:


392

您是在专门询问它们在内部如何工作,因此这里是:

没有同步

private int counter;

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

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

private boolean stopped;

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

public void pleaseStop() {
    stopped = true;
}

添加volatilestopped变量,它可以正常工作-如果任何其他线程stopped通过pleaseStop()方法修改了变量,则可以确保在工作线程的while(!stopped)循环中立即看到该更改。顺便说一句,这也不是中断线程的好方法,请参见:如何停止永远不运行的线程以及停止特定的Java线程

AtomicInteger

private AtomicInteger counter = new AtomicInteger();

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

AtomicInteger类使用CAS(比较并交换)低级别的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),但仍然存在竞争状况。这已被多次解释:增量前/后增量不是原子的。

唯一的副作用volatile是“ 刷新 ”缓存,以便所有其他方都能看到最新版本的数据。在大多数情况下,这太严格了;这就是为什么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)进行同步,代码仍然不正确。两个线程可以首先i进行temp同步读取(在中具有本地相同的值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;
  }
}

10
我唯一要添加的是JVM将变量值复制到寄存器中以对其进行操作。这意味着在单个CPU /内核上运行的线程仍然可以看到非易失性变量的不同值。
David Harkness 2012年

@thomasz:是compareAndSet(current,current + 1)同步了吗?如果没有,当两个线程同时执行此方法时会发生什么?
hardik

@Hardik:compareAndSet只是CAS操作的一个薄包装。我在回答中详细介绍了一些内容。
Tomasz Nurkiewicz 2012年

1
@thomsasz:好的,我经历了这个链接问题,并由jon skeet回答,他说:“如果不检查是否有其他线程执行过写操作,则线程无法读取volatile变量。” 但是如果一个线程介于写入操作和第二个线程之间进行读取会发生什么!我错了吗 ??这不是原子操作的竞争条件吗?
hardik

3
@Hardik:请创建另一个问题,以获取更多关于您所问问题的答复,这里只有您和我,评论不适合提出问题。不要忘记在此处发布指向新问题的链接,以便我跟进。
Tomasz Nurkiewicz 2012年

61

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

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

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

同步访问类似于原子访问,但是原子操作通常在较低的编程级别上实现。而且,完全可能仅同步对变量的某些访问,并允许其他访问不同步(例如,将所有写入同步到变量,但不读取任何变量)。

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

附录 (2016年4月)

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

附录 (2016年7月)

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

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

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


25

易挥发的:

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

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

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

使用时间:一个线程修改数据,而其他线程则必须读取数据的最新值。其他线程会采取一些措施,但是他们不会更新数据

原子XXX:

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

何时使用:多个线程可以读取和修改数据。

已同步:

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

  1. 首先,不可能synchronized在同一个对象上对方法的两次调用进行交织。当一个线程正在执行synchronized对象的方法时,所有其他调用synchronized同一对象的方法的线程阻塞(挂起执行),直到第一个线程用该对象完成。

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

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

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

相关的SE问题:

Java中的volatile和Synchronized之间的区别

挥发性布尔值与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


2
这是实际上提到所描述的关键字/功能的语义之前发生的第一个答案,这对于理解它们实际上如何影响代码执行非常重要。投票率较高的答案错过了这一方面。
jhyot19年

5

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

两个线程不能两次在同一对象上输入同步块。这意味着两个线程可以在不同对象上输入相同的块。这种混乱可能导致这样的代码。

private Integer i = 0;

synchronized(i) {
   i++;
}

这不会像预期的那样表现每次都可能锁定在不同的对象上。

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

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

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

内部读写挥发性变量/原子变量之间有什么区别?

原子类使用易失性字段。该领域没有区别。不同之处在于执行的操作。原子类使用CompareAndSwap或CAS操作。

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

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

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


@Aniket Thakur您确定吗?整数是不可变的。因此,i ++可能会自动将int值拆箱,将其递增,然后创建一个新的Integer,该实例与以前的实例不同。尝试使我成为final,在调用i ++时会出现编译器错误。
fuemf5 2014年

2

同步VS原子VS挥发性:

  • 挥发和原子仅适用于变量,而同步适用于方法。
  • 挥发物确保可见性而不是对象的原子性/一致性,而其他两者均确保可见性和原子性。
  • 易变的变量存储在RAM中,并且访问速度更快,但是我们无法达到线程安全性或没有同步的synch关键字。
  • 同步实现为同步块或同步方法,而两者都没有。我们可以在同步关键字的帮助下对多行安全的代码进行线程化,而两者都无法实现相同的目标。
  • 同步可以锁定相同的类对象或不同的类对象,而两者都不能。

如果我错过任何事情,请纠正我。


1

易失性+同步是一种愚蠢的解决方案,可确保操作(语句)完全原子化,其中包括到CPU的多个指令。

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

希望解释足够清楚。


1

Java volatile修饰符是一种特殊机制的示例,可以确保线程之间进行通信。当一个线程写一个易失性变量,而另一个线程看到该写操作时,第一个线程向第二个线程告知内存的所有内容,直到它执行对该易失性变量的写操作为止。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.