原子/易失性/同步有什么区别?无同步AtomicInteger volatile无同步 volatile无同步 (2)多个独立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 }
}

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

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


答案 1

您专门询问了它们在内部的工作方式,因此您在这里:

无同步

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

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

AtomicInteger

private AtomicInteger counter = new AtomicInteger();

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

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

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

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

volatile无同步

private volatile int counter;

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

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

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

volatile无同步 (2)

volatile int i = 0;
void incIBy5() {
  i += 5;
}

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

多个独立synchronized

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

令人惊讶的是,此代码也不正确。事实上,这是完全错误的。首先,您正在同步,即将更改(此外,它是一个原语,所以我猜您正在同步通过自动装箱创建的临时...完全没有缺陷。你也可以写:iiInteger

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

没有两个线程可以进入具有相同锁的同一块。在这种情况下(在您的代码中类似),锁对象在每次执行时都会更改,因此实际上没有任何效果。synchronizedsynchronized

即使使用了最终变量 (or) 进行同步,代码仍然不正确。两个线程可以首先同步读取(在 本地具有相同的值),然后第一个线程分配一个新值(例如,从1到6),另一个线程做同样的事情(从1到6)。thisitemptempi

同步必须跨越从读取到分配值。第一次同步没有影响(读取 a 是原子的),第二次同步也是如此。在我看来,这些是正确的形式:int

void synchronized incIBy5() {
  i += 5 
}

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

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

答案 2

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

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

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

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

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

附录(2016年4月)

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

附录(2016年7月)

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

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

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