多线程并发为什么不安全
Posted dhcao
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程并发为什么不安全相关的知识,希望对你有一定的参考价值。
一、线程安全定义
? 定义:
? 多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。
该定义由Brian Goetz在《Java Concurrency In Practice》(Java并发编程实战)中定义;被百度百科、《深入理解Java虚拟机2》引用;
二、并发安全问题
? 大概很多人都知道一点为什么在多线程并发时会不安全,多线程同时操作对象的属性或者状态时,会因为线程之间的信息不同步,A线程读取到的状态已经过时,而A线程并不知道。所以并发安全的本质问题在于线程之间的信息不同步!
? 分析并发不安全的现象,再一层层展示其原理。
2.1、 竞态条件
? 定义:
? 在并发编程中,由于不恰当的执行时序而出现不正确的结果。
? 案例:
? 这是一个线程不安全的方法,我们的期望是每次获取queryTimes都会将queryTimes的值+1;但是当多线程并发访问时,它的工作情况并不如我们所预想的那般;
static int queryTimes = 0;
public static int getTimes()
queryTimes = queryTimes +1;
return queryTimes;
?
案例图解:
图解说明:
当线程A进入方法获取到queryTimes=17时,线程B正准备进入方法;
当线程B获取到queryTimes=18时,线程A还未处理值;
当线程A处理queryTimes+1 = 18后,线程B随即处理queryTimes+1 = 18;
此时线程A才将处理后到结果写入queryTimes,随后B也将18写入到queryTimes;
? 根据上述,我们知道当竞态条件存在时,多个线程可能同时或者几乎同时读取到某个状态(值),然后将处理后到值进行写入,此时我们可以说发生了数据的"脏读"
? 总结:
? 竞态条件是指多线程同时对数据进行改变,读取到脏数据或写入错数据;
2.2、 重排序、有序性、可见行
2.2.1、 指令重排序
? 定义:
? 计算机为了性能优化会对汇编指令进行重新排序,以便充分利用硬件的处理性能。
?
? 案例:
int a;
int b;
int c;
...略...
a = 1; // 步骤a
b = 2; // 步骤b
c = a + b; // 步骤c
? 案例图解:
? 案例分析
- 虽然代码顺序是步骤a、步骤b、步骤c
- 但是从时间上以上三种情况都有可能
- 原因是步骤a和步骤b并没有依赖关系
- 所以为了能快点执行,计算机会调整步骤a和步骤b的顺序
- 因为步骤c依赖于步骤a和步骤b,所以重排序也会在a和b之后
2.2.2、 有序性
? 定义:
? 在Java中,单线程总是顺序执行的!
? 当编译器和处理器重排序时,必须保证,不管怎么重排序,单线程的执行结果不能被改变
2.2.3、 可见性
? 定义:
? 多线程中,若线程A中进行的每一步都可以被线程B观测到,则称线程A对线程B具有可见性。
? 线程B不仅可以看到线程A处理的结果,还能准确的知道在处理过程中,每一个状态的改变,已经状态改变的顺序;
? Java线程的通讯是透明的,线程之间不可以直接进行信息交换,所有的通讯必须同内存共享!所以多线程是天然不可见的,就是说如果不主动干涉的话,线程之间不可见,为什么呢,因为线程虽然第一步处理步骤a,第二步处理步骤b,但是先将步骤b的结果写入主内存,后将步骤a的结果写入主内存,则对观测线程来说,首先看到的是步骤b的结果,然后才是步骤a的结果!
2.3、内存模型
? Java线程模型由主内存和工作内存组成;
如图:
? 说明:
- 工作内存和主内存两部分一起组成Java线程的内存模型
- 工作内存是属于线程的,不同线程的工作内存之间不可共享,不可通讯
- 工作内存通过Load操作从主内存中读取数据,通过Save操作将数据写入主内存
- 线程之间的通讯:本质上是指通过主内存的数据共享
? 解释可见性:
? 如图,Java线程之间是不可见的,因为线程的操作都在它本身的工作内存中完成,完成后的数据再写入主内存。我们称线程之间不可见是因为线程本身没有直接通讯机制;但是线程可以通过主内存进行数据交换,也可以说线程之间可通过内存通讯;
?
? 解释有序性和无序性:
? 单线程有序,是因为单线程的数据操作本身在它私有的工作内存中进行,不管如何重排序,单线程的执行结果不可被改变,所以写入主内存的结果总是正确的。
a = 1; // 步骤a
b = 2; // 步骤b
c = a + b; // 步骤c
? 线程在被观测时无序,因为当线程A中顺序执行 a = 1、b = 1时,并不能保证先将a的值写回主内存,完全有可能先将b的值写入主内存,这是不可预测的。所以在线程B中观察线程A的处理顺序,是非常不可靠的!
因为线程之间只能通过主内存来进行数据交换,所以线程B读到a=0,b=1时,在线程A中可能已经时a=1,b=1。只不过还没有及时到将a的值写入主内存。这样线程B可能误以为线程A先执行的是b=1;
三、总结
? 多线程为什么不安全?现在应该有答案了!究其根本,是因为线程之间无法准确的知道互相之间的状态。那么如何使得多线程安全呢,从内存角度来讲,保证线程的工作内存之间的可见性和有序性,是多线程并发安全的基础。例如volatile关键字和synchronized关键字,我们除了从作用上了解他们,还可以从更深层的内存语义上理解,他们之所以能够一定程度的解决线程安全问题,是因为他们约束了一定的内存处理方式!
以上是关于多线程并发为什么不安全的主要内容,如果未能解决你的问题,请参考以下文章
Java并发多线程编程——集合类线程不安全之HashMap的示例及解决方案