atomic/volatile/synchronized有什么区别?

har*_*dik 282 java multithreading synchronization atomic volatile

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

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

代码1

private int counter;

public int getNextUniqueIndex() {
    return counter++; 
}
Run Code Online (Sandbox Code Playgroud)

代码2

private AtomicInteger counter;

public int getNextUniqueIndex() {
    return counter.getAndIncrement();
}
Run Code Online (Sandbox Code Playgroud)

代码3

private volatile int counter;

public int getNextUniqueIndex() {
    return counter++; 
}
Run Code Online (Sandbox Code Playgroud)

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

volatile int i = 0;
void incIBy5() {
    i += 5;
}
Run Code Online (Sandbox Code Playgroud)

相当于

Integer i = 5;
void incIBy5() {
    int temp;
    synchronized(i) { temp = i }
    synchronized(i) { i = temp + 5 }
}
Run Code Online (Sandbox Code Playgroud)

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

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

Tom*_*icz 367

你特别询问他们的内部工作方式,所以你在这里:

没有同步

private int counter;

public int getNextUniqueIndex() {
  return counter++; 
}
Run Code Online (Sandbox Code Playgroud)

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

private boolean stopped;

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

public void pleaseStop() {
    stopped = true;
}
Run Code Online (Sandbox Code Playgroud)

添加volatilestopped变量,它工作正常 - 如果任何其他线程stopped通过pleaseStop()方法修改变量,您可以保证在工作线程的while(!stopped)循环中立即看到更改.顺便说一句,这不是一个中断线程的好方法,请参阅:如何停止一个没有任何使用的永远运行的线程停止一个特定的java线程.

AtomicInteger

private AtomicInteger counter = new AtomicInteger();

public int getNextUniqueIndex() {
  return counter.getAndIncrement();
}
Run Code Online (Sandbox Code Playgroud)

AtomicInteger类使用CAS(比较并交换)低级别的CPU操作(没有同步需要!),他们让你修改只有当现值等于其他的东西(并成功返回)一个特定的变量.所以当你执行getAndIncrement()它时实际上是在循环中运行(简化的实际实现):

int current;
do {
  current = get();
} while(!compareAndSet(current, current + 1));
Run Code Online (Sandbox Code Playgroud)

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

volatile 没有同步

private volatile int counter;

public int getNextUniqueIndex() {
  return counter++; 
}
Run Code Online (Sandbox Code Playgroud)

此代码不正确.它修复了可见性问题(volatile确保其他线程可以看到更改counter),但仍然存在竞争条件.这已被多次解释:前/后递增不是原子的.

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

volatile 没有同步(2)

volatile int i = 0;
void incIBy5() {
  i += 5;
}
Run Code Online (Sandbox Code Playgroud)

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

多个独立 synchronized

void incIBy5() {
  int temp;
  synchronized(i) { temp = i }
  synchronized(i) { i = temp + 5 }
}
Run Code Online (Sandbox Code Playgroud)

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

synchronized(new Object()) {
  //thread-safe, SRSLy?
}
Run Code Online (Sandbox Code Playgroud)

没有两个线程可以使用相同的锁进入同一个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;
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 我唯一要补充的是JVM将变量值复制到寄存器中以对它们进行操作.这意味着在单个CPU /内核上运行的线程仍然可以看到非易失性变量的不同值. (9认同)
  • @Hardik:请创建另一个问题以获得更多关于您的要求的回复,这里只是您和我,并且评论不适合提问.不要忘记在这里发布一个新问题的链接,以便我可以跟进. (3认同)

Dav*_*ble 55

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

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

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

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

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

附录 (2016年4月)

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

附录 (2016年7月)

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

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

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


Rav*_*abu 20

挥发性:

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

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

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

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

AtomicXXX:

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

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

同步:

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

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

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

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

AtomicXXXvolatile + 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认同)

Pet*_*rey 5

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

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

private Integer i = 0;

synchronized(i) {
   i++;
}
Run Code Online (Sandbox Code Playgroud)

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

如果这是真的比这个atomic.incrementAndGet()如何工作没有Synchronize?并且线程安全?

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

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

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

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

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

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

这只是在共享内存时至少有一个线程更新它的问题.