关于java:Java-并发系列一多线程三大特性

6次阅读

共计 3966 个字符,预计需要花费 10 分钟才能阅读完成。

概述

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

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 缓存引起的可见性


上图是多核 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 中。

能够看到,ADD 指令的流水线上呈现了一个 stall,示意一个暂停。之所以呈现暂停,是因为 R2 的数据还没筹备好(LW R2, C 的操作还没实现)。因为 ADD 暂停的呈现,后续的操作都暂停了一个周期。

上面是一个更为简单的例子:

能够看到,因为 ADD 和 SUB 指令都须要期待上一条指令的执行后果,所以整个流水线上插入了不少 stall。下图显示了如何打消相似的暂停。

因为 LW Re, E; LW Rf, F 通过指令重排序后,并不影响代码执行逻辑。并且当重排序后,所有流水线暂停都能够打消。

尽管指令重排序会导致有序性问题,但指令重排序对性能的进步有十分重大的意义。

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

2.1 节曾经探讨过 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. 《计算机体系结构》作者:张晨光、王志英、张春元、戴葵、朱海滨

正文完
 0