【问题标题】:Java Volatile ,synchronization,atomic exampleJava Volatile,同步,原子示例
【发布时间】:2018-05-31 15:38:35
【问题描述】:

您好,我正在阅读实践中的 java 并发,并且我阅读了有趣的声明指出

锁定可以同时保证可见性和原子性;易挥发的 变量只能保证可见性。

谁能解释一下,如果将一个变量声明为 volatile,所有其他读取线程都会获取更新的值,那么我为什么要关心语句中的原子性,例如:counter = counter + 1;

提前致谢。

【问题讨论】:

  • “我为什么要关心 counter = counter + 1; 这样的语句中的原子性”。好吧,如果您希望您的计数器具有正确的值,您应该关心。
  • @so_what : 如果对你有帮助,请标记答案
  • 最后一个困扰我的疑问是我应该认为原子变量=易失性(用于获取最近的写入值)+同步(以确保原子性)?
  • 如果我们没有原子类(在 jdk 1.5 之前就是这种情况),我们将不得不使用 volatile+synchronized。但是同步有性能开销。原子类为此目的更有效。见docs.oracle.com/javase/tutorial/essential/concurrency/…

标签: java multithreading


【解决方案1】:

volatile 关键字的效果大约是对该变量的每个单独的读取或写入操作都是原子的。

然而,值得注意的是,需要多次读/写的操作——例如 i++,它相当于 i = i + 1,它执行一次读和一次写——不是原子的,因为另一个线程可能在读取和写入之间写入 i。

Atomic 类,如 AtomicInteger 和 AtomicReference,以原子方式提供更广泛的操作,特别是包括 AtomicInteger 的增量。

这就是为什么你需要关心像 counter = counter + 1 这样的语句中的原子性

请查看此帖Volatile Vs Atomic

【讨论】:

  • 这个答案似乎说明了如何解决这些非原子操作引起的问题。但它并没有回答为什么它是一个问题开始的问题。也就是说,在两个线程中增加一个变量的情况下,如果两个线程在递增之前读取相同的值,则只能导致一个递增
  • "然而,值得注意的是,需要多次读/写的操作——例如 i++,它相当于 i = i + 1,它执行一次读和一次写——不是原子性,因为另一个线程可能会在读取和写入之间写入 i ”它没有回答为什么需要关心原子性的问题吗?在得出任何结论之前请仔细阅读@Cruncher
  • "another thread may write to i between the read and the write" 语句对我来说很有意义,因为我已经对此进行了演示,但是您能否考虑以下线程安全用例:1)易失性+原子变量(不是单独的)=完美 2)易失性+同步块=完美 3)单独的原子操作+同步=完美
  • @Aarish 这实际上只是重申了非原子的定义。如果我是线程安全的新手,我会对这实际上意味着什么有很多疑问。 为什么如果线程在读写之间读取会出现问题?
  • @Cruncher 如果线程在读取和写入之间写入而不是读取,则会出现问题
【解决方案2】:

这是一个独立的示例可执行应用程序,它证明了仅靠 volatile 是不够的。四个线程每个都将计数器递增 10,000 次,因此您希望计数器最终为 40,000。它使用一个原始 int 变量和一个 AtomicInt,并且每次尝试练习 5 次。

import java.util.Collections;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

class AtomicDemo {
    interface Demo extends Callable<Void> {
        int getCounter();
    }

    static class UsePrimitive implements Demo {
        private volatile int counter = 0;

        public Void call() throws Exception {
            for (int i = 1; i <= 10000; ++i) {
                ++counter;
            }
            return null;
        }

        public int getCounter() {
            return counter;
        }
    }

    static class UseAtomic implements Demo {
        final AtomicInteger counter = new AtomicInteger(0);

        public Void call() throws Exception {
            for (int i = 1; i <= 10000; ++i) {
                counter.incrementAndGet();
                System.out.print("");
            }
            return null;
        }

        public int getCounter() {
            return counter.get();
        }
    }

    public static void main(String[] args) throws Exception {
        ExecutorService exec = Executors.newFixedThreadPool(4);
        for (int i = 1; i <= 5; ++i) {
            Demo demo = new UsePrimitive();
            exec.invokeAll(Collections.nCopies(4, demo));
            System.out.println("Count to 40000 using primitive, attempt number " + i + ": " + demo.getCounter());
        }
        for (int i = 1; i <= 5; ++i) {
            Demo demo = new UseAtomic();
            exec.invokeAll(Collections.nCopies(4, demo));
            System.out.println("Count to 40000 using atomic, attempt number " + i + ": " + demo.getCounter());
        }
        exec.shutdownNow();
    }
}

典型输出:

Count to 40000 using primitive, attempt number 1: 39711
Count to 40000 using primitive, attempt number 2: 39686
Count to 40000 using primitive, attempt number 3: 39972
Count to 40000 using primitive, attempt number 4: 39840
Count to 40000 using primitive, attempt number 5: 39865
Count to 40000 using atomic, attempt number 1: 40000
Count to 40000 using atomic, attempt number 2: 40000
Count to 40000 using atomic, attempt number 3: 40000
Count to 40000 using atomic, attempt number 4: 40000
Count to 40000 using atomic, attempt number 5: 40000

您看,只有使用 AtomicInt 才能始终获得预期的结果。

【讨论】:

  • 很好,我也试过了,得出的结论是 volatile 仅用于获取线程的最新值,不能防止违反原子性,如果上下文切换发生在某个不幸的时间,那么我们将无法获得正确的值.Also 当使用 Atomicxxx 类时,他们使用 volatile 变量来获取最新值,然后使用同步来正确更新值。
猜你喜欢
  • 1970-01-01
  • 2017-03-13
  • 2014-05-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多