首先我们先介绍下java并发编程中2个问题
我们来看下Counter类
class Counter {
private int c = 0;
public void increment() {
c++;
}
public void decrement() {
c--;
}
public int value() {
return c;
}
}
Counter类每次执行increment方法c的值加1,每次执行decrement方法c的值减1,然而当Counter被多个线程调用的时候,结果可能不是我们所期待的。
当在不同线程中运行作用于相同数据的两个操作发生交错时,就会产生干扰。这意味着这两个操作由多个步骤组成,并且步骤顺序重叠。
Counter实例上的操作似乎不可能交叉,因为c上的两个操作都是单一、简单的语句。然而,即使是简单的语句也可以被虚拟机转换成多个步骤。我们不会检查虚拟机所采取的具体步骤——只要知道单个表达式c++可以分解为三个步骤就足够了:
表达式c–亦可以按照这些步骤分解,只不过第二步是减1
假设线程A调用increment,而线程B调用decrement。如果c的初值为0,它们的交错动作可能遵循以下顺序:
在不同的情况下,可能是线程B的结果丢失了,或者根本没有错误。由于线程干扰bug是不可预测的,因此很难检测和修复它们。
当不同线程对应该是相同数据的内容有不一致的视图时,就会发生内存一致性错误。内存一致性错误的原因非常复杂,幸运的是,我么不需要详细了解这些原因。所需要的只是一种避免它们的策略。
避免内存一致性错误的关键是理解happens-before的关系。这种关系只是保证一个特定语句的内存写入对另一个特定语句是可见的。要了解这一点,请考虑下面的示例。假设定义并初始化了一个简单的int字段:
int counter = 0;
counter字段被线程A和B共享,假设线程A增加counter:
counter++;
然后,不久之后,线程B打印出counter的值:
System.out.println(counter);
如果这两个语句是在同一个线程中执行的,那么可以安全地假设输出的值是“1”。但是如果这两个语句在不同的线程中执行,输出的值很可能是“0”,因为不能保证线程A对counter的更改对线程B是可见的——除非程序员在这两个语句之间建立了happens-before关系。
有多种方式可以创建happens-before关系:
我们再来看下java并发编程中3个重要的概念(下面简单描述下,详细请查看文章最后的引用)
原子行为不能在中间停止:它要么完全发生,要么根本不发生。原子操作的作用在完成之前是不可见的。在java中以下操作都是原子的:
原子操作不能交叉,因此可以使用它们而不用担心线程干扰。然而,这并不意味着原子操作在所有情景下都不需要同步了,因为内存一致性错误仍然是可能的
可见性指的是在一个线程中修改变量的值,随后另一个线程能立即看到该值。在java中可见性由happens-before关系决定
有序性指的是代码按照编码的顺序有序执行,之所以会有这个概念是因为JVM和CPU会将指令重排序来优化程序的性能。
volatile修饰的变量可以保证原子性、可见性和有序性,但是即使能满足这3点也不能保证变量在多线程环境下没有问题,我们来看下面这个例子:
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author RLP
*/
public class TestVolatile {
private static int threadNum = 10;
private volatile int a = 0;
private static AtomicInteger c = new AtomicInteger(0);
public class R implements Runnable {
@Override
public void run() {
//循环计数 statement 1
for (int i = 0; i < 1000; i++) {
a++;
}
c.incrementAndGet();
}
}
public void pre() {
Thread[] ts = new Thread[threadNum];
for (int i = 0; i < threadNum; i++) {
Thread t = new Thread(new R());
ts[i] = t;
}
for (int i = 0; i < threadNum; i++) {
ts[i].start();
}
}
public static void main(String[] args) throws InterruptedException {
TestVolatile testAtomic = new TestVolatile();
testAtomic.pre();
while (c.get() != threadNum) {
}
System.out.println(testAtomic.a);
}
}
当我们运行上面的代码,控制台打印的值不总是我们期望的10000,因为在多线程下statement 1循环体内的a++操作并不是一个原子操作,我们前面已经描述过a++这种操作可以分解为3个指令,虽然每一条指令本身是原子的,但是3条指令在一起就不是原子操作了,这就会产生我们前面说过的线程干扰问题
https://blog.csdn.net/u012723673/article/details/80682208
https://blog.csdn.net/yjp198713/article/details/78839698
https://www.cnblogs.com/wq3435/p/6220751.html