你还不懂可见性、有序性和原子性?

前言

今天开始,王子准备开始一个新的专栏:并发编程专栏

并发编程无论在哪门语言里,都属于高级篇,面试中也尝尝会被问到。想要深入理解并发编程机制确实不是一件容易的事,因为它涉及到计算机底层和操作系统的相关知识,如果对这部分知识不是很清楚可能会导致理解困难。

在这个专栏里,王子会尽量以白话和图片的方式剖析并发编程本质,希望可以让大家更容易理解。

今天我们就来谈一谈可见性、有序性和原子性都是什么东西。

并发编程的幕后

进入主题之前,我们先来了解一下并发编程的幕后。

随着CPU、内存和I/O设备的不断升级,它们之间一直存在着一个矛盾,就是速度不一致问题。CPU的速度高于内存,内存的速度又高于I/O设备。

我们写的代码中大多数内容都会经过内存处理,有些内容会去读写I/O设备,根据木桶理论,整体的性能取决于最慢的操作,就是I/O设备,所以单单提升CPU的性能是不够的。

为了最大化体现出CPU的性能,计算机底层主要做了三部分优化:

1.CPU增加了缓存,比内存速度更快,平衡内存的速度

2.操作系统增加了进程和线程,可以对CPU分时复用

3.编译程序会进行指令的重排,使缓存更好的发挥性能

我们平时的工作中其实一直都享受着这些优化后的成果,但同时他们也会导致一些很难找到原因的BUG。

什么是可见性

首先我们就来看看什么是可见性。

一个线程对共享变量的修改,另一个线程可以感知到,我们称其为可见性

在单核时代,其实是不存在可见性问题的,因为所有的线程都是在一个CPU中工作的,一个线程的写操作对于其他的线程一定是可见的。

但是多核CPU出现后,每个CPU都有自己的缓存,多个线程在不同的CPU中处理数据就会导致不可见问题。

假设变量v的值是1, 两个线程同时执行了v++操作,首先会从内存中读取变量v的数据到各自的CPU缓存中,这个时候两个CPU缓存中的v都是1,执行v++后,两个变量v都变成了2,然后再写回内存,内存中的变量v就变成了2。

但其实我们想看到的结果v最终应该是3才对。

在CPU1缓存中执行v++后,CPU2缓存无法感知的到,这就是可见性问题。而由于可见性问题导致的最终数据不正确,就是线程安全问题。

什么是原子性

由于I/O的速度太慢,早期的操作系统发明了多进程,就是允许某个进程执行一小段时间后,重新选择一个进程来执行,这个过程叫做任务切换,而这一小段的时间我们称其为时间片。

现在操作系统的任务切换一般指的是更轻量级的线程切换,java的并发编程是基于多线程的,自然也会存在线程切换。

一般会在时间片结束的时候进行线程切换,java语言中执行的一段简单的代码往往需要多条CPU的指令实现,比如count++这部分代码,至少需要三条CPU指令:

1.首先把count从内存中读取到CPU的寄存器中

2.在寄存器中执行+1操作

3.最后将count的值写入内存中(可能写入到CPU的缓存中)

而线程切换是可以发生在任意的一条CPU指令执行之后的,注意,这里说的是CPU的指令,而不是java语言中的指令,对于上面的三条指令来说,我们假设 count=0,如果线程 A 在指令 1 执行完后做线程切换,线程 A 和线程 B 按照下图的顺序执行,那么我们会发现两个线程都执行了 count++ 的操作,但是得到的结果不是我们期望的 2,而是 1。

这就是线程切换导致的数据错误问题,我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性,CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符,这是违背我们直觉的地方。因此,很多时候我们需要在高级语言层面保证操作的原子性。

什么是有序性

有序性指的是程序按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序,例如程序中:“x=1;y=2;”编译器优化后可能变成“y=2;x=1;”。

在这个例子中,编译器调整了语句的顺序,但是不影响程序的最终结果。不过有时候调整了语句的顺序可能导致意想不到的 Bug。

在 Java 领域一个经典的案例就是利用双重检查创建单例对象,代码如下:

public class Singleton {

  static Singleton instance;

  static Singleton getInstance(){

    if(instance == null) {

      synchronized(Singleton.class) {

        if(instance == null)

            instance = new Singleton();

        }

    }

    return instance;

  }

}

假设有两个线程 A、B 同时调用 getInstance() 方法,他们会同时发现 instance == null ,于是同时对 Singleton.class 加锁,此时 JVM 保证只有一个线程能够加锁成功(假设是线程 A),另外一个线程则会处于等待状态(假设是线程 B);线程 A 会创建一个 Singleton 实例,之后释放锁,锁释放后,线程 B 被唤醒,线程 B 再次尝试加锁,此时是可以加锁成功的,加锁成功后,线程 B 检查 instance == null 时会发现,已经创建过 Singleton 实例了,所以线程 B 不会再创建一个 Singleton 实例。

这个过程看上去是不是无懈可击,没有漏洞?

答案是否定的,问题就出在了new操作上,我们以为的new操作是这样的:

1.分配一块内存空间

2.在这块内存空间上初始化Singleton实例对象

3.把这个对象的内存地址赋值给instance变量

但实际上由于指令重排,优化后的过程是这样的:

1.分配一块内存空间

2.把这快内存空间的内存地址赋值给instance变量

3.在这块内存空间上初始化Singleton实例对象

那么这样调换顺序后会发生什么呢?

我们假设线程 A 先执行 getInstance() 方法,当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上;如果此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 instance != null ,所以直接返回 instance,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。

总结

使用并发编程开发,往往会出现很多难以找到原因的BUG,通过对可见性、有序性和原子性的分析,可以为我们排查并发导致的BUG提供一些思路。

CPU缓存会导致可见性

指令重排会导致有序性

线程切换会导致原子性

以上就是本篇文章的三个核心内容,那我们下篇文章继续。

往期文章推荐:

JVM专栏

消息中间件专栏

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容