简体   繁体   English

Java volatile 关键字未按预期工作

[英]Java volatile keyword not working as expected

I am learning volatile variable.我正在学习 volatile 变量。 I know what volatile does, i wrote a sample program for Volatile variable but not working as expected.我知道 volatile 的作用,我为 Volatile 变量编写了一个示例程序,但没有按预期工作。

Why is the final value of the "count" coming sometime less then 2000. I have used volatile hence the system should not cache "count" variable and the value should always be 2000.为什么“计数”的最终值有时会小于 2000。我使用了 volatile,因此系统不应缓存“计数”变量,并且该值应始终为 2000。

When I used synchronized method it work fine but not in the case of volatile keyword.当我使用同步方法时,它工作正常,但不适用于 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);
}
}

Thank You in advance !先感谢您 !

When you do count++ , that's a read, an increment, and then a write.当您执行count++ ,这是一次读取、一次增量和一次写入。 Two threads can each do their read, each do their increment, then each do their write, resulting in only a single increment.两个线程可以各自进行读取,各自进行增量,然后各自进行写入,结果只有一个增量。 While your reads are atomic, your writes are atomic, and no values are cached, that isn't enough.虽然您的读取是原子的,但您的写入是原子的,并且没有缓存任何值,这还不够。 You need more than that -- you need an atomic read-modify-write operation, and volatile doesn't provide that.您需要的不止这些——您需要一个原子读-修改-写操作,而volatile不提供。

count++ is basically this: count++基本上是这样的:

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

individually the first and third operation are atomic.firstthird操作分别是原子的。 All 3 of them together are not atomic.他们三个together都不是原子的。

i++ is not atomic in Java . i++在 Java 中不是原子的 So two threads may concurrently read, both calculate +1 to be the same number, and both store the same result.所以两个线程可能并发读取,都计算+1为相同的数字,并且都存储相同的结果。

Compile this using javac inc.java :使用javac inc.java编译它:

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

Read the bytecode using javap -c inc .使用javap -c inc读取字节码。 I've trimmed this down to just show the function main() :我已经将其精简为仅显示函数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
}

We see that increment (of a static int) is implemented using: getstatic , iconst_1 , iadd and putstatic .我们看到(静态 int 的)增量是使用以下方法实现的: getstaticiconst_1iaddputstatic

Since this is done with four instructions and no locks, there can be no expectation of atomicity.由于这是用四条指令完成的,没有锁,所以不能期望原子性。 Also worth noting that even if this were done with 1 instruction, we may be out of luck (quote from user "Hot Licks" comment in this thread ):还值得注意的是,即使这是用 1 条指令完成的,我们也可能不走运(引自该线程中用户“热舔”评论):

Even on hardware that implements an "increment storage location" instruction, there's no guarantee that that's thread-safe.即使在实现“增量存储位置”指令的硬件上,也不能保证它是线程安全的。 Just because an operation can be represented as a single operator says nothing about it's thread-safety.仅仅因为一个操作可以被表示为一个单一的操作符,并没有说明它的线程安全性。


If you actually wanted to solve this, you could use AtomicInteger , which has an atomicity guarantee:如果你真的想解决这个问题,你可以使用AtomicInteger ,它具有原子性保证:

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

When you used synchronized method it was working as expected because it ensures that if one of the threads executes that method, the execution of the other caller threads is suspended until the currently executing one exits the method.当您使用synchronized方法时,它按预期工作,因为它确保如果其中一个线程执行该方法,则其他调用者线程的执行将暂停,直到当前正在执行的线程退出该方法。 In this case the whole read-increment-write cycle is atomic.在这种情况下,整个读-增-写循环是原子的。

From the tutorial :教程

First, it is not possible for two invocations of synchronized methods on the same object to interleave.首先,对同一对象的同步方法的两次调用不可能交错。 When one thread is executing a synchronized method for an object, all other threads that invoke synchronized methods for the same object block (suspend execution) until the first thread is done with the object.当一个线程正在为一个对象执行同步方法时,所有其他调用同一个对象的同步方法的线程都会阻塞(挂起执行),直到第一个线程完成对对象的处理。

Second, when a synchronized method exits, it automatically establishes a happens-before relationship with any subsequent invocation of a synchronized method for the same object.其次,当一个同步方法退出时,它会自动建立一个发生在同一个对象的同步方法的任何后续调用之前的关系。 This guarantees that changes to the state of the object are visible to all threads.这保证了对象状态的更改对所有线程都是可见的。

When you use volatile (as it is explained by others) this cycle is not atomic as using this keyword does not ensure that there will be no other write on the variable on other threads between the get and the increment steps on this thread.当您使用volatile (正如其他人所解释的那样)时,此循环不是原子的,因为使用此关键字并不能确保在此线程上的 get 和 increment 步骤之间不会对其他线程上的变量进行其他写入。

For atomic counting rather than the synchronized keyword, you could use eg an AtomicInteger :对于原子计数而不是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);
    }
}

Here getAndIncrement() ensures the atomic read-increment-set cycle.这里getAndIncrement()确保原子读取-增量-设置循环。

Memory Visibility and Atomicity are two different but common problem in Multithreading.内存可见性和原子性是多线程中两个不同但常见的问题。 When you use synchronized keyword, it ensures both by acquiring the locks.当您使用 synchronized 关键字时,它通过获取锁来确保两者。 Whereas volatile only solves the memory visibility issue.而 volatile 只能解决内存可见性问题。 In his book Concurrency in practice , Brain Goetz explains when you should use volatile.在他的《实践中的并发》一书中,Brain Goetz 解释了何时应该使用 volatile。

  1. Writes to the variable do not depend on its current value, or you can ensure that only a single thread ever updates the value;对变量的写入不依赖于其当前值,或者您可以确保只有一个线程会更新该值;
  2. The variable does not participate in invariants with other state variables;该变量不参与与其他状态变量的不变量;
  3. Locking is not required for any other reason while the variable is being accessed.在访问变量时,由于任何其他原因不需要锁定。

Well, in your case look at the operation count++ which isn't atomic.好吧,在您的情况下,请查看不是原子的操作 count++。

for (int i = 0; i < limit; i++) {
    count++;
}

here count++ has three serial operations read, increment then write, thread 1 may operate initially and then thread scheduler may shift to thread 2 after reading the count value, the thread 2 may modify the count, after thread scheduler shifts to thread 1 it has the previous value it read before and so here there is a race condition(which we have to check and act) and but the operation is not done as atomically, u better use AtomicInteger class else increment it as a atomic unit by synchronising这里count++有读、增、写三个串行操作,线程1可以先操作,然后线程调度器读取计数值后可以转移到线程2,线程2可以修改计数,线程调度器转移到线程1后,它有它之前读取的先前值,所以这里有一个竞争条件(我们必须检查并采取行动),但操作不是以原子方式完成的,你最好使用 AtomicInteger 类,否则通过同步将它作为原子单元递增

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM