并发——原理(一)

并发

如果程序在单核处理器上运行,多个线程将交替地换入或者换出内存,这些线程是同时“存在”的,每个线程都处于执行过程中的某个状态,高速切换感觉同时执行。

如果运行多核处理器上,此时,程序中的每个线程将分配到一个处理器核上,因此可以真正的同时运行。

我们在讨论并发时主要考虑以下几点:

  • 多线程操作相同的资源
  • 保证线程安全
  • 合理分配和使用资源

高并发

高并发(High Cuncurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并发处理很多请求。

我们在讨论高并发时主要考虑以下几点?

  • 服务器能同时处理很多个请求
  • 提高程序性能

缓存

我们知道,计算机系统中运行速度最快就是 CPU,其他部件例如:内存、磁盘、网络等等都是及其缓慢的,同时这些操作在目前的计算机体系中是很难消除的,因为我们不可能仅仅靠寄存器就完成所有的计算任务。

面对高速 CPU 和低速存储之间的鸿沟,如果想要实现高效数据通讯,一个良好的解决方案就是在它们之间添加一个 cache 层,这个 cache 层的速度和整体的速度关系如下:

CPU –> cache –> 存储

这是计算机和软件领域通用的一个问题解决方案:增加中间层。没有什么问题是一个中间层解决不了的,如果有,那就两层。在运算的时候,CPU 将需要使用到的数据复制到 Cache 中,以后每次获取数据都从较为快速的 cache 中获取,加快访问速度。

缓存一致性

所谓理想很丰面,现实很骨感。这种计算体系有一个重要的问题需要解决,那就是:缓存一致性(cache coherence)问题。

因此,当多个处理器同时需要访问同一个内存区域的数据时,首先回去访问 CPU 的 cache 区域中的数据,但是 cache 中的数据也是从共享内存中获取的,此时如果别的 CPU 修改了 cache 中的数据,那么就造成了数据不一致的问题了。

共享内存

在现代的计算机系统中,主要都是多核系统为主。在这些计算机系统中,每一个 CPU 都拥有自己独立的高速缓存,但是因为主存只有一个,因此它们之间只能共享,这种系统也称为:共享内存多核系统(Shared-Memory multiprocessors System),如下图所示:

JAVA并发——基础_2020-03-11-20-08-57.png

一致性协议

各个 CPU 在操作的时候都需要遵守缓存一致性协议来进行操作,这类型的协议有很多,例如:MSI、MESI、MOSI、Synapse、Firefly 以及 Dragon Protocol 等等。所以,通常情况下,共享内存多核系统的架构如下所示:

JAVA并发——基础_2020-03-11-20-12-11.png

指令集重排

为了能够充分利用多核 CPU 的处理性能,处理在实际执行机器指令时并不一定会按照程序设定的指令顺序执行,可能存在代码乱序执行(Out-Of-Order Execution)优化。注意,这里虽然乱序执行了,但是系统会保证执行的结果逻辑上的正确的,从宏观上看就好像是顺序执行一样。

java 内存模型

Java 在 1.5 版本中引入了 JSR 133 标准,在 JSR 133 标准中,定义了如下的 Java 并发内存模型:

JAVA并发——基础_2020-03-11-20-36-45.png

工作内存

这里需要注意的是,工作内存和 Java 内存区域中的堆、栈或者方法区(java 和 native)等并不是一个层面上的东西,它们之间也没有直接的对应关系。如果说主内存存放的是 java 堆中的一些实例对象,那么工作内存应该位于 Java 虚拟机栈。但实际上为了获得更好的运行速度,jvm 和硬件系统可能会让工作内存储存于高速缓存和寄存器中。

同步操作与规则

从上面的模拟中我们知道线程间通信可能会出现数据一致性问题,从上面的图中,可以看出每个线程的工作内存和主存之间的一致性保证是通过 save 和 load 等等一系列的操作完成的。

JAVA并发——基础_2020-03-11-20-44-01.png

由上面的交互关系可知,关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java 内存模型定义了以下八种操作来完成:

lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。

unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的 load 动作使用

load(载入):作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。

use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。

assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的 write 的操作。

write(写入):作用于主内存的变量,它把 store 操作从工作内存中一个变量的值传送到主内存的变量中。

并发的优势和风险

  • 优势
    • 速度:同时处理多个请求,响应更快。复杂的请求可以分为多个进程同时执行。
    • 设计:程序设计在某些情况下更简单,也可以有更多的选择。
    • 资源利用:CPU 能够在等待 IO 的时候做一些其他的事情。
  • 劣势
    • 安全性:多个线程共享数据时可能产生于期望不同的结果。
    • 活跃性:某个操作无法继续进行下去时,就会发生活跃性问题,如死锁、饥饿等。
    • 性能:线程过多,CPU 频繁切换,调度时间增多,同步机制消耗更多内存。
文章目录
  1. 1. 并发
  2. 2. 高并发
  3. 3. 缓存
  4. 4. 缓存一致性
  5. 5. 共享内存
  6. 6. 一致性协议
  7. 7. 指令集重排
  8. 8. java 内存模型
    1. 8.1. 工作内存
    2. 8.2. 同步操作与规则
  9. 9. 并发的优势和风险
|