龙空技术网

多线程并发中什么是竞争条件?

远洋号 227

前言:

今天小伙伴们对“多线程 竞争”都比较重视,兄弟们都想要知道一些“多线程 竞争”的相关知识。那么小编在网络上收集了一些对于“多线程 竞争””的相关知识,希望我们能喜欢,兄弟们一起来学习一下吧!

多线程引出的问题

我们都知道多线程机制能减少任务执行时间和提供并发处理能力,同时我们也知道天下没有免费的午餐,多线程机制也是需要付出代价的,它也引入了很多问题需要我们去解决,其中主要包含三个问题。

数据竞争、竞争条件问题,前者与多线并发修改内存数据相关,而后者则是并发执行导致运行结果不可预估。本篇文章将对这两个问题进行深入分析。不同级别存储的数据一致性问题,主要是现代计算机的CPU为了提高执行速度而引入了不同访问速度的存储介质,比如磁盘->主存->缓冲->寄存器结构,对于共享变量,每个线程都会有自己的副本,随着并发的进行就可能会产生一致性问题。编译器及CPU的优化,编译器和CPU可能会冲排序指令,甚至删除某些指令,这些做法在单线程中是不存在问题的,但对于多线程来说却可能会导致执行结果出错。

多线程问题

临界区

在讲竞争条件之前我们先了解什么是临界区,从代码角度来看,临界区就是一块代码区域,在该区域中多个线程不同的执行顺序以及并发交叉执行都可能会带来跟我们预期的执行结果不一样。以下面程序为例,临界区其实就是incrementValue方法大括号范围,这个例子的代码我们再熟悉不过了,由于a++其实是有好几个机器指令,所以当多个线程并发去执行时会导致结果并非我们所预期的。

public class CriticalSectionDemo { static int a = 0; public static int incrementValue() {  return a++; } public static void main(String[] args) {  for (int i = 0; i < 10; i++)   new Thread(() -> {    for (int j = 0; j < 1000; j++)     incrementValue();   }).start();  try {   Thread.sleep(1000);  } catch (InterruptedException e) {   e.printStackTrace();  }  System.out.println(a); }}
竞争条件

一旦我们知道什么是临界区后我们也就知道了竞争条件,多线程去执行临界区的代码就会产生竞争条件。如果只有一个线程,那么执行临界区后不会产生任何问题,但在多线程下却会产生不可预期的结果。用一个形象点的比喻就是:临界区好比一条赛道,而若干线程进入该赛道后则开始竞争,它们的竞争结果将直接影响临界区的执行结果。下图中,三个线程并发地进入临界区,临界区可以产生的竞争对象包括内存、数据库和文件等等。

竞争条件

竞争条件例子

下面是一个竞争条件的简单例子,与数据竞争例子不同的地方在于a、b两个变量都已经具备原子性了,update方法就是一个临界区,临界区内涉及到a和b的原子增加运算,其中使用了for循环是为了能更容易产生竞争条件。主线程中我们启动十个线程去执行update方法,因为单独一个线程经过update方法会是a加一并且b加上a加一后的值,所以十个线程执行完我们预期值应该是10,55。但实际情况可能是10,70,b可能是55-100之间的值。

public class ConditionRaceDemo { AtomicInteger a = new AtomicInteger(0); AtomicInteger b = new AtomicInteger(0); int delta; public void update() {  delta = a.incrementAndGet();  for (int i = 0; i < 10000; i++)   ;  b.addAndGet(delta); } public void print_result() {  System.out.println(a + "," + b); } public static void main(String[] args) throws InterruptedException {  ConditionRaceDemo demo = new ConditionRaceDemo();  for (int i = 0; i < 10; i++) {   Thread thread1 = new Thread(() -> {    demo.update();   });   thread1.start();  }  Thread.sleep(2000);  demo.print_result(); }}
为什么会产生竞争条件

造成竞争条件的根本原因主要有两个:线程执行顺序的不确定性和并发本身机制。

线程执行顺序的不确定性

因为现在的操作系统多数是抢占式任务型的,所有的任务调度都由操作系统完全控制,这就导致每个线程启动后被执行的顺序并非按照编码顺序,而是由操作系统的调度算法来决定的。比如代码中先编写thread1.start()再编写thread2.start()并不意味着thread1比thread2先执行。这便是不确定性,执行结果也无法由我们预测。

线程调度

比如下面这个例子,虽然编码上thread1比thread2更早被调用,但实际上却不一定thread1先执行,哪个线程先执行是不确定的,程序的最终结果可能是8或10。

public class ConditionRaceDemo2 { static volatile int a = 3; public synchronized static void calc() {  a = a + 2; } public synchronized static void calc2() {  a = a * 2; } public static void main(String[] args) throws InterruptedException {  Thread thread1 = new Thread(() -> calc());  Thread thread2 = new Thread(() -> calc2());  thread1.start();  thread2.start();  Thread.sleep(100);  System.out.println(a); }}

我们先看结果为8的情况,线程二读取a,此时为3,然后执行3*2=6并将结果写回变量a。接着切换到线程一,线程一读取a,此时为6,然后执行6+2=8并将结果写回变量a,最终变量a的值即为8。

结果为8的情况

继续看结果为10的情况,线程一读取a,此时为3,然后执行3+2=5并将结果写回变量a,此时变量a为5。接着切换到线程二,它先读取变量a,然后执行5*2=10并将结果写回变量a,最终变量a的值为10。

结果为10的情况

并发本身机制

并发过程中多个线程会进行上下文切换交叉着执行。为了更好地理解这个问题,我们通过下面这个例子来看。其中calc方法对变量a加2并赋值,主线程中启动两个线程分别执行这个方法。实际上我们想要的结果是7,因为3+2+2=7,但是多次运行的结果还可能是5。这便是并发本身机制所造成的。

public class ConditionRaceDemo4 { static volatile int a = 3; public static void calc() {  a = a + 2; } public static void main(String[] args) throws InterruptedException {  Thread thread1 = new Thread(() -> calc());  Thread thread2 = new Thread(() -> calc());  thread2.start();  thread1.start();  Thread.sleep(100);  System.out.println(a); }}

我们分析结果为5的情况,线程一读取a,此时为3。然后切换到线程二执行,也读取a,此时为3,执行3+2=5并将结果写回变量a。接着切换到线程一继续执行3+2=5,并将结果写回变量a。最终变量a的结果即为5。

结果为5的情况

如何解决竞争条件

简单来说就是将临界区原子化,一旦让临界区具有原子性后则能够保证临界区同时只能有一个线程在里面,这样就避免了竞争。对于Java语言,最简单的方式就是使用语言层面提供的synchronized。我们对calc方法进行声明为synchronized使得该方法具有原子性,这里的原子性是由互斥锁机制实现的,两个线程不管谁先执行都能原子地对变量a加2,最终结果确保为7。

public class ConditionRaceDemo5 { static volatile int a = 3; public synchronized static void calc() {  a = a + 2; } public static void main(String[] args) throws InterruptedException {  Thread thread1 = new Thread(() -> calc());  Thread thread2 = new Thread(() -> calc());  thread2.start();  thread1.start();  Thread.sleep(100);  System.out.println(a); }}

更多Java并发原理可关注作者下面的专栏:

作者简介:笔名seaboat,擅长人工智能、计算机科学、数学原理、基础算法。出版书籍:图解数据结构与算法、Tomcat内核设计剖析、图解Java并发原理、人工智能原理科普。

标签: #多线程 竞争