原子的/易失的/同步的有什么区别?

x0fgdtte  于 2021-06-29  发布在  Java
关注(0)|答案(4)|浏览(348)

原子/易失性/同步如何在内部工作?
以下代码块之间有什么区别?
代码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 ? 它是线程安全的吗?
内部读写易失性变量/原子变量有什么区别?我在一些文章中读到线程有一个变量的本地副本-那是什么?

pes8fvy9

pes8fvy91#

不稳定的: volatile 是一个关键字。 volatile 强制所有线程从主内存而不是缓存获取变量的最新值。访问可变变量不需要锁定。所有线程都可以同时访问volatile变量值。
使用 volatile 变量降低了内存一致性错误的风险,因为对易失性变量的任何写入都会与该变量的后续读取建立“先发生后发生”的关系。
这意味着 volatile 变量始终对其他线程可见。更重要的是,它还意味着当线程读取 volatile 变量,它不仅可以看到volatile的最新更改,还可以看到导致更改的代码的副作用。
何时使用:一个线程修改数据,其他线程必须读取数据的最新值。其他线程将执行一些操作,但不会更新数据。
原子XXX: AtomicXXX 类支持对单变量进行无锁线程安全编程。这些 AtomicXXX 类(如 AtomicInteger )解决在多个线程中访问的易失性变量的修改引起的内存不一致错误/副作用。
何时使用:多个线程可以读取和修改数据。
已同步: synchronized 用于保护方法或代码块的关键字。使方法同步有两种效果:
首先,不可能两次调用 synchronized 方法对同一对象进行交错。当一个线程正在执行 synchronized 方法,所有其他调用 synchronized 方法,直到对象的第一个线程完成为止。
第二,当 synchronized 方法退出时,它会自动与对的任何后续调用建立“发生在”关系 synchronized 方法。这保证了对对象状态的更改对所有线程都是可见的。
何时使用:多个线程可以读取和修改数据。您的业务逻辑不仅更新数据,而且执行原子操作 AtomicXXX 相当于 volatile + synchronized 尽管实现方式不同。 AmtomicXXX 延伸 volatile 变量+ compareAndSet 方法,但不使用同步。
相关se问题:
java中volatile和synchronized的区别
volatile boolean与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

dwthyt8l

dwthyt8l2#

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

private Integer i = 0;

synchronized(i) {
   i++;
}

这不会像预期的那样表现,因为它每次都可能锁定不同的对象。
如果这是真的,那么这个atomic.incrementandget()在没有同步的情况下是如何工作的??线程安全吗??
对。它不使用锁定

ymzxtsji

ymzxtsji3#

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

无同步

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

添加 volatilestopped 如果有其他线程修改 stopped 可变通孔 pleaseStop() 方法,则可以保证在工作线程的 while(!stopped) 循环。顺便说一句,这也不是中断线程的好方法,请参阅:如何停止一个永远运行的线程而不使用它,以及停止一个特定的java线程。

原子整数

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() 在本机代码(程序集)中实现。

不同步的易失性

private volatile int counter;

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

此代码不正确。它解决了可见性问题( volatile 确保其他线程可以看到对所做的更改 counter )但仍然有比赛条件。这已经被多次解释过了:前/后增量不是原子的。
唯一的副作用 volatile 正在“刷新”缓存,以便所有其他方都能看到最新版本的数据。这在大多数情况下过于严格;这就是为什么 volatile 不是默认值。

不同步的易失性(2)

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

同样的问题,但更糟的是因为 i 不是 private . 比赛条件仍然存在。为什么会有问题?例如,如果两个线程同时运行此代码,则输出可能是 + 5 或者 + 10 . 但是,你肯定会看到变化。

多个独立同步

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

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

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

没有两个线程可以进入同一个线程 synchronized 用同一把锁堵住。在这种情况下(在代码中也是如此),lock对象在每次执行时都会改变,所以 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;
  }
}
yacmzcpb

yacmzcpb4#

将变量声明为volatile意味着修改其值会立即影响变量的实际内存存储。编译器无法优化掉对变量的任何引用。这保证了当一个线程修改变量时,所有其他线程都会立即看到新值(对于非易失性变量,这是不能保证的。)
声明一个原子变量可以保证对该变量所做的操作以原子方式发生,即操作的所有子步骤都在执行它们的线程内完成,并且不会被其他线程中断。例如,增量和测试操作要求对变量进行增量,然后与另一个值进行比较;原子操作保证了这两个步骤都将被完成,就像它们是一个不可分割/不可中断的操作一样。
同步对变量的所有访问一次只允许一个线程访问该变量,并强制所有其他线程等待该访问线程释放对该变量的访问。
同步访问与原子访问类似,但原子操作通常在较低级别的编程中实现。此外,完全可以只同步对变量的某些访问,而允许取消对其他访问的同步(例如,同步对变量的所有写入,但不同步对变量的任何读取)。
原子性、同步性和波动性是独立的属性,但通常结合使用以强制执行适当的线程协作以访问变量。
附录(2016年4月)
对变量的同步访问通常使用监视器或信号量来实现。这些是低级互斥(mutex,互斥)机制,允许线程以独占方式获取对变量或代码块的控制,如果其他线程也尝试获取相同的互斥,则强制所有线程等待。一旦拥有的线程释放了互斥锁,另一个线程就可以依次获取互斥锁。
附录(2016年7月)
同步发生在对象上。这意味着调用类的同步方法将锁定 this 调用的对象。静态同步方法将锁定 Class 对象本身。
同样,输入同步块需要锁定 this 方法的对象。
这意味着,如果同步方法(或块)锁定在不同的对象上,则它们可以同时在多个线程中执行,但对于任何给定的单个对象,一次只能有一个线程执行同步方法(或块)。

相关问题