zoukankan      html  css  js  c++  java
  • 多线程并发为什么不安全

    一、线程安全定义

    定义:

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

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

  • 相关阅读:
    VirtualBox设置共享文件夹和镜像访问的方法
    虚拟机文件越来越大解决方案
    linux磁盘清理方法 Linux 下垃圾清理工具 BleachBit
    linux上怎么切换不同版本的arm-linux-gcc?只需改一行函数
    windows桌面添加右键环境
    各种机械键盘轴的差别,究竟什么轴好
    XML是什么,它能够做什么?——写给XML入门者
    MATLAB中导入数据:importdata函数
    理解ThreadLocal
    Leetcode:best_time_to_buy_and_sell_stock_II题解
  • 原文地址:https://www.cnblogs.com/dhcao/p/10982278.html
Copyright © 2011-2022 走看看