Java多线程学习笔记
CAS
compare and swap,比较并且交换,同compare and exchange
CAS可以在没有加锁的情况下,保值多线程的一致性。
如何解决ABA问题:
- 加版本号
- 使用一个bool类型作标记
- 没有影响的话,可以不考虑此问题
CAS如何实现的
hotspot源码,cpp内,调用了汇编指令cmpxchg,如果是多核CPU,还需要lock
1 | lock cmpxchg 指令 |
所以,cmpxchg
本身是非原子性的
硬件:
lock指令在执行后面的时候锁定一个北桥信号
,不采用锁总线的方式
对象在内存中的布局
new Object()在内存占多少字节
使用Open JDK的工具JOL(Java Object Layout),Maven导入
1 | Object o = new Object(); |
输出:
对象在内存中的布局:
普通对象说明:
- markword + 指针类型,加起来是对象头
- 关于锁(synchronized)的信息都存在markword中
- 指针类型指向所属的类
- 实例数据存的是成员变量
- 对齐是保证对象内存大小可以被8整除,使得总线读取更快
查看Java命令行参数:
1 | java -XX:+PrintCommandLineFlags -version |
对new Object()
内存布局的解释:
- 前两行:合计是markword,共8字节
- 第三行,指针,指向类,4字节(补充:64位系统指针应该是8字节,但是被ClassPoniter压缩至4字节)
- 第四行:补齐,由于不能被8整除,补齐4字节,即loss due to the next…
- 由于没有成员变量,实例数据部分0字节
面试题:
1 | Object o = new Object();在内存中占有多少字节? -- 顺丰 |
答:管理synchronized等信息的markword 8字节;如果开启了ClassPointer的压缩,类型指针占4字节;成员变量没有,所以0字节;合计12字节,由于不能被8整除,补齐至16字节。如果没有开启ClassPointer的压缩,类型指针占8个,那么就正好16字节,不需要补齐。
分析这段代码的内存布局,和之前的new Object()
作个对比:
1 | synchronized(o) { |
输出:
注意第一行value的区别,说明锁信息是保存在对象的markword内的。
锁升级:
new - 偏向锁 - 轻量级锁(无锁 == 自旋锁、自适应自旋)- 重量级锁
由上图可知,锁状态
、GC标记
、分代年龄
都是存在对象的markword的8字节内的
分代年龄:
每次GC,如果对象没有被回收,则分代年龄+1,达到一定值(PS+PO GC默认15; CMS GC默认6),则对象从年轻代到老年代
注意:
由于分代年龄是4bit表示的,最大值是1111(二进制),即1 + 2 + 4 + 8 = 15,所以将值调到大于15是没有用的
自旋锁进一步升级的条件:
- 有线程自旋超过10次
- 在自旋的线程超过总核数的1/2
自适应自旋锁可以自动调整以上的两个值
用户态和内核态
用户态:ring3 应用程序大多跑着用户态
内核态:ring0 申请锁需要在内核态进行
在内核态申请到锁之后,线程会进入一个阻塞的队列,wait的线程不消耗资源,区别于自旋锁依赖频繁的CAS会消耗大量CPU资源
锁降级:
锁降级只存在在GC的时候,其他线程已经不持有锁,只发生在VMThread访问的时候,所以没意义,一般认为不存在降级
锁消除
举例说明:
1 | public void add(String str1, String str2) { |
解释:
虽然StringBuffer
是线程安全的,即append()
方法是被synchronized修饰的,但是在本add方法内,锁会被消除。原因是,sb是局部变量而不是成员变量(栈私有),不可能存在竞争,所以sb对象的锁会被JVM消除。
锁粗化
举例说明:
1 | public String test(String str) { |
解释:
JVM检测到一连串对同一对象的加锁和解锁,JVM会粗化加锁范围,使得锁只需要加1次。