您的位置:首页 > 编程语言

并发编程-可见性,原子性,有序性问题

2020-04-01 12:59 381 查看

前言

传统计算机以单核为主,多个调度任务通过『分时』策略共享同一个处理器的资源。在之后的时间里计算机设备快速发展,随着多核处理器的出现,计算机的运算能力得到了大幅度的提升,在程序的编写方面,出现了并发编程,为的是能够最大程度地提高对处理器的利用率,与此同时,也带来了众多程序并发相关的问题,大致可以概括为以下三个问题。

可见性

在计算机发展的过程中,面临的一个大问题就是——cpu,内存,外存之间的速度差异。简单来说就是 cpu 的计算速度虽然很快,但是对内存的读取速度却逊色很多,外存 io 更是慢得夸张,为了尽可能去弥补这一块的不足,cpu 增加了缓存的概念,正因为这个缓存的介入,导致程序出现了数据可见性的问题。

在以往的计算机只有一个 cpu 的情况下,多个任务在同一个 cpu 上进行作业,对同一个 cpu 的缓存进行读写,这时候并不存在可见性的问题,但是当计算机存在多个 cpu 时,多个任务可以真正意义上的达到并行运行,这个时候,每个任务都是对不同的 cpu 缓存下的数据进行操作,缓存数据没法及时更新到主存当中,因此导致了可见性问题的出现。

原子性

拿 java 语言举例,在程序执行的过程中,一条 java 的编程语句,会被底层的编译器,解释器分解成多条 机器指令,再加上操作系统的任务切换是指令级别的,也就是说在任何一条机器指令执行完成之后都可能进行任务切换,这个时候我们认为的原子性操作就不再是真正意义上的原子性了。

举个例子:

// 简单的自增操作,其实就涉及原子性问题
count++

上面的语句被计算机分步执行:

  • 首先,把变量 count 从内存加载到 cpu 的寄存器
  • 之后,在寄存器中执行 +1 操作
  • 最后,将结果写入内存(缓存机制导致可能写入的是 cpu 缓存)

对于上面的三条指令来说,假设 count = 0,如果线程 A 在指令 1 执行完后做线程切换,线程 A 和 线程 B 按照下图序列执行,会发现两个线程都执行 count += 1 的操作(并且都是以 count = 0 作为基础值),最终导致两个线程的自增操作之后,count 的结果为 1 而不是预期的 2。

有序性

java 语言的编译器层面在将字节码解释成机器指令的过程当中,还会对指令的执行顺序进行调整-指令重排,其目的在于不改变程序的最终运行结果的前提下,对程序进行优化,然而这也为并发编程带来了问题。

在实现单例模式时我们可能会这样写:

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

而 java 中创建对象的语句相对应的机器指令可以分解为:

  • 分配一块内存 M;
  • 在该块内存上初始化对象;
  • 将对象地址返回给引用;

但是实际执行过程中,可能发生指令重排,出现下面的顺序:

  • 分配一块内存 M;
  • 将对象地址返回给引用;
  • 在该块内存上初始化对象;

那么这个时候就可能会出现问题:

  • 线程 A,B 同时执行到最外层 if 判断
  • 线程 B 优先执行其中代码,对还没有初始化对象,便将对象地址返回
  • 线程 A 这时候在对引用判断时,发现引用不为空,便直接返回

这个就是比较有名的『双检锁』问题了。

总结

  • cpu 的缓存带来了程序『可见性』问题
  • 线程切换带来了『原子性』问题
  • 编译优化带来了『有序性』问题

了解这些问题出现的本质原因,对我们后续问题排查有很大的帮助,另外在使用一门技术的时候,除了要清楚它能够带来什么好处之外,对其可能产生的问题也要了解,以便尽可能做到问题的规避。

相关知识

  • 32 位操作系统上 long 变量的操作可能存在什么问题?
  • volatile 关键字保证可见性,有序性
  • 点赞
  • 收藏
  • 分享
  • 文章举报
春娇的志明 发布了15 篇原创文章 · 获赞 0 · 访问量 147 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: