文章目录
- 1. 轻量级锁的核心原理
- 2. 代码演示
- 3. 轻量级锁的分类
- 4. 轻量级锁的膨胀
1. 轻量级锁的核心原理
轻量级锁的执行过程:在抢锁线程进入临界区之前,如果内置锁(临界区的同步对象)没有被锁定,JVM首先将在抢锁线程的栈帧中建立一个锁记录(LockRecord),用于存储对象目前Mark Word的拷贝,然后抢锁线程将使用CAS自旋操作,尝试将内置锁对象头的Mark Word的ptr_to_lock_record(锁记录指针)更新为抢锁线程栈帧中锁记录的地址
如果抢锁成功,线程就拥有了这个对象锁。然后JVM将Mark Word中的lock标记位改为00(轻量级锁标志),即表示该对象处于轻量级锁状态。JVM会将Mark Word中原来的锁对象信息(如哈希码等)保存在抢锁线程锁记录的Displaced Mark Word(可以理解为放错地方的Mark Word)字段中,再将抢锁线程中锁记录的owner指针指向锁对象
为什么复制对象头的部分信息到线程堆栈中的锁记录的Displaced Mark Word字段?
因为内置锁对象的Mark Word的结构会有所变化,Mark Word将会出现一个指向锁记录的指针,而不再存着无锁状态下的锁对象哈希码等信息,所以必须将这些信息暂存起来,供后面在锁释放时使用。
2. 代码演示
package innerlock;import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;public class InnerLockTest {int a=1;double b=1.1;public static void main(String[] args) throws InterruptedException {System.out.println(VM.current().details());Object obj=new Object();new Thread(()->{synchronized (obj) {System.out.println("t1:"+ClassLayout.parseInstance(obj).toPrintable());}try {Thread.sleep(4000);} catch (InterruptedException e) {e.printStackTrace();}},"t1").start();Thread.sleep(1000);//保证t1已经离开临界区new Thread(()->{synchronized (obj) {System.out.println("t2:"+ClassLayout.parseInstance(obj).toPrintable());}},"t2").start();}
}
上面的代码创建了两个线程,第一个线程t1获取到锁时无线程争夺资源,因此是偏向锁;当第二个线程去争夺t1持有的锁对象时,变成轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。(比如上面代码,先是线程1进行加锁变成偏向锁,然后是线程2来抢占,此时线程1已经离开临界区)
3. 轻量级锁的分类
1. 普通自旋锁
当有线程来竞争锁时,抢锁线程会在原地循环等待,而不是被阻塞,直到那个占有锁的线程释放锁之后,这个抢锁线程才可以获得锁。
自旋会消耗CPU,因此不可能无休止地进行自旋,默认情况下,自旋的次数为10次,用户可以通过-XX:PreBlockSpin选项来进行更改。
2. 自适应自旋锁
等待线程空循环的自旋次数并非是固定的,而是会动态地根据实际情况来改变自旋等待的次数
- 如果抢锁线程在同一个锁对象上之前成功获得过锁,JVM就会认为这次自旋很有可能再次成功,因此允许自旋等待持续相对更长的时间
- 如果对于某个锁,抢锁线程很少成功获得过,那么JVM将可能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源
4. 轻量级锁的膨胀
轻量级锁的优点:在多线程竞争不激烈的情况下,通过CAS机制竞争锁减少重量级锁产生的性能损耗。重量级锁使用了操作系统底层的互斥锁(MutexLock),会导致线程在用户态和核心态之间频繁切换,从而带来较大的性能损耗
轻量级锁的缺点:临界区代码执行耗时较长,在其执行期间,其他线程都在原地自旋等待,会空消耗CPU。因此,如果竞争这个同步锁的线程很多,就会有多个线程在原地等待继续空循环消耗CPU(空自旋),这会带来很大的性能损耗
在争用激烈的场景下,轻量级锁会膨胀为基于操作系统内核互斥锁实现的重量级锁
参考:《Java高并发编程卷2》 尼恩