synchronized关键字深入总结
CAS操作
Compare and swap/compare and exchange
原理:线程在操作一个资源的时候,假如这个资源是一个数字,线程要把这个数字进行+1操作,这个时候线程会把这个数值拿出来进行加一,在在写回去的时候,用操作之前拿出来的值和资源做对比,如果两个值相同,就把操作后的值写回资源,如果不相同,从新取值并做处理。
ABA问题:就是线程1进行cas操作的时候,在写回的过程中,资源被其他线程2.3.4线程更改,并且最终又改回和之前数值相同的值,这个时候线程1写回资源对比值相同,会认为资源没有被更改过,直接被写回。 解决办法,在资源上面增加一个版本号,版本号可以使数值类型,每次修改成功后给version+1,也可以使boolean类型,基础类型简单值不需要版本号。
当然,CAS的compare exchange操作中,也有可能会被其他线程打断的,为了保证cmpxchg 指令不被打断,在这个操作前面增加了lock指令(lock cmpxchg 指令 , 保证原子性操作)。
自旋锁代替悲观锁实验小程序:
https://gitee.com/zxj8524210/my-thread-test/blob/master/src/main/java/org/example/App10.java
对象的内存布局
New 一个对象,对象在内存是如何分布的? 具体是跟虚拟机JVM具体实现有关系的,这里主要说的是hotspot。
如果new一个T对象,它放在堆内存中的布局,首先是8个字节的markword,4个字节的类型指针classPointer(默认是开启压缩后的4个字节,通过这个指针可以找到T.class ),接下来是它的成员变量
(如果说一个对象在hotspot里实现,hotspot要求8字节对齐,也就是这个对象大小务必是8的整数倍,之前8个字节的markword+4个字节的classpointer不算成员变量的话=12个字节,就需要补齐4字节达到16个字节才可以,如果加上int变量四个字节=16个字节就不需要补齐了)
测试:
<dependencies>
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
</dependencies>
public class Test {
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
输出结果:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
synchronized早期版本和现在的区别
在早期,JDK synchronized被称之为重量级锁,每次加锁都需要向操作系统内核申请,走中断的0X80,从用户态到内核态的过程。
现在JDK的 synchronized做了一些优化,在某些状态下,加锁synchronized是不需要向内核申请的,直接在用户态就可以完成,synchronized优化的过程和markword息息相关。
synchronized原理
ava源码层级:synchronized(o)
字节码层级:monitorenter moniterexit
Hotspot层级:我也蒙圈中。。
锁升级
图解markword
markword占用8个字节64bit,从对象无锁态时,unused占25bit,如果有调用hashCode占31bit,unused占1bit,年龄分代占4bit,1bit占偏向锁位,最后两bit是锁标志。不同的锁状态位图代表的内容也不同。
偏向锁(默认匿名偏向):在运行加锁的代码段时,很多时候只有一个线程在运行,是没有必要设置锁竞争机制的, 第一个访问的线程直接把线程id扔到markword里面就行了,上图中的54位,hashcode会被存入到自己线程栈中LR指针指向的一个数据结构,这个数据结构记录着之前没上锁状态的markword。
设置 -XX:BiasedLockingStartupDelay=0 参数默认开启偏向锁。
明知道有多线程竞争的情况下,使用偏向锁 是否比 轻量级锁效率高?
不一定,因为偏向锁涉及到 锁撤销。
升级到轻量级锁(自旋锁):有别的线程来访问,首先会把之前的线程id从maekword撤销。每个线程都有自己的线程栈,每个线程都在自己的线程栈内部生成一个LR(lock record)锁记录。多个线程会用CAS的方式把LR的指针给markword,markword上指针指向哪个线程的LR,哪个线程就获得了锁。其他得线程会使用cas的方式(自旋,等待)获得锁。
升级到重量级锁:JDK1.6之前如果竞争加剧,有线程超过10次自旋,或者用-XX:PreBlockSpin参数设置线程自旋次数设置,或者自旋线程数量超过CPU核数的一半,就会从自旋锁升级为重量级锁。1.6之后加入 自适应自旋Adapative Self Spinning,JVM自己控制。
需要向OS申请,markword里面记录 object monitor,object monitor是JVM写的一个c++对象,这个c++对象内部去访问的时候是需要通过操作系统,经过操作系统拿到操作系统对应的那把锁
偏向锁和自旋锁都是用户态的,不需要和OS内核打交道
没有使用synchronized和使用synchronized对象对比
无锁到轻量级锁
public class Test {
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
输出结果:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 88 f1 d8 02 (10001000 11110001 11011000 00000010) (47772040)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以看出,在hotspot里,所谓的上锁,就是修改markword。锁信息被记录在markword里面。 markword里面记录了重要的信息,其中最重要的就是锁synchronized,另外还记录了GC的信息,还有hashcode。
开启偏向锁 和 匿名偏向锁
开启偏向锁:
启动程序休眠4秒 或 JVM添加启动参数-XX: BiasedLockingStartupDelay=0
实验代码:
https://gitee.com/zxj8524210/my-thread-test/blob/master/src/main/java/org/example/App11.java
可重入锁
Synchronized是可重入锁,举个例子:
如果同一个线程调用一个Synchronized方法时,这个方法调用了同一把锁的另一个方法,就是重入锁。
重入锁次数必须记录,因为要解锁几次必须要对应。不同锁实现是不一样的:
偏向锁实现重入锁:偏向锁记录在线程栈里,每次重入就把LR+1。
在new一个对象的时候,里面记录了hashcode,在调用的时候,会把hashcode记录在markword里面,如果一上锁(偏向锁),就变成了线程指针了,hashcode会被存入到自己线程栈中LR指针指向的一个数据结构,这个数据结构记录着之前没上锁状态的markword,每次重入都会在线程栈里添加一个LR,这个LR指向为null,再解锁的时候,从栈中pop LR,弹完之后锁就解开了。
自旋锁重入解锁 和 偏向锁类似,大体一样。
重量级锁会记录在objectMonitor的一个字段上。
有了自旋锁为什么还需要重量级锁:
重量级锁里面有各种各样的队列,比如waitset,里面有各种队列,每种队列都有自己的作用,用的使用来做竞争的,有的是用来做等待的,有的是用来做执行的。
因为自旋锁等待线程自旋,是需要消耗CPU资源的,随着自旋线程的增加cpu的消耗也会增加,向OS申请这把锁,OS会把这个线程仍到锁上面的一些队列里面,如果升级重量级锁,等待的线程非常多,会把这些自旋的线程放到waitset的队列里面进行等待(不需要消耗CPU时间),如果想要抢锁,则需要OS的进程调度把这个线程拿出来之后,这个线程才有资格持有这把锁。
偏向锁是否一定比自旋锁效率高?
不一定,在明确知道会有多个线程竞争的情况下,偏向锁肯定会涉及到锁撤销,这个时候直接是用自旋锁。
JVM启动过程中,明确的知道会有很多线程竞争,所以在默认的情况下,不启动偏向锁,等过一会时间再打开。
默认是4秒。
什么时候用自旋锁?什么时候用重量级锁?
自旋锁优势和劣势:加锁和释放锁不需要和OS打交道,加锁和解锁的效率比经过内核态的效率要高,但是占用cpu时间。
重量级锁优势和劣势:把等待执行的线程放到waitset队列里挂起,不占用CUP时间,但是加锁解锁的效率比较低。
线程执行时间短,使用自旋锁;线程执行时间长,使用重量级锁。
执行线程少,使用自旋锁;执行线程多,竞争大,使用重量级锁。
面试总被问,听了马士兵老师的课做了一下总结。
如果我有理解不对的地方,请留言指正,谢谢。
作者:戏入人生