关于Java:atomic / volatile / synchronized的区别是什么?

What is the difference between atomic / volatile / synchronized?

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

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

代码1

1
2
3
4
5
private int counter;

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

代码2

1
2
3
4
5
private AtomicInteger counter;

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

代码3

1
2
3
4
5
private volatile int counter;

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

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

1
2
3
4
volatile int i = 0;
void incIBy5() {
    i += 5;
}

相当于

1
2
3
4
5
6
Integer i = 5;
void incIBy5() {
    int temp;
    synchronized(i) { temp = i }
    synchronized(i) { i = temp + 5 }
}

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

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


您特别询问了它们的内部工作方式,下面是:

不同步

1
2
3
4
5
private int counter;

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

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

1
2
3
4
5
6
7
8
9
10
11
private boolean stopped;

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

public void pleaseStop() {
    stopped = true;
}

volatile添加到stopped变量中,它工作得很好——如果有任何其他线程通过pleaseStop()方法修改stopped变量,您一定能在工作线程的while(!stopped)循环中立即看到这种变化。顺便说一下,这不是中断线程的好方法,请参见:如何停止永远运行的线程,而不需要任何使用和停止特定的Java线程。

AtomicInteger

1
2
3
4
5
private AtomicInteger counter = new AtomicInteger();

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

AtomicInteger类使用cas(比较和交换)低级CPU操作(不需要同步!)它们只允许您在当前值等于其他值(并且成功返回)时修改特定变量。因此,当您执行getAndIncrement()时,它实际上是在一个循环中运行的(简化的实际实现):

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

因此,基本上是:读取;尝试存储递增值;如果不成功(该值不再等于current,则读取并重试。compareAndSet()是以本机代码(assembly)实现的。

volatile不同步

1
2
3
4
5
private volatile int counter;

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

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

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

不同步的volatile(2)

1
2
3
4
volatile int i = 0;
void incIBy5() {
  i += 5;
}

与上述问题相同,但更糟的是,因为i不是private。比赛条件仍然存在。为什么会有问题?如果两个线程同时运行这个代码,那么输出可能是+ 5+ 10。但是,您一定会看到变化。

多独立synchronized

1
2
3
4
5
void incIBy5() {
  int temp;
  synchronized(i) { temp = i }
  synchronized(i) { i = temp + 5 }
}

令人惊讶的是,这个代码也不正确。事实上,这是完全错误的。首先,你正在同步i,这将要被改变(而且,i是一个原始的,所以我猜你正在同步一个临时Integer,通过自动氧化……创建)完全有缺陷。你也可以写:

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

两个线程不能用同一个锁进入同一个synchronized块。在这种情况下(和代码中的情况类似),锁对象在每次执行时都会发生更改,因此synchronized实际上没有任何效果。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void synchronized incIBy5() {
  i += 5
}

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

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


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

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

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

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

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

附录(2016年4月)

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

附录(2016年7月)

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

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

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


不稳定的:

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

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

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

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

AtomicXXX:

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

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

同步的:

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

  • 首先,同一对象上的两个synchronized方法调用不可能交错进行。当一个线程为一个对象执行synchronized方法时,对同一个对象块(挂起执行)调用synchronized方法的所有其他线程,直到第一个线程完成对该对象的执行。

  • 第二,当synchronized方法退出时,它会自动建立与同一对象的synchronized方法的任何后续调用之间的"发生在前"关系。这保证了对对象状态的更改对所有线程都是可见的。

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

    AtomicXXX相当于volatile + synchronized,尽管实施方式不同。AmtomicXXX扩展了volatile变量+compareAndSet方法,但不使用同步。

    相关SE问题:

    Java中易失性与同步性的区别

    易变布尔与原子布尔

    阅读好文章:(以上内容摘自这些文档页)

    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


    I know that two threads can not enter in Synchronize block at the same time

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

    1
    2
    3
    4
    5
    private Integer i = 0;

    synchronized(i) {
       i++;
    }

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

    if this is true than How this atomic.incrementAndGet() works without Synchronize ?? and is thread safe ??

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

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

    And what is difference between internal reading and writing to Volatile Variable / Atomic Variable ??

    原子类使用易失性字段。这一领域没有什么不同。区别在于所执行的操作。原子类使用compareAndSwap或cas操作。

    i read in some article that thread has local copy of variables what is that ??

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

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


    同步vs原子vs易失性:1。volatile和atomic只应用于变量,而synchronized应用于方法。2。volatile确保对象的可见性而不是原子性/一致性,而其他volatile则确保对象的可见性和原子性。三。volatile变量存储在RAM中,访问速度更快,但是我们不能实现线程安全或同步,而忽略了synchronized关键字。4。同步实现为同步块或同步方法,但两者都不是。我们可以在同步关键字的帮助下安全地线程化多行代码,而同时使用这两种方法我们无法实现相同的结果。5。同步可以锁定相同的类对象或不同的类对象,但两者都不能。如果我漏了什么,请纠正我。


    volatile+同步对于一个操作(语句)来说是一个很简单的解决方案,该操作(语句)是完全原子的,其中包含多个到CPU的指令。

    例如:volatile int i=2;i++,它只不过是i=i+1;这使得我在执行此语句后在内存中成为值3。这包括从内存中读取i的现有值(即2),加载到CPU累加器寄存器,并通过使用一个(累加器中的2+1=3)增量现有值进行计算,然后将增量值写回内存。虽然i的值不稳定,但这些操作的原子性不够。易失性只保证从内存中读/写一次是原子的,而不是多次的。因此,我们还需要在i++周围进行同步,以使其成为防愚弄的原子语句。记住一个语句包含多个语句。

    希望解释清楚。


    Java易失性修改器是一种特殊机制的例子,以保证线程之间的通信发生。当一个线程写入一个易失性变量,而另一个线程看到该写入时,第一个线程将向第二个线程报告内存的所有内容,直到执行对该易失性变量的写入为止。

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