qingliu

@qingliu

hello world

333 words

Guestbook
You'll only receive email when qingliu publishes a new post

Java 并发

并发问题的核心

在并发编程领域要解决的三个核心问题: 分工,同步,互斥

  • 分工: 将任务分解,进行执行典型的有生产者-消费者模型
  • 同步:分工好之后任务之间需要协作,需要通信,线程的执行顺序等问题都要解决
  • 互斥:同一时刻只允许一个线程访问共享资源,否则将引起混乱。锁的实现保证等

并发问题源头

1. 可见性

随着多核 CPU 的发展,缓存机制的引入,这就导致了主存中的变量或者数据同时存在于多个 CPU 的缓存中,这就导致了多个线程间数据的可见性问题。

2.原子性

CPU 在进行线程切换的时候采用时间片轮换方式,当一个线程A 的时间片用完但是线程任务未执行完,CPU 使用权被交给另外一个线程 B,此时 B按照相同的代码逻辑从 内存加载数据,执行操作,写回内存执行完毕。此时 A 又回来开始执行未执行完的操作,这时 A 所操作的那份数据的拷贝对内存数据来说已经为老版本了,在此数据上继续操作将覆盖掉 B 所做的更新,导致不期望的执行结果。上述的操作就破坏了原子性,当一个或多个操作在 CPU 执行过程中不被中断的特性称为原子性。

3.有序性

有序性问题常源自编译器优化带来的指令重排序带来的一些问题导致程序执行过程中出现异常结果的现象。

Java 解决并发问题

1.解决可见性

a. volatile 关键字

通过强制不使用 CPU 缓存,直接从内存中读取和写入

b.happens-before 原则 (前一个操作的结果对后续操作可见)

  • 程序顺序性原则: 代码顺序
  • volatile 变量:写操作 happens-before 后续对该变量的读操作
  • 传递性 A happens-before B,B happens-before C 则 A happens-before C
  • 管程中锁: 解锁 happens-before 于加锁
  • 线程 start 规则: A 线程调用 B start 则 B 可以看到 A start 之前的操作(共享变量)
  • 线程 join 规则:A join B 则 A 等 B 执行完返回后可以看到 B 的操作

Java 创建对象

1.创建对象

1.去常量池中找对于类的符号引用--->检查类是否被加载,解析,初始化过
2.进行类加载
3.加载完之后进行对象堆内存分配(大小在类加载完之后就已知),分配根据内存是否规整,分为指针碰撞(就是一个分界线)和空闲列表,规整由 GC 收集器是否压缩决定,Serial,ParNew 是指针碰撞,CMS 为空闲列表
4.处理对象分配时多线程的对象指针,CAS + 重试还有 TLAB (本地线程分配缓冲)保证每个线程在自己的 TLAB 分配对象
5.对对象分配的区域进行初始化零值,一些默认类型的值
6.设置对象头,包含类的 metadata,对象 GC 分代年龄等
7.执行对象的 init 方法

2.对象内存布局

包含三部分: 对象头,对象实例数据,对齐填充

2.1 对象头

  • 第一部分:运行时数据
内容 状态
对象哈希,分代年龄 未锁定
指向锁记录的指针 轻量级锁定
指向重量锁指针 锁膨胀(重量锁锁定)
GC 标记
偏向线程 ID,时间戳,对象分代年龄 可偏向锁
  • 第二部分:类型指针

对象指向类 metadata 的指针,通过此指针确定对象属于哪一个类实例

2.2 存储对象的内容

包括每个字段的内容,基本类型,oops( ordinary object pointers)

2.3 填充部分

HotSpot VM 内存管理系统要求对象起始地址都是 8 byte 的整数倍。

3.对象访问

java 栈中本地变量表存储一个 reference,通过 reference 来访问对象

  • 句柄

堆内维护一个句柄池,reference 指向对象句柄地址,句柄内容如下:

句柄内容 指向位置
到对象实例的数据的指针 堆中实例池中的对象实例数据
到对象类型数据的指针 方法区中对象类型数据
  • 直接指针

reference 直接指向堆中对象地址,java堆中对象布局包含了类型数据的指针便于访问方法区对象类型数据
HotSpot VM 采用直接指针方式,速度快减少了指针定位。

CPU Cache 与 MESI

CPU 缓存

由于CPU 在计算之前存在取指-->译码-->执行 三个阶段,在计算之前需要将前两步准备好,并且准备好操作数,这样才能够最大效率的利用 CPU,但是CPU 的存储读写速度与主存存在巨大的差距,则引入 cache 来消减这个差距,cache 使用的是 SRAM ,SRAM 是 Static Random Access Memory 静态RAM 集成度高速度快,,他比我们的 主存(DRAM)速度快所以用其来做 CPU L1 L2 L3(速度依次降低) 等缓存

Cache 一致性问题

主存中的内存块是按照缓存行映射加载到 cache 中的,缓存行由 如下接口组成

状态 地址 数据

对于多核 CPU 来说每个核都有自己对应的 L1,L2 缓存,L3共享缓缓存,那同一块内存快就有可能被多个CPU 加载到自己的缓存中进行数据的操作,那就会带来缓存一致性问题,如何保证数据内存快的数据一致是需要解决的问题

MESI 协议缓存一致性解决方案

CPU 修改 cache 中的数据后对数据的处理:

  1. wirte through :每次修改之后立即更新到主存,那每次写共享数据就会导致总线事务,高一致性,但是效率低
  2. wirte back: 每次修改后不会立即更新到主存,而是等缓存行在某个时机的时候写回

上述两种方案多线程环境下都需要处理缓存一致性问题,CPU 处理缓存一致性问题提供两种策略
写失效:一个CPU 修改了数据,则该数据在其他CPU 未失效
写更新:一个CPU 修改了数据,则更新到其他 CPU 的该数据

MESI 协议由 Modify,exclusive,shared, invalid 四种状态,表示 修改,独占,共享,失效

Modify:当前 CPU 有最新数据,其他 CPU 该数据失效,用的就是写失效策略
Exclusive:只有当前 CPU 由数据,其他 CPU 没有该数据 和主存数据一致
Shared:当前 CPU 和 其他 CPU 由共同的数据但和主存一致
Invalid:当前数据为失效,需要从主存读取

由于上述四种状态对其他 CPU 都是可感知的,就可以调整自己对 cache 的操作。