原子/ 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 }
}

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

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

158042 次浏览

你特别问他们如何内部工作,所以你在这里:

没有同步

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;
}

volatile添加到stopped变量中,它可以正常工作-如果任何其他线程通过pleaseStop()方法修改了stopped变量,则保证在工作线程的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实际上没有任何影响。

即使你使用了final变量(或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;
}
}

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

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

private Integer i = 0;


synchronized(i) {
i++;
}

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

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

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

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

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

原子类使用volatile 字段。字段没有区别。区别在于所执行的操作。Atomic类使用CompareAndSwap或CAS操作。

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

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

这只在共享内存且至少有一个线程更新内存时才会出现问题。

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

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

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

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

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

齿顶高 (2016年4月)

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

齿顶高 (2016年7月)

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

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

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

volatile +同步是一个完全原子化的操作(语句)的可靠解决方案,它包含了对CPU的多条指令。

说eg:volatile int = 2;i++,也就是I = I + 1;这使得I在执行这条语句后在内存中的值为3。 这包括从内存中读取i的现有值(为2),加载到CPU累加器寄存器中,并通过对现有值加1(累加器中2 + 1 = 3)来进行计算,然后将增加的值写回内存。这些操作不够原子化,尽管i的值是易变的。i是volatile只保证从内存中读/写的SINGLE是原子的,而不是MULTIPLE。因此,我们需要在i++周围也有synchronized,以保持它是防傻瓜的原子语句。记住一个语句包含多个语句这一事实

希望这个解释足够清楚。

< em >挥发性:< / em >

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

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

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

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

< em > AtomicXXX: < / em >

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

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

同步:< em > < / em >

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

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

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

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

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

相关的SE问题:

Java中volatile和synchronized的区别 .

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

Java 挥发性修饰符是一种特殊机制的例子,它保证线程之间发生通信。当一个线程写入一个易失性变量,而另一个线程看到这个写入,第一个线程就会告诉第二个线程关于内存中的所有内容,直到它执行对该易失性变量的写入。

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

同步Vs原子Vs Volatile:

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

如有遗漏,请指正。