阅读 757

Java 多线程 :漫谈多线程模型

总文档 :文章目录
Github : github.com/black-ant

一 . happens-before 模型

1.1 happens-before 定义

happens-before 规则可以帮助我们有效的判断操作的顺序 , 帮助我们理解多线程的数据流动

  1. 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果,将对第二个操作可见,而且第一个操作的执行顺序,排在第二个操作之前。
  2. 两个操作之间存在 happens-before 关系,并不意味着一定要按照 happens-before 原则制定的顺序来执行。如果重排序之后的执行结果与按照 happens-before 关系来执行的结果一致,那么这种重排序并不非法。

1.2 happens-before 规则

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作,happens-before 于书写在后面的操作。
  • 锁定规则:一个 unLock 操作,happens-before 于后面对同一个锁的 lock 操作。
  • volatile 变量规则:对一个变量的写操作,happens-before 于后面对这个变量的读操作。
  • 传递规则:如果操作 A happens-before 操作 B,而操作 B happens-before 操作C,则可以得出,操作 A happens-before 操作C
  • 线程启动规则:Thread 对象的 start 方法,happens-before 此线程的每个一个动作。
  • 线程中断规则:对线程 interrupt 方法的调用,happens-before 被中断线程的代码检测到中断事件的发生。
  • 线程终结规则:线程中所有的操作,都 happens-before 线程的终止检测,我们可以通过Thread.join() 方法结束、Thread.isAlive() 的返回值手段,检测到线程已经终止执行。
  • 对象终结规则:一个对象的初始化完成,happens-before 它的 finalize() 方法的开始

1.3 其他规则

  1. 将一个元素放入一个线程安全的队列的操作,happens-before 从队列中取出这个元素的操作。
  2. 将一个元素放入一个线程安全容器的操作,happens-before 从容器中取出这个元素的操作。
  3. 在 CountDownLatch 上的 countDown 操作,happens-before CountDownLatch 上的 await 操作。
  4. 释放 Semaphore 上的 release 的操作,happens-before 上的 acquire 操作。
  5. Future 表示的任务的所有操作,happens-before Future 上的 get 操作。
  6. 向 Executor 提交一个 Runnable 或 Callable 的操作,happens-before 任务开始执行操作。

二 . 重排序

为了提高性能,处理器和编译器常常会对指令进行重排序 , 其主要目的是在不改变程序执行结果的前提下,尽可能提高程序的运行效率

  1. 在单线程环境下,不能改变程序运行的结果。
  2. 存在数据依赖关系的情况下,不允许重排序。
    • 多线程情况下 , 当代码中存在控制依赖性时,会影响指令序列的执行的并行度,所以编译器和处理器会采用猜测执行来克服控制依赖对并行度的影响

总结 : 无法通过 happens-before 原则推导出来的,JMM 允许任意的排序。

as-if-serial 语义

  • 所有的操作均可以为了优化而被重排序,但是你必须要保证重排序后执行的结果不能被改变,编译器、runtime、处理器都必须遵守 as-if-serial 语义
  • as-if-serial 只保证单线程环境,多线程环境下无效

JIT 优化原则

尽可能地优化程序正常运行下的逻辑,哪怕以 catch 块逻辑变得复杂为代价。

三 . 线程的 CPU 时间片

Java中线程会按优先级分配CPU时间片运行

  1. 当前运行线程主动放弃CPU,JVM暂时放弃CPU操作(基于时间片轮转调度的JVM操作系统不会让线程永久放弃CPU,或者说放弃本次时间片的执行权),例如调用yield()方法。
  2. 当前运行线程因为某些原因进入阻塞状态,例如阻塞在I/O上。
  3. 当前运行线程结束,即运行完run()方法里面的任务。

yield 放弃 CPU

yield操作并不会永远放弃CPU,仅仅只是放弃了此次时间片,把剩下的时间让给别的线程

IO 柱塞

运行程序将有两条线程工作,ioThread每次遇到I/O阻塞就放弃当前的时间片,而主线程则按JVM分配的时间片正常运行

CPU 优先级

Java把线程优先级分成10个级别,线程被创建时如果没有明确声明则使用默认优先级,JVM将根据每个线程的优先级分配执行时间的概率。有三个常量 :

  • Thread.MIN_PRIORITY : 最小优先级值(1)
  • Thread.NORM_PRIORITY : 默认优先级值(5)
  • Thread.MAX_PRIORITY : 最大优先级值(10)。

线程的调度策略决定上层多线程运行机制,JVM的线程调度器实现了抢占式调度,每条线程执行的时间由它分配管理,它将按照线程优先级的建议对线程执行的时间进行分配,优先级越高,可能得到CPU的时间则越长。

四 .内存屏障

硬件层提供了一系列的内存屏障 memory barrier / memory fence(Intel的提法)来提供一致性的能力

几种主要的内存屏障 :

  1. lfence,是一种Load Barrier 读屏障
  2. sfence, 是一种Store Barrier 写屏障
  3. mfence, 是一种全能型的屏障,具备ifence和sfence的能力
  4. 带Lock前缀类,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。
    • Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。
    • 它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。

内存屏障的能力

  1. 阻止屏障两边的指令重排序
  2. 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效

**内存屏障最常见的地方就是 Volatile **

五. CPU

CPU 高速缓存原理

Java内存模型中每个线程的工作内存实际上就是寄存器以及高速缓存的抽象 , 各个核心直接通过系统总线连接 , 而总线是一种共享资源 , 这就意味着资源竞争.

计算机的局部性原理

局部性原理是缓存技术的底层理论基础。局部性包括两种形式:

  1. 时间局部性,一个具有良好时间局部性的程序中,被引用过一次的存储器位置很可能在不远的将来再被多次引用
  2. 空间局部性,一个具有良好空间局部性的程序中,如果一个存储器位置被引用了一次,那么程序很可能在不远的将来引用附近的一个存储器位置

存储器体系结构 :

存储器呈现金字塔结构 , 主要包括寄存器 , 高速缓存 , 主存等几个概念 , 存储器有以下几个特点 :

  • 一层存储器只和下层存储器打交道,不会跨级访问
  • 下层作为上层的一个缓存。CPU要访问的数据的最终一般都经过主存,主存作为下层其他设备的一个缓存,其他设备的数据最终都要进入主存才能被CPU访问到

CPU 高速缓存核心原理

TODO @ blog.csdn.net/iter_zc/art…

六. 内存模型之一致性

一致性的满足 @ blog.csdn.net/iter_zc/art…

  • 在单机器多CPU的情况下,多CPU并发执行,共用一个内存,一般通过共享内存的方式来处理一致性问题,通过定义满足不同一致性需求的内存模型来解决内存一致性问题(Memory Consistency)

  • 在分布式环境中,多台机器多CPU通过网络来并发执行,一般通过消息通信的方式来处理一致性问题 (Paxos协议 , Zab协议)

6.1 一致性分类

严格一致性 Strict Consistency 线性一致性 Linearizability

所有的读写操作都按照全局的时序来排列执行 , 所有的CPU需要共享一个全局的时钟顺序 , 且按照该顺序执行

顺序一致性 Sequential Consistency

  • 对每个单个CPU来说,它看到自己程序的执行顺序始终是和程序定义是一致的(单个CPU角度)
  • 每个CPU看到的其他CPU的写操作都是按照相同的顺序执行的,大家看到的最终执行的视图是一致的(从全局的角度)
  • 单个CPU对共享变量的写操作马上对其他CPU可见

因果一致性 Causal Consistency

  • 因果一致性是一种弱的顺序一致性,只有有因果关系的数据才需要保证顺序一致性,没有因果关系的数据不需要保证顺序一致性
  • 通俗来说就是 B 对 x 的写操作 W(x)B 会依赖于 A 对 x 的写操作 , 即对外展现为 W(x)a, W(x)b

处理器一致性/ PRAM(Piplined RAM) 管道式存储器

  • 只要求从一个处理器来的写操作按照同样的顺序被其他处理器看到,不同处理器的写操作可以按照不同的顺序被看到
  • 就是说它不保证有因果关系的写操作按照执行的顺序执行

弱一致性 Weak Consistency

弱一致性只对被同步操作保护的共享变量而言,规定了只有对共享变量的同步操作完成之后,共享数据才可能保持一致性.在同步操作过程中,是不保证一致性的,单个处理器对共享变量的修改对其他处理器是不可见的。相比与严格的顺序一致性,它只保持了执行顺序上的顺序一致性,至于可见性必须要等待同步操作结束

  • 对同步变量的读写按照顺序一致性
  • 只有所有对同步变量的写操作完成之后才能对同步变量进行访问
  • 只有所有对同步变量的访问(读/写)完成后才能对同步变量访问

释放一致性 Release Consistency

  • 释放一致性规定了对同步变量的释放操作后,就对同步变量的状态广播到其他处理器

进入一致性 Entry Consistency

  • 进入同步变量时,获取同步变量的最新状态

缓存一致性 Cache Consistency

  • TODO

九 . 多线程模型

9 .1 并行模型

并行 Worker : (许多 java.util.concurrent 包下的并发工具都使用了这种模型)

并行 worker 的核心思想是,它主要有两个进程即代理人和工人,Delegator 负责接收来自客户端的任务并把任务下发,交给具体的 Worker 进行处理,Worker 处理完成后把结果返回给 Delegator,在 Delegator 接收到 Worker 处理的结果后对其进行汇总,然后交给客户端。

9 .2 响应式 - 事件驱动系统 : Actor 模型

在 Actor 模型中,每一个 Actor 其实就是一个 Worker, 每一个 Actor 都能够处理任务。

简单来说,Actor 模型是一个并发模型,它定义了一系列系统组件应该如何动作和交互的通用规则,最著名的使用这套规则的编程语言是 Erlang。一个参与者Actor对接收到的消息做出响应,然后可以创建出更多的 Actor 或发送更多的消息,同时准备接收下一条消息。

9 .3 响应式 - 事件驱动系统 : Channels 模型 .

在 Channel 模型中,worker 通常不会直接通信,与此相对的,他们通常将事件发送到不同的 通道(Channel)上,然后其他 worker 可以在这些通道上获取消息,下面是 Channel 的模型图

致谢

欢迎大家关注我的相关文档
多线程集合目录

芋道源码

死磕系列

JVM 源码

文章分类
后端
文章标签