• 并发编程安全有关的三大特性


      CPU增加高速缓存,线程切换分时复用CPU,编译程序优化指令次序。这三个操作提升了性能,但也带来了三个导致并发安全性的特性,即 可见性,原子性,有序性

      下面深入介绍一下三个特性出现的原因:

    一.可见性: (多核CPU缓存导致)

      单核CPU的线程操作同一个CPU缓存,具有线程间的可见性。而多核CPU的缓存,多个线程操作会操作不同CPU的缓存,造成不可见性。

    例如:A线程,B线程 同时对变量C进行循环1万次自增操作 。假如某时A线程将变量C加1,写入自己的CPU缓存,但还未写入内存,这将导致B线程操作C变量的时候,读的还是内存中旧值。导致最终变量C的结果不是20000,而是 10000到20000的随机数。

    如以下代码:

     执行了方法后的结果:

    总结:A线程计算的变量放在自己的CPU缓存中,还未读入内存。导致B不可见,从内存读的还是旧的数据,导致数据计算错误。

    二.原子性: (非原子操作和线程切换带来)
           一条高级语言指令通常需要多条CPU指令完成。CPU指令是原子操作。

    如 count +=1 语句需要三条CPU指令
    1.将count变量从内存写入CPU的寄存器。
    2.在CPU中进行 +1 操作。
    3.结果写入内存(或写入CPU缓存)。

    操作系统根据线程分时切换来调度任务。线程切换时,可能发生在任何一条CPU指令执行完
    假如线程A,线程B都对count做自增操作,当线程A执行到指令1后,切换到了线程B执行指令1,2,3。这时内存中count值写入为1。
    再切换到线程A,此时直接执行2,3。此时写入内存的仍然是count=1。而不是原子操作自增的2。

    总结就是:非原子性操作时,线程A操作变量操作还未写入内存中,线程B已经对此变量进行读操作。导致数据不正确。

    三.有序性:(编译优化)
      编译器为了优化性能,会改变程序执行的先后顺序

    例子:双重检查创建单例对象

    如以下代码:

      若有两个线程来获取实例,比如此时线程A获取锁,进入 instance = new LazySingletonSync2() 时。线程B开始调用getInstance方法,进行 if(instance == null) 的判断,

    此时关键出现在 instance = new LazySingletonSync2() 语句的指令执行顺序上:

     new对象时,我们认为的操作是:
    1.分配一块内存M
    2.在内存M上初始化对象 instance
    3.将内存M的地址赋值给变量

    实际编译器优化后是
    1.分配一块内存M
    2.将内存M的地址赋值给变量 
    3.在内存M上初始化对象 instance

        若线程A这时只执行到第2步指令,导致 线程B 判断 instance == null 为否,直接返回了instance。但其实此时对象还没初始化。这就导致了线程B返回的对象是有问题的,直接调用会造成空指针引用的异常。

  • 相关阅读:
    星空雅梦
    星空雅梦
    星空雅梦
    星空雅梦
    星空雅梦
    星空雅梦
    星空雅梦
    星空雅梦
    lambda表达式
    VIM--保存和退出等命令
  • 原文地址:https://www.cnblogs.com/liumz0323/p/10467527.html
Copyright © 2020-2023  润新知