多线程并发为什么不安全

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;

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

以上是关于多线程并发为什么不安全的主要内容,如果未能解决你的问题,请参考以下文章

多线程与高并发线程安全

多线程与高并发线程安全

多个请求是多线程吗

Java并发多线程编程——集合类线程不安全之HashMap的示例及解决方案

Java多线程,线程安全与不安全的理解,程序的多线程并发编程的基础概念,进程与线程的区别是什么

Java多线程,线程安全与不安全的理解,程序的多线程并发编程的基础概念,进程与线程的区别是什么