
多线程引出的问题
我们都知道多线程机制能减少任务执行时间和提供并发处理能力,同时我们也知道天下没有免费的午餐,多线程机制也是需要付出代价的,它也引入了很多问题需要我们去解决,其中主要包含三个问题。
数据竞争、竞争条件问题,前者与多线并发修改内存数据相关,而后者则是并发执行导致运行结果不可预估。本篇文章将对这两个问题进行深入分析。 不同级别存储的数据一致性问题,主要是现代计算机的CPU为了提高执行速度而引入了不同访问速度的存储介质,比如磁盘->主存->缓冲->寄存器结构,对于共享变量,每个线程都会有自己的副本,随着并发的进行就可能会产生一致性问题。 编译器及CPU的优化,编译器和CPU可能会冲排序指令,甚至删除某些指令,这些做法在单线程中是不存在问题的,但对于多线程来说却可能会导致执行结果出错。
... 多线程问题
数据竞争
所谓的数据竞争就是指对于某个内存,存在至少两个线程会读写该内存,其中至少有一个线程对该内存进行写操作,而其它线程可以是读操作或写操作。如下图所示,线程一和线程二在并发执行的过程中都对某个共享内存进行读写操作,这种情况下如果没有其它措施来保证的话则可能就会导致执行结果错误。简单来理解就是多个线程对同时一个内存进行写操作,那么在写的过程中其它线程读取该内存的数值并非是预期的。
数据竞争例子
下面是一个数据竞争的例子,为了更容易产生数据竞争现象,这里我们定义一个Memory类表示共享内存,然后我们定义update方法来表示对共享内存的写操作,它包含了对Memory中a、b两个变量的更新。接着在主线程中创建并启动一个新线程去调用update方法,可以看到该方法并非是原子的,最后再主线程中打印出a、b两个变量,结果可能是0,0、0,1或1,1。实际上如果把update作为一个操作的话,它要么就是两个变量都加一,要么就是都不加一,但却出现了0,1的情况,也就是update执行到一半主线程去读了该内存,这就造成了数据竞争。
public class DataRaceDemo { Memory mem = new Memory(); public void update() { mem.b++; mem.a++; } public void print_result() { System.out.println(mem); } public static void main(String[] args) throws InterruptedException { DataRaceDemo demo = new DataRaceDemo(); Thread thread1 = new Thread(() -> { demo.update(); }); thread1.start(); for (int i = 0; i < 5000; i++) ; demo.print_result(); } static class Memory { public int a = 0; public int b = 0; public String toString() { return (a + "," + b); } } }
为什么会产生数据竞争
产生数据竞争的根本原因是一个CPU任意时刻只能执行一条机器指令,但对某个内存的写操作可能需要若干条机器指令,这就可能导致写的过程中还未完全修改完内存其它线程就进行读取,从而导致执行结果不可预知,也就产生了数据竞争。从CPU底层来看,一条机器指令是原子的,它要么被执行要么不执行,所以如果某个操作只需一个机器指令则该操作天生具备原子性。如果要避免数据竞争则需要保证写内存的操作是原子的,这样就能避免在修改一半的状态被其它线程读取到。如下图所示,其中一个线程执行count++操作,该操作分成四步,期间另外一个线程对其进行读写,这就产生了数据竞争。实际上编程语言层面上很简单的count=1赋值操作在某些硬件平台上都并非是单独一条机器指令,所以即使是对某个变量简单赋值操作都可能会导致数据竞争。此外,一些编程语言提供的并发模型(比如Java)的每个线程栈都会保存一个共享变量的副本,而某个线程对该副本的更改可能需要一定的时间才能刷新到主存,并且其它线程读取到最新值可能也需要一些时间,这种情况也会造成数据竞争。
如何解决数据竞争
解决数据竞争的方法其实很简单,就是使对共享内存的更新操作原子化,同时保证内存的可见性。如下代码所示,我们对原来的代码进行改动,引入AtomicReference类使得对Memory对象的更新具备原子性,而且还将a、b声明为volatile保证可见性。可以看到对update方法的主要是通过自旋+CAS来达到原子更新,此时如果在运行的话就不会产生0,1的中间状态了,即update方法是一个原子方法,a、b只能同时为0或1。
public class DataRaceDemo2 { Memory mem = new Memory(); AtomicReference<Memory> aMem = new AtomicReference<Memory>(mem); public void update() { for (;;) { Memory newMem = new Memory(); newMem.a = aMem.get().a + 1; newMem.b = aMem.get().b + 1; if (aMem.compareAndSet(aMem.get(), newMem)) break; } } public void print_result() { System.out.println(aMem.get()); } public static void main(String[] args) throws InterruptedException { DataRaceDemo demo = new DataRaceDemo(); Thread thread1 = new Thread(() -> { demo.update(); }); thread1.start(); for (int i = 0; i < 50000; i++) ; demo.print_result(); } static class Memory { public volatile int a = 0; public volatile int b = 0; public String toString() { return (a + "," + b); } } }
更多Java并发原理可关注作者下面的专栏:
作者简介:笔名seaboat,擅长人工智能、计算机科学、数学原理、基础算法。出版书籍:图解数据结构与算法、Tomcat内核设计剖析、图解Java并发原理、人工智能原理科普。