1.Java内存模型
内存模型决定了程序上的每个点可以读取什么值。
2.CPU缓存与指令重排序
为提高程序运行性能,CPU做了很多优化,主要体现在2点。
- CPU高速缓存
- 指令重排序
CPU高速缓存
CPU读取数据时先在L1缓存查找,再从L2缓存查找,再从L3缓存查找,然后是内存。一般多个CPU共享一个L3缓存。
多处理器时,单个CPU对数据的改动需要通知给其他CPU,那么就需要采取一定侧策略来保证缓存一致性。
指令重排序
在不影响单线程执行结果的前提下,CPU允许将多条指令不按程序规定的顺序分发给各个电路计算单元处理,重排序的指令无依赖关系。
又比如当CPU写缓存时,发现缓存区块正被其他CPU占用,为提高性能可将后面无依赖关系的读命令先执行。
另外,在Java中,JIT编译器会对执行的程序做程序的优化,也算是指令重排序。比如热点代码的执行优化、代码的等效替换、顺序替换等。
内存屏障
CPU提供了内存屏障指令来解决上述两个问题。
3.volatile语义
解决数据可见性问题,对volatile变量的读(在并发情况下)一定是最新值。
volatile实现原理
针对volatile变量进行写操作时,JIT编译器生成的汇编指令会有Lock前缀指令。而Lock前缀指令会引发以下几件事:
- Lock前缀指令相当于内存屏障,内存屏障之后的指令不能排序到内存屏障之前
- 当前处理器缓存数据回写到主存
- 这个回写操作会使其他CPU缓存了该内存地址的数据无效,多处理器下,为保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器会嗅探自己缓存的值是否过期,如果是则缓存置为无效,当读取这个数据时重新从内存中读入到缓存里。这个过程也可以理解为线程间的通信。
- volatile从Java语义上禁止JIT编译器的重排序优化。
4.CAS
CompareAndSwap,线程会拿当前读取到的值与当前内存中的实际值做比较,若相同则修改为目标值,否则再次执行CAS。CAS是一个原子操作。
即以无锁编程方式解决并发问题。
Java中的Unsafe类提供了底层的CAS功能方法,但无法直接调用(编译不通过),不过可以通过反射的方式使用。所以Java也是矛盾的语言,既从编译级别不让你使用Unsafe类,又可以通过反射以后门的方式去使用Unsafe类。
Java中的JUC包中,很多工具都是基于CAS实现的。
CAS的不足:
- 如果线程过多,CAS操作长时间不成功,会有很大的CPU消耗。
- 无法观测到ABA问题。
- CAS仅针对单个变量操作。
5.锁粗化
JIT编译器的优化,虚拟机探测到过于零碎的对同一个对象加锁,会把锁范围扩大,一般是代码写法有问题。
6.锁消除
JIT编译器的优化,虚拟机检测到不可能存在锁竞争,进行锁消除,比如单线程情况下使用StringBuffer。
需要开启逃逸分析,逃逸分析除了可以锁消除,一定程度上还可以减少冗余对象的创建。比如在一个循环里创建大量对象,虚拟机认为这些大量对象都在一个线程内,没必要创建大量对象来占用堆内存空间,直接将对象的属性值压入虚拟机栈内,用完即可将栈帧释放,从而提高空间利用率和性能。
7.对象内存布局
对象在内存中存储布局可分为3块区域:对象头、实例数据、对齐填充。我们重点关注对象头。
对象头又包括两部分:
- 第一部分存储了HashCode、GC年龄、锁标志位、偏向锁标志等,这部分被称为MarkWord
- 第二部分是类型指针,指向类元数据,用于确定是哪个类的实例。
- 另外,如果是数组还存储数组长度
偏向锁、轻量级锁、重量级锁及所膨胀过程都是跟MarkWord打交到。
8.自旋锁
首先,自旋锁是个状语,线程以自旋的方式争取锁。挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来很大的压力。共享数据的锁定状态只会持续很短的时间,为了这段时间去挂起和恢复线程并不值得,不妨让后面的线程“稍等下”,以自旋的方式尝试获取锁。
JDK1.6默认开启了自旋锁,并引入了自适应自旋,自旋次数基于程序运行情况动态变化。
9.偏向锁
虚拟机认为,默认情况下未必存在锁竞争。偏向锁是只有一个线程获取锁的情况下使用,个人认为这也是锁消除的一种实现方式。
如果有另一个线程尝试去获取该对象的锁,则锁膨胀为轻量级锁。
偏向锁MarkWord存储结构
上图中,MarkWord本该存储HashCode的位置用于存储现成引用,那此时HashCode应该存哪里呢?
我在知乎上找到了些比较靠谱的答复。
当程序调用未覆盖的Object.hashCode()方法时,System.identityHashCode()会生成hashCode存储在MarkWord中。
当一个对象已经计算过IdentityHashCode,它就无法进入偏向锁状态,因为此时MarkWrod里HashCode位置已经有值了。
当一个对象正处于偏向锁状态时,并且需要计算IdentityHashCode则锁膨胀位轻量级锁。
Hotspot VM假定程序中很少使用对象去计算HashCode。换句话说,如果很多对象都计算了HashCode,那么Hotspot被迫使用比较不友好的模式。
10.轻量级锁
当锁竞争不大时,一个线程持有轻量级锁,另一个线程以自旋CAS的方式尝试获取锁。当竞争的线程大于2个以上时,锁膨胀为重量级锁。
线程会在栈帧中创建一个锁记录空间(Lock Record),用于存储锁对象的MarkWord,然后虚拟机将使用CAS操作尝试将LockRecord的指针更新到对象的MarkWord中,并将锁标志位更新位00。
解锁的过程就是将LockRecord中MarkWord的副本替换回MarkWord。
11.重量级锁
也称之为监视器锁。MarkWord指向监视器对象的引用。Java线程会映射到操作系统的线程,线程的挂起与唤醒需要由用户态切换到内核态完成。
12.AQS
几个关键属性:
state:锁重入次数,0即无锁
exclusiveOwnerThread:持有当前锁的线程
CLH队列等等。。
有闲心我再补充说明 画几张图~
具体可看 javadoop.com/post/Abstra… 这篇文章对AQS的源码解析,写得非常好
参考资料
《深入理解Java虚拟机》
《Java并发编程实战》
《Java并发编程艺术》
javadoop:javadoop.com/post/Abstra…