Java 并发系列(一)多线程三大特性:原子性、可见性、有序性

概述

多线程三大特性:原子性、可见性、有序性。

1. 原子性

原子性是指:多个操作作为一个整体,不能被分割与中断,也不能被其他线程干扰。如果被中断与干扰,则会出现数据异常、逻辑异常。

多个操作合并的整体,我们称之为复合操作。一个复合操作,往往存在前后依赖关系,后一个操作依赖上一个操作的结果。如果上一个操作结果被其他线程干扰,对于当前线程看来整个复合操作的结果便不符合预期。同理线程也不能在复合操作中间被中断,中断必须发生在进入复合操作之前或者等到复合操作结束之后。

保证原子性就是在多线程环境下,保证单个线程执行复合操作符合预期逻辑。

典型的复合操作:『先检查后执行』和『读取—修改—写入』

1.1 先检查后执行

@NotThreadSafe
public class LazyInitClass {
    private static LazyInitClass instance ;

    public static LazyInitClass getInstance() {
        if(instance == null)
            instance = new LazyInitClass() ;

        return instance ;
    }
}

LazyInitClassgetInstance 中包含先检查后执行的复合操作,通常我们也可以称 getInstance 中包含竞态条件。假设线程 A 和线程 B 同时执行 getInstance。A 看到 instance 为空,便执行 new LazyInitClass() 逻辑。A 还未完成初始化并设置 instance,B 检查 instance,此时 instance 为空,B 便也会执行 new LazyInitClass()。那么两次调用 getInstance 时可能会得到不同的结果。通常 getInstance 的预期结果是多次调用得到相同的对象实例。

LazyInitClassgetInstance 方法虽然存在竞态条件,多数情况下并不会造成业务异常,影响仅仅是增加了 JVM 垃圾回收负担而已。这也是多线程问题隐蔽性强且偶发的原因之一。

但话说回来,编程原则之一就是所有逻辑都必须建立在确定性之上,任何建立在不确定性上的逻辑都是隐患。虽然从业务上看多数情况下没问题,但竞态条件的存在,让代码逻辑建立在不确定性之上。作为编码者应该重视此类问题。

1.2 读取—修改—写入

@NotThreadSafe
public class ReadModifyAndWriteClass {
    private int count = 0 ;

    public int increase() {
        return count++ ;
    }
}

由于 i++ 本身不是原子操作,属于复合操作。ReadModifyAndWriteClassincrease 包含了读取—修改—写入。假设线程 A 和线程 B 同时执行 increase。A 看到 count 为 0,执行 ++ 逻辑。当 ++ 操作还未完成,此时 B 读取 count 看到的仍然是 0。A、B 各自完成 ++ 逻辑后,count 的值等于 1。这就造成了虽然调用了两次 increase 方法,但 count 只增加了 1。这也与预期:每调用一次 increase,count 增加 1 的结果不符。

2. 可见性

可见性问题是指,一个线程修改的共享变量,其他线程是否能够立刻看到。对于串行程序而言,并不存在可见性问题,前一个操作修改的变量,后一个操作一定能读取到最新值。但在多线程环境下如果没有正确的同步则不一定。

有很多因素会使得线程无法立即看到甚至永远无法看到另一个线程的操作结果。在编译器中生成的指令顺序,可以与源代码中的顺序不同,此外编译器还会把变量保存在寄存器而非内存中;处理器可以采用乱序或并行等方式来执行指令;缓存可能会改变将写入变量提交到主内存的次序;而且,保存在处理器本地缓存中的值,对于其他处理器是不可见的。这些因素都会使得一个线程无法看到变量的最新值,并且会导致其他线程中的内存操作似乎在乱序执行。

2.1 缓存引起的可见性

multi-core processor.png

上图是多核 CPU 内存图,其中 individual memory 表示核心多级缓存。main memory 表示主内存,即共享内存。共享内存(shared memory)是线程之间共享的内存,也称为堆内存(heap memory)。所有实例域(instance fields)、静态域(static fields)和数组元素(array elements)都保存在堆内存中。

A 线程与 B 线程共同操作共享变量 V(初始值为 0),A、B 线程分别将 V 变量从主内存复制到 CPU 内核的多级缓存中,此时 A 与 B 都读到 V 的值为 0。A 更新自己的 individual memory 中的 V 的值为 1,此时如果没有将 V 值同步至主内存中,B 从自己的 individual memory 中读到 V 的值仍然为 0。当 V 值同步到主内存后,多级缓存失效,此时 B 才能够从主内存中读取到最新的 V 值为 1。由于多线程环境下何时将多级缓存同步到主内存时间上不确定,所以造成了可见性问题,即 A 线程对共享变量 V 的写操作,位于写操作后执行的 B 线程的读操作不能立即感知。

3. 有序性

有序性问题是指从观察到的结果推测,代码执行的顺序与代码组织的顺序不一致。

3.1 指令重排序引起的有序性问题

在计算机体系结构中,为了提高执行部件的处理速度,经常在部件中采用流水线技术。所谓流水线技术,是指将一个重复的时序过程,分解成若干个子过程,而每一个子过程都可有效地在其专用功能段上与其他子过程同时执行。

以 DLX 指令集结构为例,一条指令的执行简单说可以分为以下几个步骤:

  1. 取指令(IF)
  2. 指令译码/读寄存器(ID)
  3. 执行/有效地址计算(EX)
  4. 存储器访问/分支完成(MEM)
  5. 写回(WB)

每一个步骤都可能使用不同的硬件完成。
指令流水线

由上图所示,如果没有指令流水线,指令2 需要等待指令1 完全执行完成后执行。假设每一个步骤(子过程)需要花费 1 个 CPU 时钟周期,则指令2 需要等待 5 个时钟周期。而使用指令流水线后,指令2 只需等待 1 个时钟周期就可以开始执行。指令2 开始执行时,指令1 根本还没开始执行,仅仅完成了取指操作而已。这仅仅是 DLX 指令集结构的流水线,实际商用 CPU 的流水线级别甚至可以达到 10 级以上,性能提升可谓是非常明显。

由于流水线技术的引入,不得不面对流水线的三种类型的相关:结构相关、数据相关、控制相关。

  1. 结构相关:当指令在重叠执行过程中,硬件资源满足不了指令重叠执行的要求,发生资源冲突时将产生“结构相关”。
  2. 数据相关:当一条指令需要用到前面指令的执行结果,而这些指令均在流水线中重叠执行时,就可能引起“数据相关”。
  3. 控制相关:当流水线遇到分支指令和其他会改变 PC 值的指令时就会发生“控制相关”。

一旦流水线中出现相关,指令在流失线中的执行就会出现问题,消除相关的最基本方法是让流水线中的某些指令暂停执行。一旦暂停,所有硬件设备都会进入一个停顿周期,直接影响是性能的下降。

我们说的指令重排序就是在产生数据相关时替代流水线暂停的重要方法。指令重排序仅仅是减少流水线暂停技术的一种,在 CPU 设计中还有很多其他软硬件技术来防止流水线暂停。

下图展示了 A = B + C 操作的执行过程。LW 表示加载,LW R1, B 表示把 B 的值加载到寄存器 R1 中。ADD 表示加法,ADD R3, R1, R2 表示把寄存器 R1 和 R2 中的值相加保存到寄存器 R3 中。SW 表示存储,SW A, R3 表示将寄存器 R3 中的值保存到变量 A 中。
A=B+C执行流程

可以看到,ADD 指令的流水线上出现了一个 stall,表示一个暂停。之所以出现暂停,是因为 R2 的数据还没准备好( LW R2, C 的操作还没完成 )。由于 ADD 暂停的出现,后续的操作都暂停了一个周期。

下面是一个更为复杂的例子:
复杂计算执行流程
可以看到,由于 ADD 和 SUB 指令都需要等待上一条指令的执行结果,所以整个流水线上插入了不少 stall。下图显示了如何消除类似的暂停。
指令重排序以消除暂停
由于 LW Re, E; LW Rf, F 经过指令重排序后,并不影响代码执行逻辑。并且当重排序后,所有流水线暂停都可以消除。
消除流水线暂停后

虽然指令重排序会导致有序性问题,但指令重排序对性能的提高有非常重大的意义。

3.2 CPU 缓存引起的有序性问题

2.1 节已经讨论过 CPU 缓存导致的可见性问题。CPU 缓存也会导致有序性问题。

看如下的例子:
CPU 缓存引起的有序性
假设 b、c 为局部变量,初始值为 1,A、D 为共享变量,初始值为 0 和 false。Thread1 先于 Thread2 运行,运行结果:Thread2 输出 0。

从结果推测 Thread1 中的 D = true 先于 A = b + c 执行了。

当 D = true 执行完成后,A = b + c 还没来得及执行,此时 Thread2 输出 A 的值,才会出现结果为 0 的情况。

分析:Thread1 将 A、D 共享变量从主内存复制到当前 CPU 内核的多级缓存中,按顺序执行完 A = b + c 和 D = true 后,多级缓存中 A = 2, D = true。然后 Thread1 将 D 的值优先同步到主缓存,A 的值没有同步到主缓存。此时 Thread2 执行,能看到 D 的最新值 true,却不能看到 A 的最新值,只能看到主缓存中 A 的初始值 0。

所以从 Thread2 看,Thread1 线程的执行出现了有序性问题,但从 Thread1 看,自己的确是按照代码组织顺序执行的。

4. 总结

本章详细讲解了多线程的三大特性:原子性、可见性、有序性。想要正确编写多线程程序,一定要正确理解这三大特性。

5. 参考资料

  1. 《The Java® LanguageSpecification Java SE 8 Edition》作者:James Gosling、Bill Joy、Guy Steele、Gilad Bracha、Alex Buckley
  2. 《Java Concurrency in Practice》作者:Brain Goetz、Tim Peierls、Joshua Bloch、Joseph Bowbeer、David Holmes、Doug Lea
  3. 《计算机体系结构》作者:张晨曦、王志英、张春元、戴葵、朱海滨
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容