Java多线程学习笔记

Java多线程学习笔记

CAS

compare and swap,比较并且交换,同compare and exchange

image-20200726215411185

CAS可以在没有加锁的情况下,保值多线程的一致性。

如何解决ABA问题:

  1. 加版本号
  2. 使用一个bool类型作标记
  3. 没有影响的话,可以不考虑此问题

CAS如何实现的

hotspot源码,cpp内,调用了汇编指令cmpxchg,如果是多核CPU,还需要lock

1
lock cmpxchg 指令

所以,cmpxchg本身是非原子性的

硬件:

lock指令在执行后面的时候锁定一个北桥信号,不采用锁总线的方式


对象在内存中的布局

new Object()在内存占多少字节

使用Open JDK的工具JOL(Java Object Layout),Maven导入

1
2
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());

输出:

image-20200726222016355

对象在内存中的布局:

image-20200726222735685

普通对象说明:

  • markword + 指针类型,加起来是对象头
  • 关于锁(synchronized)的信息都存在markword中
  • 指针类型指向所属的类
  • 实例数据存的是成员变量
  • 对齐是保证对象内存大小可以被8整除,使得总线读取更快

查看Java命令行参数:

1
2
3
4
5
6
$ java -XX:+PrintCommandLineFlags -version

-XX:G1ConcRefinementThreads=4 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:MinHeapSize=6815736 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC
java version "14" 2020-03-17
Java(TM) SE Runtime Environment (build 14+36-1461)
Java HotSpot(TM) 64-Bit Server VM (build 14+36-1461, mixed mode)

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
2
3
synchronized(o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

输出:

image-20200726225404529

注意第一行value的区别,说明锁信息是保存在对象的markword内的。


锁升级:

new - 偏向锁 - 轻量级锁(无锁 == 自旋锁、自适应自旋)- 重量级锁

image-20200726233046984

由上图可知,锁状态GC标记分代年龄都是存在对象的markword的8字节内的

分代年龄:

每次GC,如果对象没有被回收,则分代年龄+1,达到一定值(PS+PO GC默认15; CMS GC默认6),则对象从年轻代到老年代

注意:

由于分代年龄是4bit表示的,最大值是1111(二进制),即1 + 2 + 4 + 8 = 15,所以将值调到大于15是没有用的

自旋锁进一步升级的条件:

  1. 有线程自旋超过10次
  2. 在自旋的线程超过总核数的1/2

自适应自旋锁可以自动调整以上的两个值


用户态和内核态

用户态:ring3 应用程序大多跑着用户态

内核态:ring0 申请锁需要在内核态进行

在内核态申请到锁之后,线程会进入一个阻塞的队列,wait的线程不消耗资源,区别于自旋锁依赖频繁的CAS会消耗大量CPU资源


锁降级:

锁降级只存在在GC的时候,其他线程已经不持有锁,只发生在VMThread访问的时候,所以没意义,一般认为不存在降级


锁消除

举例说明:

1
2
3
4
public void add(String str1, String str2) {
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}

解释:

虽然StringBuffer是线程安全的,即append()方法是被synchronized修饰的,但是在本add方法内,锁会被消除。原因是,sb是局部变量而不是成员变量(栈私有),不可能存在竞争,所以sb对象的锁会被JVM消除。


锁粗化

举例说明:

1
2
3
4
5
6
7
8
9
public String test(String str) {
int i = 0;
StringBuffer sb = new StringBuffer();
while(i < 100) {
sb.append(str);
i++;
}
return sb.toString();
}

解释:

JVM检测到一连串对同一对象的加锁和解锁,JVM会粗化加锁范围,使得锁只需要加1次。