synchronized锁优化的背景
用锁能够实现数据的安全性,但是会带来性能的下降
无锁能够基于线程并行提升程序性能,带来安全性的下降
java5 synchronized默认是重量级锁,java6以后引入偏向锁和轻量锁,java15 逐步废弃了偏向锁
锁升级的过程
无锁---》偏向锁(一个线程重复请求)---》轻量锁(cas)---》重量锁
为什么每个对象都可以成为一个锁?
Monitor 可以理解为一种同步工具,也可以理解为一种同步工具,常常被描述为一个java对象。java对象是天生的Monitor,每个对象都有成为Monitor的潜质,因为在java设计中,每个对象自打娘胎里出来就带着一把看不见的锁,它叫做内部锁或者Monitor锁(管程)。
一、无锁:
初始状态,一个对象被实例化后,如果还没有任何线程竞争锁,那么它就是无锁状态(标志位是001)
public static void main(String[] args) {
Object o = new Object();
System.out.println("10进制:" + o.hashCode());
System.out.println("16进制:" + Integer.toHexString(o.hashCode()));
System.out.println("2进制:" + Integer.toBinaryString(o.hashCode()));
//2进制:1100111011100110010011110110110
// 1100111011100110010011110110110
//如果没有调用 hashcode 方法 则Mark Word 的打印结果不会体现出来2进制的hash值
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
二、偏向锁:
当一个同步代码块,被同一个线程多次访问,由于只有一个线程,那么该线程在后续访问时便会自动获得锁(标志位101)。偏向锁不涉及用户态(自己写的代码)到内核态(jvm调用的底层代码)的转化,不必要直接升级为最高级(重量锁)。JVM不需要与操作系统协商设置Mutex(争取内核),它只需要记录下线程ID,就标志自己获取了当前锁,不用操作系统接入。在没有其他线程竞争的时候,一直偏向(偏心)当前线程,当前线程可以一直执行。
结论:当一个线程访问synchronized代码块的时候,JVM使用CAS操作把线程指针ID记录到MarkWord当中,并修改偏向标志,标示当前线程获得该锁。锁对象变成偏向锁(通过CAS修改对象头里锁的标志位),字面意思是“偏向于第一个获得它的线程”的锁,执行完同步代码块后,线程并不会主动释放偏向锁。当该线程第二次到达同步代码块时会判断此时持有的线程是否还是自己(持有锁的线程ID也在对象头里),JVM通过对象的Mark Word 判断:当前线程ID还在,说明还持有着这个对象的锁,就可以继续进入临界区工作。由于之前没有释放锁,这也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁没有额外开销,性能极高。
JVM 查看偏向锁基本配置信息命令:java -XX:+PrintFlagsInitial |grep BiasedLock*
如图:jdk1.8
偏向锁在jdk1.6之后是默认启动的,但是启动时间 延时4秒钟
1.所以需要添加jvm参数 -XX: BiasedLockingStartupDelay=0 让其程序启动时立刻启动,不延时。
开启偏向锁参数:
-XX:+UseBiasedLocking -XX: BiasedLockingStartupDelay=0
关闭偏向锁:关闭之后程序默认直接进入 轻量级锁状态
-XX:-UseBiasedLocking
2.也可以睡眠4秒以上等待偏向锁启动,再观察对象头中的锁标志位
public static void main(String[] args){
try {TimeUnit.MILLISECONDS.sleep(5000);} catch (InterruptedException e)
{e.printStackTrace();}
Object o = new Object();
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
3.偏向锁的撤销
当有其他线程逐步来竞争锁时,就不能再使用偏向锁了,而是要升级为轻量锁。偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。撤销需要等待全局安全点(该时间点没有任何字节码执行),同时检查持有偏向锁的线程是否还在执行。
①第一个线程正在执行synchronized代码块,它还没有执行完,其他线程来抢夺,这是偏向锁会被取消掉,升级为轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码块,而正在竞争的线程会进入自旋等待获取该轻量锁。
②第一个线程执行完成synchronized代码块,则将对 象头设置为无锁状态并且撤销偏向锁,重新偏向。
三、轻量锁
有线程参与锁的竞争,但是获取锁的冲突时间极短,本质就是CAS自旋锁(标志位 00),是为了在线程近乎交替执行同步块时提高性能。
多线程竞争,但是任意时刻最多只有一个线程竞争,既不存在锁竞争太过激烈的情况,也没有现场阻塞。
1.轻量锁的获取:
假如线程A已经拿到锁,这时线程B又来抢该对象锁,由于该对象锁已经被A拿到,当前该锁已经是偏向锁。而线程B在争抢锁时发现对象头Mark Word中的线程ID 不是线程B自己的线程ID,而是A线程的ID,那么线程B就会进行CAS操作希望能获取到锁。
情况1:如果锁获取成功,直接替换Mark Word中的线程ID 为B自己的ID(A变成B),重新偏向于其他线程(即将偏向锁交给了其他线程,当前线程被释放了锁),该锁会保持偏向状态,A线程OVER,B线程成功上位。
情况2:如果锁获取失败,则偏向锁升级为轻量级锁(设置偏向锁标志位为0,设置锁标志位为00),此时轻量锁是由原来持有偏向锁的线程持有,继续执行同步代码块 ,而正在竞争的线程B会进入自旋等待获取该轻量锁。
//偏向锁默认延迟4秒启动,所以没有设置偏向锁立即启动,会直接使用轻量锁
Object o = new Object();
new Thread(()->{
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}).start();
2.轻量锁与偏向锁的区别和不同
①偏向锁只是一个线程,没有其他线程竞争。轻量级锁是两个及以上线程进行争抢。轻量级锁争抢失败时,会自旋尝试获得锁。但是偏向锁只有一个线程,不会自旋。
②轻量级锁每次退出同步块时都会释放锁,而偏向锁是在竞争发生时才释放锁。
四、重量级锁
java中的synchronized重量级锁(标志位10),是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始位置插入monitor enter 指令,在结束位置插入 monitor exit 指令。当线程执行monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了锁。会在Monitor 中的ower中记录当前线程的ID,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取这个Monitor.
Object o = new Object();
new Thread(() -> {
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}, "t1").start();
new Thread(() -> {
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}, "t2").start();
五、锁的指向
偏向锁:Mark Word 存储的是偏向的线程ID。
轻量级锁:Mark Word 存储的是指向线程栈帧中Lock Record的指针。
重量级锁:Mark Word 存储的是指向堆中的monitor对象的指针。
六、hashcode问题
hashcode与偏向锁不共存,如果在偏向锁执行同步代码块时调用获取hashcode的方法,则该偏向锁会立即撤销,偏向锁会膨胀为重量级锁。
七、锁的优缺点