首页 文章

atomic / volatile / synchronized有什么区别?

提问于
浏览
243

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

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

代码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 的情况下工作?它是线程安全的吗?

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

7 回答

  • 329

    易失性同步是一种简单的解决方案,可以使操作(语句)完全原子化,包括对CPU的多条指令 .

    比如说:volatile int i = 2;我,这只不过是i = i 1;这使得i在执行此语句后作为内存中的值3 . 这包括从i的内存中读取现有值(即2),加载到CPU累加器寄存器中并通过将现有值递增1(累加器中为2 1 = 3)来进行计算,然后将该递增值写回记忆 . 这些操作不够原子,尽管i的值是不稳定的 . 我是volatile,只保证来自内存的SINGLE读/写是原子的而不是MULTIPLE . 因此,我们需要在i周围同步,以保持它是傻瓜式的原子声明 . 请记住,语句包含多个语句 .

    希望解释清楚 .

  • 14

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

    没有同步

    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) 循环中立即看到该更改 . 顺便说一句,这不是中断线程的好方法,请参阅:How to stop a thread that is running forever without any useStopping a specific java thread .

    AtomicInteger

    private AtomicInteger counter = new AtomicInteger();
    
    public int getNextUniqueIndex() {
      return counter.getAndIncrement();
    }
    

    AtomicInteger 类使用CAS(compare-and-swap)低级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 所做的更改)但仍然存在竞争条件 . 这已多次explained:前/后增量不是原子的 .

    volatile 的唯一副作用是“刷新”缓存,以便所有其他方看到最新版本的数据 . 在大多数情况下,这太严格了;这就是为什么 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?
    }
    

    没有两个线程可以输入相同的 synchronizedwith the same 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;
      }
    }
    
  • 44

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

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

    private Integer i = 0;
    
    synchronized(i) {
       i++;
    }
    

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

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

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

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

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

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

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

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

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

  • 0

    Java volatile 修饰符是保证线程之间进行通信的特殊机制的示例 . 当一个线程写入一个volatile变量,而另一个线程看到该写入时,第一个线程告诉第二个线程关于内存的所有内容,直到它执行对该volatile变量的写入 .

    Atomic operations 在单个任务单元中执行,不受其他操作的干扰 . 在多线程环境中必须进行原子操作以避免数据不一致 .

  • 1

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

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

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

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

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

    Addendum (2016年4月)

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

    Addendum (2016年7月)

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

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

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

  • 2

    volatile:

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

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

    这意味着对其他线程始终可以看到对 volatile 变量的更改 . 更重要的是,它也意味着 when a thread reads a volatile variable, it sees not just the latest change to the volatile, but also the side effects of the code that led up the change .

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

    AtomicXXX:

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

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

    synchronized:

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

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

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

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

    AtomicXXX 相当于 volatile + synchronized ,即使实现方式不同 . AmtomicXXX 扩展 volatile 变量 compareAndSet 方法但不使用同步 .

    相关的SE问题:

    Difference between volatile and synchronized in Java

    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

  • 5

    Synchronized Vs Atomic Vs Volatile:
    1. Volatile和Atomic仅适用于变量,而同步应用于方法 .
    2. 挥发性确保可见性而不是对象的原子性/一致性,而其他两者都确保可见性和原子性 .
    3. RAM中的易失性变量存储,访问速度更快,但我们无法实现线程安全或同步whitout synchronized关键字 .
    4. 同步实现为同步块或同步方法,而两者都没有 . 我们可以通过synchronized关键字帮助线程安全多行代码,而两者都无法实现相同 .
    5. Synchronized可以锁定相同的类对象或不同的类对象,而两者都不能 .
    Please correct me if anything i missed.

相关问题