锁升级过程与优化操作
前文我们学习了CAS自旋锁知道CAS对应的就是一条指令操作,属于一种轻量级锁,那么有轻必有重,从无锁到轻量级锁到重量级锁是一个升级过程,此文我们对锁升级的过程以及一些优化锁的操作一探究竟。
1. 锁升级
从前文 《程序员不可能不知道的锁策略》 一文中,结合锁策略,我们就可以总结出,synchronized
具有以下特性 (只考虑JDK1.8) :
- 开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁.
- 开始是轻量级锁实现,如果锁被持有的时间较长,就转换成重量级锁.
- 实现轻量级锁的时候大概率用到的自旋锁策略
- 是⼀种不公平锁
- 是⼀种可重入锁
- 不是读写锁
加锁工作过程:
JVM将synchronized锁分为无锁、偏向锁(轻量级锁)、自旋锁、重量级锁状态。会根据情况,进行依次升级。
-
无锁
不存在锁 -
偏向锁
偏向锁是Java中最轻量级的锁升级策略。 当一个线程获取到锁时,该锁会进入偏向模式,并将获取到锁的线程ID记录下来(做标记)
。接下来,当这个线程再次请求同一个锁时,无需竞争,可以直接获取,(避免了加锁解锁的开销)
。偏向锁本质上相当于"延迟加锁".能不加锁就不加锁,尽量来避免不必要的加锁开销.但是该做的标记还是得做的,否则⽆法区分何时需要真正加锁.这种策略适用于大部分情况下都是由同一个线程持有锁的场景。 -
轻量级锁
当多个线程同时请求同一个锁时,偏向锁就无法满足需求,锁升级到轻量级锁。 轻量级锁使用CAS(Compare and Swap)操作来避免线程的阻塞和唤醒,从而提高并发性能。当线程获取轻量级锁失败时,锁会升级到下一个阶段。
⾃旋操作是⼀直让CPU空转,比较浪费CPU资源.
因此此处的自旋不会⼀直持续进行,而是达到⼀定的时间/重试次数,就不再自旋了.
也就是所谓的"自适应"
-
自旋锁
自旋锁是轻量级锁升级的一种策略。 当线程在获取轻量级锁失败后,它不会立即被挂起,而是会自旋一段时间,不断尝试获取锁。自旋锁的目的是为了避免线程的上下文切换,提高性能
。但如果自旋时间过长或者自旋次数达到一定阈值,仍然没有成功获取锁,那么锁将会升级到下一个阶段。 -
重量级锁
重量级锁是Java中最重量级的锁升级策略。 当自旋锁尝试获取锁的次数达到阈值时,锁会进入重量级模式。重量级锁采用操作系统的互斥锁实现,真正的调用CPU的指令LOCK
2. 锁优化操作
2.1 锁粗化
概念:⼀段逻辑中如果出现多次加锁解锁操作,JVM会⾃动进⾏锁的粗化.
例如: 在公司业务当中,涉及多段方法的调用,每个方法都有加锁解锁操作,但是这多段方法之间并没有其他操作,可以在第一段方法前加锁,最后一段方法解锁,其中的其他方法不涉及加锁解锁操作
实际开发过程中,使用细粒度锁,是期望释放锁的时候其他线程能使用锁.
但是实际上可能并没有其他线程来抢占这个锁.这种情况JVM就会自动把锁粗化,避免频繁申请释放锁.
把方法级别的细粒度锁,粗化成业务级别的粗粒度锁
2.2 锁消除
概念:JVM判断锁是否可消除.如果可以,就直接消除.
解释: 锁消除是synchronized的一种优化策略
程序员写代码的时候,什么时加synchronized,什么不加,JVM管不了,但是代码在编译运行的时候JVM可以知道加了synchronized的代码是对变量读还是写,还知道当前是多线程状态还是单线程状
如果加了synchronized的代码,并没有对变量进行写操作,那么synchronized对应的锁就不会生效(不会被编译成LOCK)
线程安全问题只有多个线程对同一个共享变量进行写操作时才会发生,如果没有写操作,那么
synchronized就没有必要 ,所有JVM不会真正的去加锁,这个现象叫做锁消除
只有JVM100%确定没有问题的时候才会执行锁消除操作,并不一定所有的代码都会发生
例如:
有些应⽤程序的代码中,⽤到了synchronized,但其实没有在多线程环境下.(例如StringBuffer)
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");
此时每个append的调用都会涉及加锁和解锁.但如果只是在单线程中执行这个代码,那么这些加锁解锁操作是没有必要的,白白浪费了一些资源开销.