【问题标题】:Java volatile keyword not working as expectedJava volatile 关键字未按预期工作
【发布时间】:2017-07-13 17:33:14
【问题描述】:

我正在学习 volatile 变量。我知道 volatile 的作用,我为 Volatile 变量编写了一个示例程序,但没有按预期工作。

为什么“count”的最终值有时会小于 2000。我使用了 volatile,因此系统不应缓存“count”变量,并且该值应始终为 2000。

当我使用同步方法时,它可以正常工作,但在 volatile 关键字的情况下却不行。

public class Worker {

private volatile int count = 0;
private int limit = 10000;

public static void main(String[] args) {
    Worker worker = new Worker();
    worker.doWork();
}

public void doWork() {
    Thread thread1 = new Thread(new Runnable() {
        public void run() {
            for (int i = 0; i < limit; i++) {

                    count++;

            }
        }
    });
    thread1.start();
    Thread thread2 = new Thread(new Runnable() {
        public void run() {
            for (int i = 0; i < limit; i++) {

                    count++;

            }
        }
    });
    thread2.start();

    try {
        thread1.join();
        thread2.join();
    } catch (InterruptedException ignored) {}
    System.out.println("Count is: " + count);
}
}

提前谢谢你!

【问题讨论】:

  • volatile 和 synchronized 不一样...改用 Atomic ....
  • “我知道 volatile 做什么” 无意冒犯,但这个问题证明并非如此。
  • “我使用了 volatile 因此系统不应该缓存“count”变量”,除了竞争条件(这里的明显问题),这是完全错误的。 Volatile 不会阻止 CPU 在内存中缓存值 - 甚至没有任何方法可以告诉现代 CPU 做这样的事情,因为这毫无意义。 Volatile 做了一些非常不同的事情,如果不了解内存顺序和可见性保证是什么,你真的不应该使用它(在你理解它之后,你会注意到周围有更好的高级解决方案)。

标签: java multithreading


【解决方案1】:
for (int i = 0; i < limit; i++) {
    count++;
}

这里的count++有三个串行操作read,increment然后write,线程1可以初始操作然后线程调度器读取count值后可以转移到线程2,线程2可以修改count,在线程调度程序转移到线程 1 之后,它具有之前读取的先前值,因此这里有一个竞争条件(我们必须检查并采取行动),但是操作不是原子地完成的,你最好使用 AtomicInteger 类,否则增加它通过同步作为一个原子单位

【讨论】:

    【解决方案2】:

    内存可见性和原子性是多线程中两个不同但常见的问题。当您使用同步关键字时,它通过获取锁来确保两者。而 volatile 仅解决内存可见性问题。 在他的书Concurrency in practice 中,Brain Goetz 解释了何时应该使用 volatile。

    1. 对变量的写入不依赖于其当前值,或者您 可以确保只有一个线程更新该值;
    2. 变量不参与其他状态的不变量 变量;
    3. 当变量被锁定时,出于任何其他原因不需要锁定 正在被访问。

    好吧,在你的情况下,看看不是原子的操作 count++。

    【讨论】:

      【解决方案3】:

      i++ is not atomic in Java。所以两个线程可能同时读取,都计算+1为相同的数字,并且都存储相同的结果。

      使用javac inc.java编译:

      public class inc {
          static int i = 0;
          public static void main(String[] args) {
              i++;
          }
      }
      

      使用javap -c inc 读取字节码。我已将其缩减为仅显示函数main()

      public class inc {
        static int i;
      
        public static void main(java.lang.String[]);
          Code:
             0: getstatic     #2                  // Field i:I
             3: iconst_1
             4: iadd
             5: putstatic     #2                  // Field i:I
             8: return
      }
      

      我们看到(静态 int 的)增量是通过以下方式实现的:getstaticiconst_1iaddputstatic

      由于这是用四条指令完成的,并且没有锁,所以不能期望原子性。另外值得注意的是,即使这是用 1 条指令完成的,我们也可能不走运(引用 this thread 中的用户“Hot Licks”评论):

      即使在实现“增量存储位置”指令的硬件上,也不能保证这是线程安全的。仅仅因为一个操作可以表示为单个操作符,就说明它是线程安全的。


      如果你真的想解决这个问题,你可以使用AtomicInteger,它有原子性保证:

      final AtomicInteger myCoolInt = new AtomicInteger(0);
      myCoolInt.incrementAndGet(1);
      

      【讨论】:

        【解决方案4】:

        当您使用synchronized 方法时,它按预期工作,因为它确保如果其中一个线程执行该方法,则其他调用者线程的执行将暂停,直到当前正在执行的线程退出该方法。在这种情况下,整个读-增量-写周期是原子的。

        来自tutorial

        首先,两次调用同步方法是不可能的 在同一个对象上交错。当一个线程正在执行一个 对象的同步方法,调用的所有其他线程 同一对象块的同步方法(暂停执行) 直到第一个线程处理完对象。

        其次,当一个同步方法退出时,它会自动建立一个 与任何后续调用的发生之前的关系 同一个对象的同步方法。这保证了变化 对象的状态对所有线程都是可见的。

        当您使用volatile(正如其他人所解释的那样)时,此循环不是原子的,因为使用此关键字并不能确保在 get 和增量步骤之间的其他线程上不会对变量进行其他写入这个线程。

        对于原子计数而不是 synchronized 关键字,您可以使用例如AtomicInteger:

        public class Worker {
            private AtomicInteger count = new AtomicInteger(0);
            private int limit = 10000;
        
            public static void main(String[] args) {
                Worker worker = new Worker();
                worker.doWork();
            }
        
            public void doWork() {
                Thread thread1 = new Thread(new Runnable() {
                    public void run() {
                        for (int i = 0; i < limit; i++)
                            count.getAndIncrement();
                    }
                });
                thread1.start();
                Thread thread2 = new Thread(new Runnable() {
                    public void run() {
                        for (int i = 0; i < limit; i++)
                            count.getAndIncrement();
                    }
                });
        
                thread2.start();
        
                try {
                    thread1.join();
                    thread2.join();
                } catch (InterruptedException ignored) {
                }
                System.out.println("Count is: " + count);
            }
        }
        

        这里getAndIncrement() 确保原子读取增量集循环。

        【讨论】:

          【解决方案5】:

          count++基本上是这样的:

          // 1. read/load count
          // 2. increment count
          // 3. store count
          count = count + 1;
          

          firstthird 单独的操作是原子的。他们三个together 都不是原子的。

          【讨论】:

            【解决方案6】:

            当您执行count++ 时,这是一个读取、一个增量,然后是一个写入。两个线程可以各自进行读取,各自进行增量,然后各自进行写入,从而只产生一个增量。虽然您的读取是原子的,但您的写入是原子的,并且没有缓存任何值,这还不够。你需要的不止这些——你需要一个原子的读-修改-写操作,而volatile 没有提供。

            【讨论】:

              猜你喜欢
              • 2013-07-19
              • 1970-01-01
              • 2012-12-22
              • 1970-01-01
              • 1970-01-01
              • 2011-02-28
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多