atomic / volatile / synchronized有什么区别?

时间:2012-03-17 11:46:41

标签: java multithreading synchronization atomic volatile

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

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

代码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的情况下工作?它是线程安全的吗?

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

7 个答案:

答案 0 :(得分:354)

您具体询问他们内部如何工作,所以在这里:

无同步

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变量并且工作正常 - 如果任何其他线程通过stopped方法修改pleaseStop()变量,您可以确保在工作中立即看到更改线程的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的唯一副作用是“刷新”缓存,以便所有其他方查看最新版本的数据。在大多数情况下,这太严格了;这就是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到{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;
  }
}

答案 1 :(得分:51)

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

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

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

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

原子性,同步性和波动性是独立属性,但通常组合使用以强制执行正确的线程协作以访问变量。

附录 (2016年4月)

通常使用 monitor semaphore 实现对变量的同步访问。这些是低级互斥(互斥)机制,允许线程独占地获取对变量或代码块的控制,强制所有其他线程在尝试获取相同的互斥锁时等待。一旦拥有线程释放互斥锁,另一个线程就可以依次获取互斥锁。

附录 (2016年7月)

对象上进行同步。这意味着调用类的synchronized方法将锁定调用的this对象。静态同步方法将锁定Class对象本身。

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

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

答案 2 :(得分:16)

<强> 易失性:

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

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

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

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

<强> AtomicXXX:

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

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

<强> 同步:

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

  1. 首先,对同一对象的两个synchronized方法的调用不可能进行交错。当一个线程正在为一个对象执行synchronized方法时,所有其他线程都会为同一个对象阻塞synchronized方法(暂停执行),直到第一个线程完成该对象为止。

  2. 其次,当synchronized方法退出时,它会自动为同一对象的synchronized方法的任何后续调用建立一个before-before关系。这可以保证对所有线程都可以看到对象状态的更改。

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

    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

答案 3 :(得分:5)

  

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

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

private Integer i = 0;

synchronized(i) {
   i++;
}

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

  

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

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

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

  

内部读取和写入易失性变量/原子变量之间的区别是什么?

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

  

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

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

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

答案 4 :(得分:1)

volatile +同步是一个简单的解决方案,可以使操作(语句)完全原子化,包括对CPU的多条指令。

比如说:volatile int i = 2; i ++,这只不过是i = i + 1;这使得我在执行此语句后作为内存中的值3。 这包括从内存中读取i(即2)的现有值,加载到CPU累加器寄存器中,并通过将现有值递增1(累加器中的2 + 1 = 3)来进行计算,然后写回递增的值回到记忆中。这些操作不够原子,尽管i的值是不稳定的。我是volatile只保证来自内存的SINGLE读/写是原子的而不是MULTIPLE。因此,我们需要围绕i ++进行同步,以使其成为傻瓜式的原子语句。请记住,语句包含多个语句。

希望解释清楚。

答案 5 :(得分:1)

同步Vs与原子Vs易失:
1。。挥发性和原子性仅适用于变量,而同步化适用于方法。
2。易失性确保对象的可见性而不是原子性/一致性,而其他两者都确保可见性和原子性。
3。。易变的变量存储在RAM中,访问速度更快,但我们无法达到线程安全或同步主题的“ synchronized”。
4。同步实现为同步块或同步方法,但两者均未实现。我们可以在同步关键字的帮助下对安全的多行代码进行线程化,而两者都无法实现相同的效果。
5。。同步可以锁定相同的类对象或不同的类对象,而两者都不能。
如果我错过了任何内容,请纠正我。

答案 6 :(得分:0)

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

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