并发编程笔记1:并发bug的成因

简介: 并发编程笔记1:并发bug的成因

前言:

    最近在学习并发编程的知识,打算好好学习下并发这块,之前有处理过并发的问题,但是学的不够体系,知识比较零散.
所以买了极客时间的并发课程(java并发编程实战)和《并发编程实战》从头系统化的学一遍。这里记录一下自己的学习过程和心得之类的。

    首先咱们得知道为啥会产生并发编程的bug,究其根本原因是因为cpu处理速度>>内存>>I/O,这里用了">>",是远远大于的意思,极客时间里面的例子更形象说是
天上一天,人间一年。天上指的是cpu对于内存,内存对于I/O.用来凸显其处理速度的差距。因为有了这三者的差距,cpu的利用率就比较低,一直得等待内存,I/O处理完。
所以为了减少这三者的差距,做出了三种优化:
1.CPU 增加了缓存,以均衡与内存的速度差异;
2.操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;
3.编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。

这三类优化相应的耶带来了三种问题,都可能会导致并发问题:

1.缓存的可见性问题:
    上面的第一种优化,在cpu和内存之间增加了缓存,在多核cpu的情况下,每个cpu可能都有自己的缓存。现在假设一个value=0,当线程a在cpu1的缓存中给value加1,线程b在cpu的缓存中给value加1,它们再往内存中刷新value的值的时候,可能顺序并不是我们想的value变为2.
可能的执行过程是线程a,b如果都将value从内存中读入到自己的cpu缓存中,线程a先给value加了1,然后刷新到内存中value变为1,此时线程b也将value加1,刷新到内存中,覆盖了a写入的值,value还是1.这就是缓存的可见性问题,各自的cpu缓存并不知道,其他线程也对相同的值做了操作。
比较经典的例子就是,启动多个线程对同一个变量进行增加操作,得出来的值不是预想的值。

2.线程切换导致的原子性问题:
    这个问题其实比较好理解,我们学过操作系统知道,多进程操作系统在执行任务的时候,不是串行的进程a执行完,再去执行进程b,而是每个进程有一个操作系统分配的执行时间(称为是时间片),当进程a时间片消耗完了,
就切换到另一个进程。这样进程之间交替的执行,又因为时间片的时间非常短,用户是感知不出来这种切换的。介绍完进程的切换,咱们再说说像java这种的高级语言,对应的底层操作系统指令可能是好多条。
比如我们想的count+=1,对于操作系统就不是原子操作,可能是1,2,3...好几条,那么有两个线程都对count加1的时候,如果进程a中count的值还没有加完,此时操作系统给进程a的时间片用完了,切换到进程b,这时候进程b读到的还是没有增加的的值,那么计算结果就不符合我们的预期。

3.编译优化带来的指令执行次序问题:
    我们写java的知道volatile关键字有使变量可见和禁止指令重排序的作用,其实就是针对的这个问题。编译器的优化能大大提高执行效率,但是也会带来一些问题,“int a = 1;int b = 2”这种被优化成“int b = 2;int a = 1”倒是问题不大。
这里引用极客时间的例子:

public class Singleton {
  static Singleton instance;
  static Singleton getInstance(){
    if (instance == null) {
      synchronized(Singleton.class) {
        if (instance == null)
          instance = new Singleton();
        }
    }
    return instance;
  }
}

这是双重校验锁的单例,代码执行顺序是
1.分配一块内存M;
2.在内存M上初始化 Singleton 对象;
3.然后M的地址赋值给 instance 变量。
可能会被优化成:
1.分配一块内存M;
2.将M的地址赋值给 instance 变量;
3.最后在内存M上初始化 Singleton 对象。
这样就可能会导致下面这种情况,线程a调用getInstance(),a如果处于被优化的步骤2,然后时间片用完了,此时线程b进来,执行到第一个instance == null这里,发现instance不为null,就返回了未初始化的instance。此时访问instance对象可能抛出空指针异常。

相关文章
|
1月前
|
存储 监控 算法
深入探究Java线程池:提升并发性能的利器
在当今高度并发的应用开发中,Java线程池作为一种广泛应用的并发编程技术,提供了一种优雅且高效的线程管理方案。本文深入探究Java线程池的相关技术,涵盖其核心概念、优势、常见类型(如FixedThreadPool、CachedThreadPool、SingleThreadExecutor、ScheduledThreadPool、ForkJoinPool及WorkStealingPool)、核心参数配置、异常处理与监控方法,以及性能调优的最佳实践,帮助读者更好地理解和应用线程池,从而提升并发性能。
|
3月前
|
Java 调度
Java并发编程中的常见陷阱及解决方案
Java并发编程中的常见陷阱及解决方案
|
3月前
|
Java 测试技术 容器
多线程编程基础与并发问题解决方案
多线程编程基础与并发问题解决方案
|
3月前
|
安全 Java 容器
Java并发编程:避免常见的陷阱
Java并发编程:避免常见的陷阱
|
5月前
|
算法 Java
Java多线程基础-13:一文阐明死锁的成因及解决方案
死锁是指多个线程相互等待对方释放资源而造成的一种僵局,导致程序无法正常结束。发生死锁需满足四个条件:互斥、请求与保持、不可抢占和循环等待。避免死锁的方法包括设定加锁顺序、使用银行家算法、设置超时机制、检测与恢复死锁以及减少共享资源。面试中可能会问及死锁的概念、避免策略以及实际经验。
86 1
|
5月前
|
算法 Java 开发者
【专栏】在Java并发编程中,死锁是一个常见且棘手的问题
【4月更文挑战第27天】本文探讨了Java并发编程中的死锁问题,阐述了死锁的基本概念、产生原因及常见场景,并提出了五种解决死锁的策略:避免嵌套锁、设置超时时间、制定锁顺序、检测与恢复死锁以及使用高级并发工具。理解死锁原理和采取相应措施能有效提升并发程序的效率和安全性。在实践中,注重线程协作、选择合适并发工具及框架,有助于降低死锁风险,实现高效并发系统。
105 5
|
5月前
|
资源调度 安全 调度
在讨论Python多线程与并发
在讨论Python多线程与并发
24 2
|
缓存 Java 编译器
并发编程Bug源头
并发编程Bug源头
49 1