你真的了解Synchronized加锁流程么?

Synchronized的前世今生

众所周知,jdk1.6之前Synchronized加锁是直接使用Linux提供的互斥量(mutex)来获取锁,当线程未获取锁时,会阻塞当前线程sleep,直到达到sleep限制或者被主动唤醒,再次获取锁直到获取锁为止。无论调用Linux的mutex还是sleep,线程获取锁时都会有CPU用户态到内核态的切换,其实,有时候对于锁资源只有一个或者两个线程交替竞争的,仍然需要使用系统调用,无疑对CPU资源是极大的消耗。因此,在jdk1.6针对Synchronized加锁进行了优化。按对锁的竞争程度划分成:偏向锁,轻量级锁,重量级锁。

锁优化后不同锁的定义

偏向锁

对于一个Synchronized修饰的锁对象的竞争只存在于一个线程,那么意味着锁对象只会被一个线程持有,我们定义当前时刻这个锁对象偏向当前线程。偏向锁获取锁的本质就是CAS修改Mark Word的线程id,有人就会有疑问了,既然是偏向锁,那只有一个线程获取,那为什么还需要CAS修改线程id呢?其实,这里只是定义偏向锁,只有一个线程获取锁,但实际情况有可能是,会有多个线程竞争获取锁,如果开启了可偏向,那么CAS保证只有一个线程能拿到偏向锁,而其他CAS失败的线程会走到锁撤销,再次获取锁走轻量级加锁流程,再次失败就会膨胀成重量级锁,因此这里CAS是为了兼容并发加锁的情况。

轻量级锁

通常情况对于轻量级加锁的定义是:两个线程交替执行的加锁,我认为是比较模糊的,因为交替执行不太容易界定的。下面我用几种交替执行的实验证明两个线程交替执行其实可以拿到三种情况,分别是偏向锁、轻量级锁、重量级锁。

拿到偏向锁

public static void main(String[] args) throws InterruptedException {
    Object o = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (o){
            System.out.println("t1-----"+ClassLayout.parseInstance(o).toPrintable());
        }
    });
    Thread t2 = new Thread(() -> {
        synchronized (o){
            System.out.println("t2----"+ClassLayout.parseInstance(o).toPrintable());
        }
    });
    t1.start();
    t1.join();
    TimeUnit.SECONDS.sleep(0);
    t2.start();
}
复制代码

image.png

拿到轻量级锁

public static void main(String[] args) throws InterruptedException {
    Object o = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (o){
            System.out.println("t1-----"+ClassLayout.parseInstance(o).toPrintable());
        }
    });
    Thread t2 = new Thread(() -> {
        synchronized (o){
            System.out.println("t2----"+ClassLayout.parseInstance(o).toPrintable());
        }
    });
    t1.start();
    t1.join();
    //TimeUnit.SECONDS.sleep(0);
    t2.start();
}
复制代码

image.png

拿到重量级锁

public static void main(String[] args) throws InterruptedException {
    Object o = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (o){
            System.out.println("t1-----"+ClassLayout.parseInstance(o).toPrintable());
        }
    });
    Thread t2 = new Thread(() -> {
        synchronized (o){
            System.out.println("t2----"+ClassLayout.parseInstance(o).toPrintable());
        }
    });
    t1.start();
    //t1.join();
    TimeUnit.SECONDS.sleep(1);
    t2.start();
}
复制代码

image.png 因此我认为轻量级加锁应该是当锁标志为无锁(001)情况的加锁就是轻量级加锁。并且我认为轻量级加锁是不会自旋的,因此也就不存在网上有人所说的轻量级锁是自旋锁。但是,Synchronized加锁是有自旋操作的,即当轻量级加锁CAS修改Lock Record地址失败后膨胀成重量级锁,并不是直接入队或者是通过Mutex获取锁。而是会继续通过自旋CAS获取锁,这一点其实也是借鉴JUC RentrantLock加锁流程的。

重量级锁

当有两个或者两个以上的线程同时竞争锁,Synchronized会最终膨胀成重量级锁。注意这里说的是最终,不是直接就是重量级锁。 本文着重讨论的是加锁流程,因此对象头的组成不作解释。

我所理解的Synchronized加锁流程图

Synchronized升级源码流程图.drawio.png

猜你喜欢

转载自juejin.im/post/7074989449905438733