java内存模型与并发编程理论
2021-01-15 22:07:39 2 举报
java并发编程理论
作者其他创作
大纲/内容
B
原子操作1
1安达尔定律:并行最高加速比speedup<=———————— F +(1-F)/N结论:性能受 串行操作的比例+并行度 影响
工作内存
a新值
加锁标志位2bit
执行线程t2的指令
I状态可以写入,基于I值计算的其他值也可以写入,因此MESI不保证原子性
lock.unlock
附属共享变量有关系的话,还可以反推主共享变量的阶段算法
并发锁性能优化历程
i=1
hb
同一变量
cpu取指令和数据只能从工作内存这唯一来源获取。因此线程间通讯只能通过主内存
算法无状态(只有局部变量,无入参或不修改入参)
保存t1上下文(t1挂起)
指令段(存储一条条指令,如方法区)
其他cpu,每台主机最多可连接8个cpu,每个cpu可以有多个核心(如32核)
指令重排序
cpu
解决方案
缓存
不一致的数据
volatile a
A
cpu反复重试,如果长时间不能获得锁或高并发场景浪费cpu做无用功
00
内存
cpu内核2
线程t1
B-》E
t2线程修改B,因修改同一缓存行等待
xxx
非阻塞算法:基于cas构建的并发安全算法都具备可见性和重排序,且单个共享变量具备原子性因此线程安全,但多个共享变量不具备原子性,此时通过hb对中间状态的识别和处理使代码具备原子性(达到一致状态后才继续执行)的算法叫做非阻塞算法
运算器
ALU运算(加减乘除与或移位)
t2
2.多个原子操作不具有原子性:多个原子操作间没有串行化
写或读
附属共享变量B1->B2
volatile=可见性+重排序。不包括原子性
共享对象不并发
4.assign
L3缓存128M(共享)15ns-40~45个时钟周期
提高并行度:1.增加线程数量在一定限度内可提高性能,但线程数过多使得cpu调度线程频繁,上下文切换开销大,还会占用更多的内存2.读写分离
cpu核心2
JVM规定的8大原子指令(jvm的汇编语言,最终会被翻译成c++),只有这些指令具备原子性:read/load/use/assign/store/write/lock/unlock。lock和unlock指令与synchronized、cas和reentrantlock都符合该规则
i=0
3.use
未来得及改B
t1
不能直接调用:保护模式
分布式锁
内核空间(kernel memory)
读
不能通讯
并发算法
S1-1
使用KLT管理线程,线程切换耗时较大,减少切换,保持持续运行有助提供性能。如何保持:减少线程数,使得线程数与cpu核心数一样,这样就不必切换线程了
一级缓存
volatile读
自旋锁与阻塞锁有各自适应的场景:1.自旋锁适合等待锁时间短的场景(争抢锁的线程少+占用锁的时间短),因为可以减少线程上下文切换的开销2.阻塞锁适合等待时间长的场景,因为可以释放cpu做更多的业务代码,而不是自旋抢锁做无用功
写
C
c
单共享变量S1->S2
cas可保证单个共享变量原子性
否,但不影响
将并发算法拆分为多个步骤,使用cas分阶段循环递进处理
同一lock
持有
3.反复重试一定次数失败
读写分离
2.lock a
JMM:java内存模型,这是约定的抽象概念,具体实现看hotspot提供商
A=true
串行线程封闭(数据库连接)
1.存储a旧值并计算b值
lock和unlock指令
t2栈
存放t2
通过内存总线,缓存与内存交换数据
主内存,交互数据需耗时80ns
偏向锁与无锁靠标志位区分
S->I
不同jvm
storeload
MarkWord
原子操作
同步全都是串行操作,减少锁竞争损耗加快锁获得速度:1.锁快进快出,不在同步区块内做耗时长的操作,如访问数据库2.减小锁的粒度,降低竞争概率3.减少持有锁的个数 4.锁分段。在高度竞争的环境,锁分段可以有效降低锁竞争,代价是增大系统开销(用空间换时间)和大大提高系统复杂度与排除难度。在竞争不高的情况不能使用分段锁5.避免热点访问:concurrenthashmap的size热点并发方法不会实时计算,而是put方法时维护size属性
本次内核指令入参
进程表PCB
失败重试
用户空间
阻塞算法
多核cpu,以AMD 3970X为例
5.store
数据段
锁标志位
pc1时钟周期
否
缓存行
每成功完成一个阶段cas,就可以安全的修改其他共享变量
2.写入b值
空间局部性原则:临近位置的存储将来会被使用(增加缓存命中率),因此一次性加载多个缓存行可以减少缓存与内存的交互次数,提高程序性能。cpu与内存交互甚至与L3交互都很慢,如果等到加载数据,cpu将浪费大量时钟周期,此时cpu不等待加载完成,直接执行下一条指令,看起来就是乱序执行。磁盘文件顺序读写比随机读写快除了减少磁头移动时间外,也有类似原因
锁B或资源B
线程2
线程1
寄存器
内存与缓存交互的基本单位为缓存行,所以这里原子指令操作的是缓存行
多个CPU在read时同步置为S状态
4.将结果拷贝回用户空间
t1线程修改A对象
同步块2自旋+阻塞
object
2.l1未命中,查找l2缓存
3.置位锁标志失败
lock和unlock指令:锁定主内存的变量地址,使得线程有序访问共享变量
volatile写
锁定失败时的应对方案:阻塞还是重试
反推处于哪个阶段
以缓存行为加锁单位,这可能导致伪共享问题
栈封闭(局部变量)
cpu加载到I状态的缓存行时,将会重新加载
线程阻塞算法(重量锁/LockSupport)
cpu内核3
synchronized轻量锁
8.唤醒_waitset线程
使用失效数据
写A
线程t2
线程上下文切换:上下文即线程栈包括PC计数器、本地变量表、操作数栈上下文保存在哪?KLT保存在内核空间数据段,ULT保存在用户空间数据段。JAVA使用KLT线程模型,由内核管理线程同一时刻最多只有cpu核心数的线程执行,其他线程都在等待队列中等候内核调度
使用hb传递性,构建并发算法间的hb关系,例如AQS.Node.status,线程获取锁失败阻塞前必须设置status=1,因为唤醒队头和入队不具备Hb
wait是等待条件符合时唤醒,但实现方式是进入_waitset,当持有锁的线程退出时,就会唤醒它:while(condition){ lock.wait;}
内存总线(ab具备volatile语义)
5.+1
同步块自旋+阻塞
3.原子操作泄漏:原子操作的中途抛出异常,导致AB处于不一致状态
A=false
无锁
原子操作2
B=1
多个数据缓存行
同线程的写入操作对这个变量的读可见(非阻塞算法中常用的hb)
synchronized重量锁
6.恢复无锁状态
2.重新read
程序计数器PC(指向下一条指令的地址)
存放t1
阻塞算法与非阻塞算法的区别是:阻塞算法只能有一个线程可以进入算法,其他线程阻塞;非阻塞算法同时可以有多个线程进入算法,但只有一个成功,其他线程需自旋重头执行这个算法直到成功或放弃
s==s2
缓存行-64byte,存放多个对象
7.write
其他线程只要获取不到锁就立刻升级轻量锁。这意味着只要存在竞争,那么偏向锁就失效,成为无用功
A=1
5.撤销偏向锁升级轻量锁
持有偏向锁的threadId+偏向次数epoch
单共享变量S2->S3
对象头前25bit
无论何种执行时序,都不影响结果
不可访问其他线程工作内存
01
2.拷贝参数到数据段
threadId25bit
阻塞
锁记录指针
改后变为M状态
_owner
1查找l1缓存
1.创建锁记录
对象分代年龄4bit
共享变量
线程非阻塞算法(CAS)
数据寄存器们(操作数和中间结果)
均衡性能的锁方式,适合不明确场景的算法
3.安全点暂停t1
cas
硬中断、软中断、时钟中断
主内存(线程间通过主存交互数据,主内存数据共享)
死锁:1.以固定顺序申请锁或资源2.开放调用3.使用轮询锁或定时锁
java进程
S1-0
使用CAS用作共享锁,可限制线程数量(Semaphore)
对象的identity的hashcode
可见性:cpu1将A改为true后,不执store+write,或者cpu2不执行read+load,那么A的新值对cpu2不可见,A一直使用A的副本值,基于false算出的c是不对的
a旧值
B=2
1.synchronized(object)
L23ns-10
monitor
执行完同步块无需释放偏向锁
t2需要重新加载缓存行
线程2执行的指令地址
串行队列(redis单工作线程将共享对象访问串行起来)
强制写回内存
5.使用内核调用结果
由lock修饰的a在read时,总线设置E状态
内存总线
t2.interupt
主内存
数据段(存储计算数据,如堆和栈)
有锁
3.置位成功
是否为偏向锁1bit
a
原子性(串行)争抢状态(一致状态)
cpu2
成功
9.唤醒
3.未命中,查找l3缓存
4.unlock
共享对象不可变
可见且有序的共享变量
三、并发性能
3.比较旧值与当前值,一致更新新值,不一致不更新返回false
2.CAS
cpu2执行引擎
storestore
E->S
基于false算出c
cpu核心1
6.指向
饥饿(得不到调度):1.wait和notify时序问题2.显示锁unlock逃逸
3.重新load
6.store
如果计算了System.identityHashCode,对象头无法存放偏向信息,此时不会启用偏向锁,而是升级轻量锁。注意不是可重写的equals
A=0
普通读写
t2线程修改B
副本A=false↓改副本A=true
线程封闭(ThreadLocal)
同步块1自旋+阻塞
jvm内存模型:规定cpu-工作内存-主内存,一般对应硬件为cpu-缓存-内存,不是绝对的,有时主内存也可能被当做工作内存使用
6.write
按中断号0x80查找中断向量表,从寄存器读取入参找到处理程序(cpu需要执行的指令)和传给处理程序的入参:保存上下文(寄存器、l1l2l3缓存)+用户态向内核态切换(保护模式->实模式)+cpu从ring3->ring0。内核指令调用完毕后:恢复上下文+内核态向用户态切换+cpu从ring0->ring3
同步块阻塞
总线发布消息,将其他cpu缓存改为I状态
补救或回退b
单共享变量S1
串行阻塞其他线程
CAS(N)
执行线程t1的指令
IO总线控制权毫秒,比内存慢10000倍,比缓存慢10万倍
检测到中断
1.系统调用:int 0x80指令
二、并发活跃性
D
11
1.read
防止重排序违背hb原则
L1缓存3M
_waitset
1.覆盖
线程创建和销毁:1.使用线程池
空
objectWaiter
loadload
A-》D
其他写
S->M
6.从主存加载数据到l3 80ns
中断打了cpu一顿,让它跳转执行中断号指定的代码
t2.start
a值
缓存行伪共享问题:修改的不是共享数据,但因为同处一个缓存行,修改过程被迫串行缓存行64byte,是cpu与内存交换数据的基本单位,一次交换多个缓存行。避免伪共享可以提高性能:缓存行齐,@Contended修饰和启动参数-XX:-RestrictContended
缓存行64byte
wait和唤醒有先后顺序要求:不恰当的顺序导致线程活跃性(饥饿)问题。例如步骤8先于4执行,此时t1将无法被唤醒。LockSupport没有时序要求!
一、并发安全性
cpu内核1
读A
可见性常见问题原因:
synchronized偏向锁
cpu1执行引擎
副本A=false
锁漂移:即期望是同一lock实际却是不同的lock:不能用基础类型的包装类型和String作为锁,且锁要用final修饰
1
cpu3
对象成为监视器时创建并指向
2.new
4.blocked
aba问题
用锁来排除其他线程并行干扰,串行访问共享对象。锁一定会降低程序性能,高度竞争的锁会成为系统瓶颈,应尽量减少程序协同
synchronized加锁过程
CAS指定次数仍未获取锁,转为阻塞算法(轻量锁/ReentrantLock)
hashcode25bit
修改A
可被其他线程观测到的中间不一致状态
线程表
附属共享变量C1->C2
同jvm
a旧值+b新值
解决可见性和重排序的原则:happen-before
活锁1.重试机制中引入随机性
1.检查是否指向自己
4.从主内存查找数据
不能XXX访问
cpu1
一致
一致状态
其他30个核心
b新值
基于内核管理线程阻塞非常慢,如果可以很快获取锁浪费cpu做切换操作无用功
4.a变化考虑是否需要恢复b原值,未变化操作完成
i++
hb可以解决重排序和可见性,但多个线程同时读写共享变量(竞态条件)时,因不同执行时序而导致结果不正确。原子性禁止打断,这样保证不受执行时序的影响,说白了就是让这些线程串行(按顺序)执行,同一时刻只有一个线程可以执行共享块:其他线程阻塞的为阻塞算法,自旋等待的为非阻塞算法。并发一定会降低性能,甚至成为系统瓶颈!!竞态条件与cpu个数无关,与运行的线程数目有关——单核cpu也会产生并发问题
4.拷贝锁记录
1.存储旧值并计算新值
6.加载到l2
使用CAS用作排他锁,可保证线程安全(LongAdder.cellsBusy)
原子性常见问题:没有串行执行导致,专业名词‘竞态条件’
非阻塞算法
重试
观测到不一致状态时,可以放弃本次循环,等待下次循环到达一致状态后再争抢CAS。如果任何执行时序都不影响一致性,那么观测到不一致的线程可以帮助其他线程执行以便尽快达到一致状态。FutureTask.state=COMPLETING就是为了通知处于中间状态
只要数据可以被多个线程看到,那么就存在并发安全性问题。即便这个数据不可修改,但在创建这个数据时也会出现可见性问题。因此,只要数据可被多个线程看到就必须做并发安全处理
DMA
锁升级的过程。不可降级
无状态
t1栈
读写共存
volatile读写
CPU
4.才能use
使用cas修改多个共享变量的过程:a前后变化判断b是否修改有效,如ThreadPoolExecutor.execute.workqueue.offer(command)
本次内核指令出参
锁记录
使用基于失效数据得到的推论:所有条件判断都适用这个可见性问题
构建的共享变量
java使用KLT线程模型,由内核管理线程,线程的创建、阻塞、唤醒、销毁都要切换到内核态完成。可在任务管理器观察到。ULT需要自己管理跨平台cpu的线程非常复杂,但性能更好(最大限度让cpu别闲着+无需切换内核态),单核多线程称为纤程。线程是什么?cpu执行的指令数据和线程栈数据的代表
多个数据缓存行按块读写数据
cpu2发出lock指令,锁定Acpu1 lock了A,cpu2只能等待
不能直接访问:保护模式
线程1执行的指令地址
控制器
工作内存:JVM创建线程的同时分配默认2M内存独享空间去存储私有数据-线程栈和TLAB(这就导致jvm可创建的线程数是有限的)。大部分操作系统都会将缓存分配给工作内存
L2缓存16M
串行自旋一定次数再阻塞其他线程
kernel
锁分段
loadstore
补充
多个指令缓存行
lock.lock
年龄
原子性
共享变量A=false↓改共享变量A=true
xxxhb
指向持有轻量锁线程的栈指针30bit
10
10.置位
防止volatile间重排序
4.锁还用吗
3.检查a值是否变化
分代年龄最大为15,即15次后进入老年代
控制单元:取指令、取数写数
是
时间局部性原则:一个变量会被多次使用,如循环。多驻留缓存一段时间可以减少寻址次数
volatile解决可见性:由开启硬件层面的缓存一致性协议解决(这里描述intel的MESI一致性协议):为修饰的变量增加lock前缀,启动MESI总线嗅探机制同步缓存行状态:避免修改共享变量后其他cpu永远感知不到变化。由于寄存器存在,I状态的变量可能之前已经加载到了寄存器,此时MESI实际上是失效的。MESI本质只是一个线程通知机制,当共享变量被某个线程修改并写回后,其他线程可以感知到而已。多个缓存行同步只能使用总线锁!
可见性
装载线程t2上下文,并执行
重排序只保证单线程的串行执行一致性:重排序不改变串行执行结果,但并发时乱序导致程序逻辑混乱
读写、写写共存
L11ns-3
硬盘
读写互斥
抛出异常
CAS(1)
cpu控制单元
多个指令缓存行按块读取数据
8.unlock
2.CAS更新失败
指向持有重量锁线程的栈指针30bit
锁A或资源A
不可直接访问主内存,必须经过工作内存
5.cpu不等加载数据完成,直接异步执行下一条指令
volatile禁止重排序:由jvm添加下图描述内存屏障实现,这些屏障禁止两条指令重排序且会触发缓存与内存交换数据(如果是写操作到内存会触发MESI)。而硬件层面则是通过cpu的指令(具体哪些指令依cpu不同而不同)sfence/ifence/mfence/lock解决。
t2.join
7.加载到l1
对变量A执行lock指令后,需重新load,unlock前需write到主内存。可多次lock同一变量,未lock不能unlock
0
3.找到处理程序并执行内核指令,写入结果
cpu1释放锁
3.CAS更新
4.升级为重量锁
Kernel内核指令段
4.指向
死锁原因
GC回收
2.还活着吗
2.load
_count
低幼却又常见的加锁问题:锁加给了代码,而没有加给需要串行访问的共享数据
volatile不能保证原子性,由cas实现:cas是不会使线程阻塞的锁,通过感知I状态,与原值比较,保证单个共享变量的原子性(a可逆如a:1->2->1会出现aba问题,一般a不可逆时才考虑这么玩)
7.wait
0 条评论
下一页