Java性能 -- 并发一致性
背景
在并发编程中,Java是通过共享内存来实现共享变量操作的,所以在多线程编程中会涉及到数据一致性的问题
1 | public class Example { |
- 有两个线程分别执行count方法,x是共享变量
- 可能出现3种结果:**
<1,1>
**,<2,1>
,<1,2>
Java内存模型
- Java采用共享内存模型来实现多线程之间的信息交换和数据同步
- 程序运行时,局部变量将会存放在虚拟机栈中,而共享变量将会被保存在堆内存中
- 由于局部变量随线程的创建而创建,线程的销毁而销毁,Java栈数据并非线程共享,所以不需要关心数据的一致性
- 共享变量存储在堆内存或方法区中,堆内存和方法区的数据是线程共享的
- 堆内存中的共享变量在被不同线程操作时,会被加载到线程的工作内存中,即_CPU中的高速缓存_
- CPU缓存可以分为L1缓存、L2缓存和L3缓存,每一级缓存中所存储的全部数据都是下一级缓存的一部分
- 当CPU要读取一个缓存数据时,会依次从L1缓存、L2缓存、L3缓存、内存中查找
- 如果是单核CPU运行多线程,多个线程同时访问进程中的共享数据,CPU将共享变量加载到高速缓存后
- 不同线程在访问缓存数据时,都会映射到相同的缓存位置,即使发生线程切换,缓存仍然有效
- 如果是多核CPU运行多线程,_每个核都有一个L1缓存_
- 如果多个线程运行在不同的内核上访问共享变量时,每个内核的L1缓存都将会缓存一份共享变量
- 假设线程A操作CPU从堆内存中获取一个缓存数据
- 此时堆内存中的缓存数据值为0,该缓存数据会被加载到L1缓存中
- 操作后,缓存数据的值变为了1,然后刷新到堆内存中
- 在正好刷新到堆内存之前,另一个线程B将堆内存中为0的缓存数据加载到另一个内核的L1缓存中
- 此时线程A将堆内存的数据刷新为1,而线程B实际拿到的缓存数据值为0
- 此时,内核缓存中的数据和堆内存的数据就不一致了
重排序
在不影响运算结果的前提下,编译器可能会改变顺序代码的指令顺序
1 | public class Example { |
Happens-before 规则
- 程序次序规则
- 在单线程中,代码的执行是有序的,虽然可能会存在指令重排序,但最终执行的结果和顺序执行的结果是一致的
- 锁定规则
- 一个锁处于被线程锁定占用状态,只有当这个线程释放锁之后,其他线程才能再次获取锁
- volatile变量规则
- 如果一个线程正在写volatile变量,其他线程读取该变量会发生在写入之后
- 线程启动规则
- Thread对象的start()方法先行发生于此线程的其它每一个动作
- 线程终止规则
- 线程中的所有操作都先行发生于对此线程的中止检测
- 对象终结规则
- 一个对象的初始化完成先行发生于该对象finalize()方法的开始
- 线程中断规则
- 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 传递性
- 如果操作A happens-before 操作B,操作B happens-before 操作C,那么操作A happens-before 操作C
一致性等级
强一致性 - 全局锁
所有读写操作都按全局时钟下的顺序执行,任何时刻线程读取到的缓存数据都是一样的,Hashtable就是强一致性
顺序一致性 - volatile
- 多个线程的整体执行可能是无序的,但对于单个线程而言执行是有序的
- 要保证任何一次读都能读到最近一次写的数据,volatile可以阻止指令重排序
弱一致性 - 读写锁
不能保证任何一次读都能读到最近一次写入的数据,但能保证最终可以读到写入的数据,读写锁就是弱一致性
参考资料
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.