概念
JMM是一个抽象的概念,并不真实存在。JMM主要是指java线程之间的通信,也就是工作内存和主内存之间如何通信,它涵盖了缓存、写缓存区、寄存器和其他的硬件编译器优化等内容。
JMM的实现
主要是通过重排序、三个同步原语(synchronize、volatile、final)、内存屏障构成的happen-before原则。
重排序
重排序是指编译器和处理器为了优化程序性能对指令序列进行重新排序的手段
内存屏障
为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序,java把内存屏障分为4类。
LoadLoad(读读)、StoreStore(写写)、LoadStore(读写)、StoreLoad(写读),其中StoreLoad同时具备其他三个屏障的效果。
volatile的定义和实现
volatile是轻量级的synchronize,有volatile修饰的变量转换成汇编代码时,会增加lock前缀,lock前缀的指令在多核处理器会发生两件事:
1)将当前处理器的缓存行的数据写入到系统内存
2)这个写回内存会导致其他处理器缓存了该内存地址的数据无效
volatile的内存语义
原子性:对volatile变量的单个读/写,具有原子性
可见性:对一个volatile变量的读,总能看到对这个变量最后的写。
volatile的内存屏障
当第二个操作为volatile写时,不管第一个操作是什么,都禁止重排序。
当第一个操作为volatile读时,不管第二个操作是什么,都禁止重排序
synchronize的实现原理
代码块的实现是通过moniorenter和monitorexit指令 方法的实现是通过修饰符ACC_SYNCHRONIZE实现的。
Java对象头:
synchronize用的锁是存在java的对象头里。如果是数组类型的对象,JVM用3个字宽存储对象头,如果不是数组类型,JVM用2个字宽存储,在32位JVM中,一个字宽位4个字节。 java对象头的存储结构:25位是hashcode,4位对象分带年龄,1位是否是偏向锁,2位所标志位。
锁的升级:
偏向锁、轻量级锁、重量级锁。
偏向锁:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,以后该线程在进入和退出时,不需要CAS操作来枷锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
轻量级锁:JVM会在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到所记录中,然后尝试用CAS将对象头中的Mark Word替换为指向所记录的指针。如果遇到竞争,锁就会膨胀会重量级锁。
不同锁的优缺点
锁 | 优点 | 缺点 | 适用场景 |
偏向锁 | 加锁和解锁不需要额外的消耗,与非同步方法相比,仅有纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问的同步块 |
轻量级锁 | 竞争的线程不会阻塞,提高程序性能 | 如果线程得不到锁,会自旋消耗CPU | 追求相应时间,同步块执行速度快 |
重量级锁 | 线程竞争不使用自旋,不消耗CPU | 线程阻塞,响应时间慢 | 追求吞吐量,同步块执行时间长 |
Java如何实现原子操作
java通过锁和循环CAS来实现原子操作
CAS:java的CAS使用Unsafe类,是C++实现的,主要通过lock前缀实现的,
CAS的问题:1.ABA问题 2.自旋时间开销大 3.只能保证单个变量原子操作
公平锁和非公平锁的实现不同
公平锁会先去读volatile变量,非公平锁会先去更新这个volatile ## 重排序 编译器和处理器为了优化程序性能,会对指令序列进行重新排序的手段。
happen-before原则:
程序顺序原则:一个线程内的每个操作,happens-before之后的操作
监视器原则:对一个锁的解锁happs-before随后的加锁
volatile原则:对于一个volatile对象的写happens-before对这个对象的读
传递性:如果a happens-before b,b happens-before c,则a happens-before c
happens-before意义
两个操作具有happens-before关系,并不代表一个操作肯定比另一个操作先执行,而是指前一个操作的结果对后一个操作课件。
JMM的基本方针
在不改变执行结果的前提下,尽可能地为编译器和处理器优化打开方便之门。