Java中的线程干扰和内存一致性错误
Java允许多线程,这涉及同时执行程序的两个或多个部分。它通过同时执行多个任务来提高 CPU 利用率。线程通过共享对象引用和成员变量相互通信。
当两个线程访问同一个共享内存时
如果没有正确实施,多线程中的这种通信可能会导致两种类型的错误:
- 螺纹干涉
- 内存不一致
线程干涉错误
当多个线程共享同一个内存时,有可能两个或多个不同的线程对同一数据执行不同的操作相互交错,并在内存中创建不一致的数据。当线程执行具有多个步骤的操作并且它们的步骤顺序重叠时,线程会交错。这称为线程干扰。
即使对于像使用语句 i++ 或 i– 递增或递减变量(比如 i)的值这样的小操作,虚拟机也会执行如下多个步骤:
- 从内存中检索 i 的值
- 将 i 的值递增/递减 1
- 将 i 的新递增/递减值存储回内存
如果有两个线程 A 和 B 对同一个对象进行操作,A 执行递增操作,B 执行递减操作,可能会导致数据不一致。如果 i 的初始值为 10。线程 A 从内存中读取 i 的值为 10 并将其值递增到 11。在将新值存储到内存之前,线程 B 从内存中读取 i 的值: 10、由于内存还没有更新。现在,线程 B 将 i 的值递减到 9。现在内存中的新值将是 9 或 11,其中预期值实际上是 10。线程的任何一个结果都可能丢失并被另一个或那里覆盖可能根本没有错误。由于不可预测,线程干扰错误很难检测和修复。
线程干扰错误的时序图
例子:
// Java program to explain the
// concept of thread interference.
import java.io.*;
// Creating thread by creating the
// objects of that class
class SharedClass {
static int i=10;
void increment()
{
for(int j=0;j<50;j++)
{
// incrementing value of i
i=i+1;
System.out.println("i after increment "+i);
}
}
void decrement()
{
for(int j=0;j<50;j++)
{
// decrementing value of i
i=i-1;
System.out.println("i after decrement "+i);
}
}
}
class GFG {
public static void main(String[] args)
{
final SharedClass s1 = new SharedClass();
Thread t1 = new Thread()
{
@Override
public void run()
{
s1.increment();
}
};
Thread t2 = new Thread()
{
@Override
public void run()
{
s1.decrement();
}
};
t1.start();
t2.start();
}
}
在上面的代码中,预期输出的最后一行应该是“i after increment 10”或“i after decrement 10”,但实际输出可能会因线程的干扰而有所不同。线程干扰是不可预知的,尝试多次运行上述代码,在某些情况下会发现线程干扰错误。可以明显看到交错线程操作。
如何避免线程干扰错误
通过使代码线程安全,可以避免线程干扰:
- 同步
- 多个线程对同一对象的访问限制
- 将变量声明为 final。
- 将变量声明为 volatile。
- 创建不可变对象。
内存一致性错误
在多线程中,一个线程所做的更改可能对其他线程不可见,并且它们对相同共享数据的视图都不一致。这称为内存一致性错误。
内存一致性更多的是基于架构的概念,而不是基于 Java 的。对主内存的访问可能不会按照 CPU 启动它们的顺序发生,特别是对于经常通过硬件写入缓冲区的写入操作,因此 CPU 不需要等待它们。 CPU 保证从所有 CPU 的角度来看,保持对单个内存位置的写入顺序,即使 CPU 感知其他 CPU 的写入时间与实际时间不同。由于缺乏正确数据的可见性,这有时会导致内存不一致。
内存一致性错误的时序图
如何避免内存一致性错误
通过建立先发生关系可以避免内存一致性错误。这种关系保证了一个线程执行的内存写入操作对于同一共享内存上的任何其他线程的读取操作都是可见的。
以下操作可以创建先发生关系:
- Thread.start() – 该语句使导致创建新线程的代码对新线程可见。
- Thread.join() - 此语句使线程中代码的效果对执行连接的线程可见。
线程干扰和内存一致性错误的区别
Thread interference error | Memory consistency error |
---|---|
Thread interference deals with interleaving of the execution process of two threads. | Memory inconsistency is about visibility and deals with hardware memory. |
Thread interference can be avoided by granting exclusive access to threads, that is only one thread at a time should access the shared memory. | Memory consistency errors can be dealt with by establishing happens-before relationship which is simply a guarantee that memory writes by one specific statement are visible to another specific statement. |