1. Overview
1.概述
While the volatile keyword in Java usually ensures thread safety, it’s not always the case.
虽然Java中的volatile关键字通常能确保线程安全,但情况并非总是如此。
In this tutorial, we’ll look at the scenario when a shared volatile variable can lead to a race condition.
在本教程中,我们将研究共享volatile变量可能导致竞赛条件的情况。
2. What Is a volatile Variable?
2.什么是挥发性变量?
Unlike other variables, volatile variables are written to and read from the main memory. The CPU does not cache the value of a volatile variable.
与其他变量不同的是,volatile变量被写入和从主内存中读取。CPU不会对volatile变量的值进行缓存。
Let’s see how to declare a volatile variable:
让我们看看如何声明一个volatile变量。
static volatile int count = 0;
3. Properties of volatile Variables
3.易失性变量的属性
In this section, we’ll look at some important features of volatile variables.
在这一节中,我们将看看volatile变量的一些重要特征。
3.1. Visibility Guarantee
3.1.能见度保证
Suppose we have two threads, running on different CPUs, accessing a shared, non-volatile variable. Let’s further suppose that the first thread is writing to a variable while the second thread is reading the same variable.
假设我们有两个线程,在不同的CPU上运行,访问一个共享的、非易失性的变量。让我们进一步假设,第一个线程正在向一个变量写入,而第二个线程正在读取同一个变量。
Each thread copies the value of the variable from the main memory into its respective CPU cache for performance reasons.
出于性能方面的考虑,每个线程都将变量的值从主内存复制到各自的CPU缓存中。
In the case of non-volatile variables, the JVM does not guarantee when the value will be written back to the main memory from the cache.
在非易失性变量的情况下,JVM并不保证数值何时会从缓存中写回主内存。
If the updated value from the first thread is not immediately flushed back to the main memory, there’s a possibility that the second thread may end up reading the older value.
如果第一个线程的更新值没有立即刷回主内存,那么第二个线程就有可能最终读到旧的值。
The diagram below depicts the above scenario:
下图描述了上述情况。
Here, the first thread has updated the value of the variable count to 5. But, the flushing back of the updated value to the main memory does not happen instantly. Therefore, the second thread reads the older value. This can lead to wrong results in a multi-threaded environment.
这里,第一个线程已经将变量count的值更新为5。 但是,更新后的值并没有立即冲回主内存。因此,第二个线程读取的是旧值。这在多线程环境中会导致错误的结果。
On the other hand, if we declare count as volatile, each thread sees its latest updated value in the main memory without any delay.
另一方面,如果我们将count声明为volatile,每个线程都能在主内存中看到其最新的更新值,没有任何延迟。
This is called the visibility guarantee of the volatile keyword. It helps in avoiding the above data inconsistency issue.
这被称为volatile关键字的可见性保证。它有助于避免上述数据不一致的问题。
3.2. Happens-Before Guarantee
3.2.发生前保证
The JVM and the CPU sometimes reorder independent instructions and execute them in parallel to improve performance.
JVM和CPU有时会对独立指令进行重新排序,并并行执行以提高性能。
For example, let’s look at two instructions that are independent and can run simultaneously:
例如,让我们看一下两个独立的、可以同时运行的指令。
a = b + c;
d = d + 1;
However, some instructions can’t execute in parallel because a latter instruction depends on the result of a prior instruction:
然而,有些指令不能并行执行,因为后一条指令取决于前一条指令的结果。
a = b + c;
d = a + e;
In addition, reordering of independent instructions can also take place. This can cause incorrect behavior in a multi-threaded application.
此外,独立指令的重新排序也可能发生。这可能导致多线程应用程序中的不正确行为。
Suppose we have two threads accessing two different variables:
假设我们有两个线程在访问两个不同的变量。
int num = 10;
boolean flag = false;
Further, let’s assume that the first thread is incrementing the value of num and then setting flag to true, while the second thread waits until the flag is set to true. And, once the value of flag is set to true, the second thread reads the value of num.
此外,我们假设第一个线程正在递增num的值,然后将flag设置为true,而第二个线程则等待,直到flag被设置为true。而且,一旦flag的值被设置为true,第二个线程就会读取num.的值。
Therefore, the first thread should execute the instructions in the following order:
因此,第一个线程应该按照以下顺序执行指令。
num = num + 10;
flag = true;
But, let’s suppose the CPU reorders the instructions as:
但是,让我们假设CPU将指令重新排序为:。
flag = true;
num = num + 10;
In this case, as soon as the flag is set to true, the second thread will start executing. And because the variable num is not yet updated, the second thread will read the old value of num, which is 10. This leads to incorrect results.
在这种情况下,一旦该标志被设置为true,第二个线程将开始执行。由于变量num尚未更新,第二个线程将读取num的旧值,即10。这就导致了不正确的结果。
However, if we declare flag as volatile, the above instruction reordering would not have happened.
然而,如果我们将flag声明为volatile,上述指令的重新排序就不会发生。
Applying the volatile keyword on a variable prevents instruction reordering by providing the happens-before guarantee.
在一个变量上应用volatile关键字,通过提供 happens-before保证来防止指令的重新排序。
This ensures that all instructions before the write of the volatile variable are guaranteed not to be reordered to occur after it. Similarly, the instructions after the read of the volatile variable cannot be reordered to occur before it.
这确保了在写volatile变量之前的所有指令都保证不会被重新排序到它之后。同样地,读volatile变量之后的指令也不能被重新排序到它之前。
4. When Does the volatile Keyword Provide Thread Safety?
4.volatile关键字何时能提供线程安全?
The volatile keyword is useful in two multi-threading scenarios:
volatile关键字在两种多线程情况下很有用。
- When only one thread writes to the volatile variable and other threads read its value. Thus, the reading threads see the latest value of the variable.
- When multiple threads are writing to a shared variable such that the operation is atomic. This means that the new value written does not depend on the previous value.
5. When Does volatile Not Provide Thread Safety?
5.什么时候volatile不能提供线程安全?
The volatile keyword is a lightweight synchronization mechanism.
volatile关键字是一种轻量级的同步机制。
Unlike synchronized methods or blocks, it does not make other threads wait while one thread is working on a critical section. Therefore, the volatile keyword does not provide thread safety when non-atomic operations or composite operations are performed on shared variables.
与同步方法或块不同,它不会在一个线程在关键部分工作时让其他线程等待。因此,volatile关键字不能提供线程安全当对共享变量进行非原子操作或复合操作时。
Operations like increment and decrement are composite operations. These operations internally involve three steps: reading the value of the variable, updating it, and then, writing the updated value back to memory.
像增量和减量这样的操作是复合操作。这些操作在内部涉及三个步骤:读取变量的值,更新它,然后,把更新的值写回内存。
The short time gap in between reading the value and writing the new value back to the memory can create a race condition. Other threads working on the same variable may read and operate on the older value during that time gap.
读取数值和将新的数值写回内存之间的短暂时间间隔会产生一个竞赛条件。其他在同一变量上工作的线程可能会在这个时间间隔内读取和操作旧值。
Moreover, if multiple threads are performing non-atomic operations on the same shared variable, they may overwrite each other’s results.
此外,如果多个线程对同一个共享变量进行非原子操作,它们可能会覆盖对方的结果。
Thus, in such situations where threads need to first read the value of the shared variable to figure out the next value, declaring the variable as volatile will not work.
因此,在这种情况下,线程需要首先读取共享变量的值来计算出下一个值,将变量声明为volatile将不起作用。
6. Example
6.例子
Now, we’ll try to understand the above scenario when declaring a variable as volatile is not thread-safe with the help of an example.
现在,我们将尝试借助一个例子来理解上述将变量声明为volatile不是线程安全的情况。
For this, we’ll declare a shared volatile variable named count and initialize it to zero. We’ll also define a method to increment this variable:
为此,我们将声明一个名为count的共享volatile变量并将其初始化为零。我们还将定义一个方法来增加这个变量。
static volatile int count = 0;
void increment() {
count++;
}
Next, we’ll create two threads t1 and t2. These threads call the above increment operation a thousand times:
接下来,我们将创建两个线程t1和t2。这些线程会调用上述增量操作一千次。
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for(int index=0; index<1000; index++) {
increment();
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for(int index=0; index<1000; index++) {
increment();
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
From the above program, we may expect that the final value of the count variable will be 2000. However, every time we execute the program, the result will be different. Sometimes, it will print the “correct” value (2000), and sometimes it won’t.
从上面的程序来看,我们可能期望count变量的最终值是2000。然而,每次我们执行该程序时,结果都会有所不同。有时,它会打印出 “正确 “的值(2000),有时则不会。
Let’s look at two different outputs that we got when we ran the sample program:
让我们来看看我们在运行样本程序时得到的两个不同的输出。
value of counter variable: 2000
value of counter variable: 1652
The above unpredictable behavior is because both the threads are performing the increment operation on the shared count variable. As mentioned earlier, the increment operation is not atomic. It performs three operations – read, update, and then write the new value of the variable to the main memory. Thus, there’s a high chance that interleaving of these operations will occur when both t1 and t2 are running simultaneously.
上述不可预测的行为是因为两个线程都在对共享的count变量进行增量操作。如前所述,增量操作不是原子性的。它执行了三个操作–读取、更新,然后将变量的新值写入主内存。因此,当t1和t2同时运行时,很有可能出现这些操作的交错。
Let’s suppose t1 and t2 are running concurrently and t1 performs the increment operation on the count variable. But, before it writes the updated value back to the main memory, thread t2 reads the value of the count variable from the main memory. In this case, t2 will read an older value and perform the increment operation on the same. This may lead to an incorrect value of the count variable being updated to the main memory. Thus, the result will be different from what is expected – 2000.
假设t1和t2同时运行,t1对count变量执行了增量操作。但是,在它将更新的值写回主内存之前,线程t2从主内存读取count变量的值。在这种情况下,t2将读取一个较旧的值并对其执行增量操作。 这可能导致count变量的错误值被更新到主内存。因此,结果将与预期的不同–2000。
7. Conclusion
7.结语
In this article, we saw that declaring a shared variable as volatile will not always be thread-safe.
在这篇文章中,我们看到,将共享变量声明为volatile并不总是线程安全的。
We learned that to provide thread safety and avoid race conditions for non-atomic operations, using synchronized methods or blocks or atomic variables are both viable solutions.
我们了解到,为了提供线程安全并避免非原子操作的竞赛条件,使用同步方法或块或原子变量都是可行的解决方案。
As usual, the complete source code of the above example is available over on GitHub.
像往常一样,上述例子的完整源代码可以在GitHub上找到。