多线程并发为什么不安全

一、线程安全定义

定义:

多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。

该定义由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;
}

运行结果:https://www.cnblogs.com/dhcao/p/10970604.html

案例图解:

图解说明:

  • 当线程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关键字,我们除了从作用上了解他们,还可以从更深层的内存语义上理解,他们之所以能够一定程度的解决线程安全问题,是因为他们约束了一定的内存处理方式!

原文地址:https://www.cnblogs.com/dhcao/p/10982278.html