优秀网站建设哪家好,网页设计实训报告格式,做跨境的网站有哪些内容,常州网站关键词优化咨询Amdahl 定律代替摩尔定律成为了计算机性能发展的新源动力#xff0c;也是人类压榨计算机运算能力的最有力武器#xff1b;
摩尔定律#xff0c;描述处理器晶体管数量与运行效率之间的发展关系#xff1b;Amdahl 定律#xff0c;描述系统并行化与串行化的比重与系统运算加…Amdahl 定律代替摩尔定律成为了计算机性能发展的新源动力也是人类压榨计算机运算能力的最有力武器
摩尔定律描述处理器晶体管数量与运行效率之间的发展关系Amdahl 定律描述系统并行化与串行化的比重与系统运算加速能力之间的关系
让计算机同时做几件事情可以在大量 I/O 等待中充分利用计算机的计算能力避免计算资源处于等待其他资源的空闲状态
TPS每秒处理的事务数是衡量一个服务性能高低的重要指标这也与程序的并发能力有密切关系
程序线程并发协调的有条不紊效率才能最佳线程间频繁争用数据相互阻塞甚至死锁则会大大降低程序并发能力
硬件的效率与一致性
计算机的存储与处理器的运算速度存在几个数量级的差距而计算过程不可能消除存储设备内存交互、读取运算数据、存储运算结果等为了尽可能给运算加速计算机系统不得不加入一层或多层高速缓存Cache作为内存与处理器之间的缓冲 缓存一致性Cache Coherence多路处理器每个处理器都有自己的高速缓存共享内存多核系统Shared Memory Multiprocessors System它们又共享统一主内存Main Memory当多处理器的运算任务涉及统一主内存区域可能导致各自缓存数据的不一致为了解决不一致问题需要定义一些协议如 MSI、MESIIllinois Protocal、MOSI、Synapse、Firefly、Dragon Protocal 等内存模型在内存缓存一致性协议下对特定内存或高速缓存进行读写访问的过程抽象不同架构的物理机拥有不一样的内存模型JVM 也有自己的内存模型乱序执行Out-Of-Order Execution为了使处理器内存的运算单元能力尽可能充分利用处理器将输入的代码乱序执行在将乱序执行的结果重组但不保证各语句计算的先后顺序与代码中定义的顺序一致这可能导致计算结果与顺序执行时不一致指令重排Instruction ReorderJVM 即时编译器中的乱序执行 文章目录1. 主内存与工作内存2. 内存间交互操作3. 对于 volatile 型变量的特殊规则4. 针对 long 和 double 型变量的特殊规则5. 原子性、可见性与有序性6. 先行发生原则Java Memory ModelJMMJava 内存模型定义程序中各种变量实例字段、静态字段、构成数组对象的元素等不包含局部变量与方法参数等线程私有变量的访问规则如 JVM 把变量存储在内存、从内存中取出变量值的底层细节试图屏蔽各种硬件和操作系统的内存访问差异实现让 Java 程序在各平台达到一致内存访问效果C/C 等主流程序语言直接使用物理硬件和操作系统的内存模型导致一些场景在不同平台需要编写正对性的程序JDK 2 建立JDK 5 成熟并完善
JMM 的定义需要让 JVM 的实现有足够自由地去利用硬件的各种特性寄存器、高速缓存和指令集中某些特有的指令来获取更好的执行速度
1. 主内存与工作内存 主内存Main Memory与物理硬件的主内存类似实际只是 JVM 内存的一部分主要对应 Java Heap 中对象实例数据部分对应了物理硬件的内存工作内存Working Memory与物理硬件的高速缓存类似每条线程独占保存了该线程使用的变量的主内存副本仅对象中被线程访问到的字段非整个对象线程对变量的所有操作读取、赋值等都必须在工作内存中进行不能直接在主内存进行线程间的变量值的传递必须通过主内存来进行主要对应 Java VM Stack 中的部分区域JVM 为了更好的运行速度可能会让工作内存优先存储在寄存器和高速缓存中因为程序运行时主要访问的是工作内存
reference 类型引用的对象在 Java Heap 被各个线程共享但 reference 本身是 Java Stack 的局部变量是线程私有的
volatile 变量的读写也必须经过工作内存的拷贝但其有序性让其如同直接在主内存读写
2. 内存间交互操作
主内存与工作内存交互协议的 8 种原子性操作
lock锁定把一个主内存的变量标识为某个线程独占状态unlock解锁把一个主内存的锁定状态的变量释放出来释放之后的变量才可以被其他线程锁定read读取把一个主内存的变量传输到线程的工作内存中load载入把通过 read 操作传输到工作内存的变量放入工作内存的变量副本中use使用把工作内存中一个变量的值传递给执行引擎JVM 需要使用变量的字节码指令时所需执行的操作assign赋值把一个从执行引擎接收的值赋给工作内存的变量JVM 给变量赋值的字节码指令所需执行的操作store存储把一个工作内存的变量传递给主内存write写入把通过 store 操作传输到主内存的变量放入主内存的变量中
JMM 只要求 read 和 load、store 和 write 是顺序成对执行但中间可以插入其他指令如 read a、read b、load a、load b
JMM 基本操作的规则
不允许 read 和 load、store 和 write 操作单独出现不允许主内存变量传输到工作内存但工作内存不接收反之亦然不允许一个线程丢弃它最近的 assign 操作变量在工作内存中改变后必须将改变同步会主内存不允许一个线程无 assign 操作时无原因的将数据同步回主内存新的变量必须在主内存中诞生对一个变量实施 use、store 之前必须先执行 assign、load 操作一个变量在同一时刻只允许被一个线程 lock但可以被同一线程多次 lock多次 lock 后需要执行相同次数的 unlock 操作变量才会被解锁不允许对一个没有被 lock 的变量执行 unlock也不允许对其他线程 lock 的变量执行 unlock对一个变量 unlock 之前必须先把它同步回主内存执行 store、write 操作
double 和 long 类型的变量在一些平台的 load、store、read、write 操作允许拆分
3. 对于 volatile 型变量的特殊规则
volatile 是 JVM 提供的最轻量的同步机制JMM 为 volatile 变量定义了一些特殊的访问规则
可见性JMM 保障 volatile 变量对所有线程可见即当一个线程修改了这个变量新值对其他所有线程立即可见普通变量的传递需要通过主内存A 线程回写新值且 B 线程从主内存读取新值新增对 B 线程才可见 volatile 变量在各线程的工作内存中是不存在一致性问题的但 Java 的运算操作并非原子操作因此 volatile 变量的运算在并发下也不是安全的 禁止指令重排普通变量仅保障在方法执行过程中所依赖赋值结果的地方得到正确的结果不保障变量赋值操作的顺序与程序代码中的顺序一致线程内部表现为串行的语义、Within-Thread As-If-Serial Semantics
/*** volatile 变量自增运算测试** author Aurelius Shu* since 2023-02-25*/
public class VolatileTest {public static volatile int race 0;public static void increase() {race;}private static final int THREADS_COUNT 20;public static void main(String[] args) throws InterruptedException {Thread[] threads new Thread[THREADS_COUNT];for (int i 0; i THREADS_COUNT; i) {threads[i] new Thread(() - {for (int i1 0; i1 10000; i1) {increase();}});threads[i].start();}// 等待所有累加线程都结束for (Thread thread : threads) {thread.join();}System.out.println(race);}
}执行结果并非预期的 200000而是小于 200000且每次都不一样
Class 字节码
public static void increase();Code:Stack2, Locals0, Args_size00: getstatic #13; //Field race:I3: iconst_14: iadd5: putstatic #13; //Field race:I8: returnLineNumberTable:line 14: 0line 15: 8volatile 关键字保障了 getstatic 获取的 race 值是正确的但在对栈顶的 race 执行 iconst_1、iadd 这些指令时其他线程可能已经对 race 进行了更新此时栈顶的 race 值就过期了因此 putstatic 指令最终将较小的 race 值同步回了主内存
一条字节码指令还可能被转化为若干本地机器指令因此字节码指令也不是原子性操作
通过 volatile 保障一致性的必要条件
运算结果并不依赖变量的当前值或者能够保障只有单一线程修改变量的值一写多读变量不需要与其他的状态变量共同参与不变约束
volatile 使用场景示例
volatile boolean shutdownRequested;public void shutdown() {shutdownRequested true;
}public void doWork() {while (!shutdownRequested) {// 代码的业务逻辑}
}当 shutdown() 方法被调用时可以保证所有线程中 doWork() 可以立即停止
指令重排演示
Map configOptions;
char[] configText;
// 此变量必须定义为volatile
volatile boolean initialized false;// 假设以下代码在线程 A 中执行
// 模拟读取配置信息当读取完成后
// 将 initialized 设置为 true通知其他线程配置可用
configOptions new HashMap();
configText readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized true;// 假设以下代码在线程 B 中执行
// 等待 initialized 为true代表线程 A 已经把配置信息初始化完成
while (!initialized) {sleep();
}
// 使用线程A中初始化好的配置信息
doSomethingWithConfig();若未对 initalized 变量使用 volatile则线程 A 中 initialized true; 操作机器码级别指令可能被提前执行从而导致线程 B 中也提前以为配置信息已就绪导致 B 线程出错
DCL 单例模式
public class Singleton {private volatile static Singleton instance;public static Singleton getInstance() {if (instance null) {synchronized (Singleton.class) {if (instance null) {instance new Singleton();}}}return instance;}public static void main(String[] args) {Singleton.getInstance();}
}对 instance 变量赋值的汇编代码
0x01a3de0f: mov $0x3375cdb0,%esi ;...beb0cd75 33; {oop(Singleton)}
0x01a3de14: mov %eax,0x150(%esi) ;...89865001 0000
0x01a3de1a: shr $0x9,%esi ;...c1ee09
0x01a3de1d: movb $0x0,0x1104800(%esi) ;...c6860048 100100
0x01a3de24: lock addl $0x0,(%esp) ;...f0830424 00;*putstatic instance; - Singleton::getInstance24voliatile 修饰的变量在赋值后mov %eax,0x150(%esi)还插入了一个 lock add1 $0x0,(%esp) 操作这是一个内存屏障 内存屏障Memory Barrier、Memory Fence指令重排不能把后面的指令排序到内存屏障之前的位置若存在多个处理器同时访问一块内存且其中一个在观测另一个就需要内存屏障来保证一致性 lock add1 $0x0,(%esp) 把 ESP 寄存器的值加 0操作将本处理的缓存写入内存并引起别的处理器或内核无效化Invalidate其缓存相当于对缓存中的变量做了一次 JMM 中的 store 和 write 操作通过这一操作可以让 volatile 变量对其他处理器可见这也正是指令重排无法越过内存屏障的原因 指令重排处理器允许将多条指令不安程序代码顺序分发给各相应电路单元进行处理但必须保证指令依赖情况保障得到正确执行结果
// 指令 1AA10
// 指令 2AA*2
// 执行 3BB-3
/// 指令 1 与 2 存在 A 变量的依赖关系不能重排
/// 指令 3 与 指令 1、2 不存在变量依赖关系可以发生重排volatile 变量的读操作与普通变量几乎无差别写操作则稍慢因为需要再本地代码中插入许多内存屏障指令来保障处理器不发生乱序执行
volatile 的同步机制的性能优于锁在 volatile 与锁中选择的唯一依据是 volatile 的语义是否满足使用场景的需求
假定 T 线程对 V 和 W 两个 volatile 变量进行 read、load、use、assign、store、write 操作需满足如下规则
在工作内存中每次使用 V 前必须先从主内存刷新最新值用于保障看见其他线程对 V 的修改执行 use 前必须先执行 load只有执行 use 前才能执行 loaduse 和 load、read 是必须连续一起出现的在工作内存中每次修改 V 后必须立即同步回主内存中用于保障其他线程可以看到自己对 V 的修改执行 assign 后必须执行 store只有执行 assign 后才能执行 storeassign 和 store、write 是必须连续一起出现的volatile 修饰的变量不会被指令重排用于保障代码的执行顺序与程序中的顺序相同T 对 V 的 use 或 assign 先于 T 对 W 的 use 或 assign则相应的 T 对 V 的 read 或 write 必须先于 T 对 W 的 read 或 write
volatile 的指令重排屏蔽语义在 JDK 5 中被完全修复此前是无法完全避免的因此在 JDK 5 之前无法安全的使用 DCL 实现单例模式
4. 针对 long 和 double 型变量的特殊规则
long 和 double 的非原子性协定Non-Atomic Treatment of double and long VariablesJMM 特别规定运行 VM 将没有被 volatile 修饰的 64 位数据的读写操作划分为 2 次 32 位数据进行操作允许 JVM 自行选择是否保证 64 位数据的 load、store、read、write 的原子性
若多个线程共享一个非 volatile 的 long 或 double 变量多个线程同时对他们进行读写操作可能读取到一个半个变量的数值一般只出现在 32 位 JVM由于存在浮点运算器 Floating Point UnitFPUdouble 类型通常不会出现非原子性访问问题
-XX:AlwaysAtomicAccessesJDK 9 起可以如此约束 JVM 对所有数据类型进行原子性访问
实际开发中除非数据有明确的线程争用否则一般不需要因此而刻意将 long 和 double 变量声明为 volatile
5. 原子性、可见性与有序性
JMM 是围绕着并发过程中如何处理原子性、可见性、有序性着三个特征来建立的
原子性AtomicityJMM 直接保证 read、load、assign、use、store、write 这 6 个操作的原子性可以大致认为基本数据类型的访问、读写是原子性的long 和 double 存在非原子性协定但基本可以忽略
在实际应用中更大范围的原子性保障可以通过 JMM 提供的 lock、unlock 操作来实现在字节码层面相应是 monitorenter、monitorexit 指令在 Java 代码层次则是同步块 synchronized 关键字
可见性Visibility当一个线程修改了共享变量的值其他线程能够立即得知这个修改
JMM 规定变量的修改必须通过主内存来同步volatile 变量相比普通变量会保证新值立即同步到主内存以及每次使用前立即从主内存刷新
synchronized 可以实现可见性因为对一个变量执行 unlock 之前必须先把变量同步回主内存执行 store、write
final 被 final 修饰的字段在构造器中被初始化后会立即被其他线程看见
public static final int i;
public final int j;static {// 一经初始化其他线程可见i 0;// ...
}{// 一经初始化其他线程可见j 0;// ...
}有序性Ordering机器码执行的顺序与程序中代码顺序是否一致 在本线程内观察所有操作都是有序的线程内似表现为串行Within-Thread As-If-Serial Semantics 在一个线程中观察另一线程所有操作都是无序的指令重排、工作内存与主内存同步延迟
Java 语言通过 volatile 和 synchronized 两个关键字保障线程间操作的有序性volatile 本身直接禁止指令重排synchromized 则同一时间只允许一个线程对变量进行 lock 操作
synchronized 虽同时可以满足原子性、可见性、有序性但滥用可能导致性能受到急剧影响
6. 先行发生原则
先行发生Happens-Before原则描述 JMM 中两个操作的偏序关系Java 语言无须任何同步手段就能保障的一些天然先行发生关系如下
程序次序规则Program Order Rule在一个线程内的控制流中书写在前面的操作先行发生于书写在后面的操作需注意控制流不是代码顺序而是分支、循环等结构管程锁定规则Monitor Lock Rule一个 unlock 操作先行发生于后面对同一锁的 lock 操作volatile 变量规则Volatile Variable Rule对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作线程启动规则Thread Start RuleThread 对象的 start() 方法先行发生于此线程的每一个动作线程终止规则Thread Termination Rule线程中所有操作都先行发生于对此线程的终止检测Thread:: join()、Thread::isAlive()线程中断规则Thread Interruption Rule对线程 interrupt() 方法的调用先行发生于中断线程的代码检测到中断事件的发生Thread::interupted() 可以检测中断是否发生对象终结规则Finalizer Rule一个对象的初始化完成先行发生于它 finalize() 方法的开始传递性Transitivity操作 A 先行发生于操作 B操作 B 先行发生于操作 C则 A 先行发生于 C
先行发生原则演示
// A 线程执行
i 1;
// B 线程执行
j i;
// C 线程执行
i 2;若 A 先行于 BC 不存在可以保证 j 的值是 1 若 A 先行于 BC 和 B 没有先行关系则 C 可能发生在 A 与 B 之间此时 j 的值可能是 1 也可能是 2因为 B 可能不会观察到 C 对 i 的修改这时不具备并发安全
private int value 0;public void setValue(int vlaue) {this.value value;
}public int getValue() {return value;
}假设两个方法在多线程中执行
不在一个线程不适用程序次序规则没有同步快不适用管程锁定规则value 没有被 volatile 修饰不适用 volatile 变量规则与线程启动、终止、中断、对象终结等规则无关不存在规则可以传递 因此对 value 的这两个操作不是线程安全的
修复线程安全的方案
通过对 getter/setter 方法加 synchronized 同步实现管程锁定规则给 value 添加 volatile 修饰套用 volatile 变量规则value 的更新不依赖 value 的原值这里可以适用
// 在同一线程中执行int j2 可能先辈处理器执行这不影响先行发生原则的正确性
int i 1;
int j 2;时间先后顺序与先行发生原则之间基本没有因果关系我们衡量并发安全问题时不能受到时间顺序的干扰一切以先行发生原则为准 上一篇「JVM 编译优化」Graal 编译器
PS感谢每一位志同道合者的阅读欢迎关注、评论、赞 参考资料
[1]《深入理解 Java 虚拟机》