Synchronized原理

112 阅读4分钟

Java对象头

image.png

  • 当对象头最后三位为001时,处于无锁状态
  • 当对象头最后三位为101时,处于偏向锁状态
  • 当对象头最后三位为000时,处于轻量锁状态
  • 当对象头最后三位为010时,处于重量锁状态

锁的升级流程

image.png

轻量级锁

轻量级锁在线程交替进行的情况下相对于重量级锁效率有所提高。这是因为在申请轻量级锁时不会进行系统调用,减少了用户态到内核态的切换。

轻量级锁加锁过程:

  1. 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录对象,内部可以存储锁定对象的Mark Word信息

image.png

  1. 让锁记录中Object reference指向锁对象,并尝试用cas替换Object中的Mark Word,将Mark Word的值存入锁记录中

image.png

  1. 如果cas替换成功,对象头中存储了锁记录和状态00,表示该线程给对象加锁,此时如图

image.png

  1. 如果cas失败,此时有两种情况
    1. 一种是已经有线程对obj对象进行了加锁,证明有竞争发生,此时会发生锁膨胀
    2. 如果是自己执行了synchronized锁重入,那么线程中将会再添加一条Lock record作为重入的计数

image.png

  1. 锁膨胀

image.png

代码验证轻量级锁

在这里准备打印出对象头信息来观察对象头的bit变化,使用jol查看对象头

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.14</version>
</dependency>

由于jol打印的对象头不太好观察,这里写了一个方法进行解析

public static String parseHeader(String header) {
    int index = header.indexOf('\n');
    int i2 = header.indexOf('\n', index + 1);
    header = header.substring(i2+1);
    int toFRBracket = header.indexOf(')');
    int toValBracket = header.indexOf('(',toFRBracket+1);
    int toValEndBracket = header.indexOf(')',toValBracket);

    String first4bit = header.substring(toValBracket+1,toValEndBracket);
    int endline = header.indexOf('\n');
    header = header.substring(endline+1);
    String second4bit = header.substring(toValBracket+1,toValEndBracket);

    String[] firsts = first4bit.split(" ");
    String[] seconds = second4bit.split(" ");
    StringBuilder res = new StringBuilder();
    StringBuilder out = new StringBuilder();
    for (int i = seconds.length-1; i >= 0; i--) {
        res.append(seconds[i]).append(" ");
        out.append(seconds[i]);
    }
    for (int i = firsts.length-1; i >= 0; i--) {
        res.append(firsts[i]).append(" ");
        out.append(firsts[i]);
    }

    String unused = out.substring(0, 25);
    String hashcode = out.substring(25, 56);
    String unused2 = out.substring(56, 57);
    String age = out.substring(57, 61);
    String bias = out.substring(61, 62);
    String sort = out.substring(62, 64);
    System.out.println("unused_25bit             " +
                        " hashcode_31bit                  " +
                        "  " + "age  " + "b " + "st ");
    System.out.println(unused + " " + hashcode + " " + unused2 + " " + age + " " + bias + " " +sort);

    return res.toString();
}

我们先不谈论偏向锁,可以使用虚拟机参数-XX:-UseBiasedLocking关闭偏向锁。

public class Synchronized {

    public static void main(String[] args) throws InterruptedException {
        ThreadTest test = new ThreadTest();
        Thread t1 = new Thread(test,"first thread");
        Thread t2 = new Thread(test,"second thread");
        t1.start();
        t2.start();
    }
    
    static class ThreadTest implements Runnable{

        int count = 0;

        @Override
        public void run() {
            synchronized (this) {
                System.out.println("thread: "+Thread.currentThread().getName());
                System.out.println(parseHeader(ClassLayout.parseInstance(this).toPrintable()));
                System.out.println();
                count++;
            }
        }
    }
}
    

在synchronized处打下断点,设置为thread断点类型

image.png

使用debug启动程序

image.png 目前是first thread线程执行到了断点处,直接令first thread将run方法执行完毕,观察结果

image.png 可以看到对象头的最后三位是000,处于轻量锁状态,接下来执行second thread线程,观察输出

image.png

image.png

可见second thread线程获取到的锁也是轻量锁,此时就是线程的交替进行。下面我们来看看有竞争的情况 ,还是debug启动,让first thread执行进入run方法

image.png

此时选择second thread,点击箭头尝试进入run方法

image.png 此时就发生了锁的争抢,first thread会进行所升级,进入到重量级锁状态,让first thread线程执行完,观察输出

image.png

此时second thread 线程也可以执行了,观察输出

image.png

可以看到无论是线程1还是线程2获取到的锁的对象头的最后三位都是010,表示重量级锁。

偏向锁

偏向锁默认是开启的,不过延迟了4s,可以在加虚拟机参数设置无延迟开启偏向锁XX:BiasedLockingStartupDelay=0

虚拟机为对象加偏向锁的步骤:

  • 检查锁对象的mark word的线程id
  • 如果为空则使用cas设置为当前线程id,设置成功则获取锁成功,失败则撤销偏向锁
  • 如果不为空就检查线程id是否和当前线程的id匹配,如果匹配则获取锁成功,否则撤销偏向锁

这在轻量锁的基础上简化了cas的步骤,一定程度上提高了效率,但是偏向锁只适用与一个线程反复的获得锁

代码验证偏向锁

还是上方的代码,只不过虚拟机参数变为XX:BiasedLockingStartupDelay=0,先让线程1执行完毕,观察输出:

image.png

可以看到线程1获取到的锁的对象头的最后三位是101,代表是偏向锁,此时执行线程2的话,就会发现对象头的mark word线程id和当前线程id不匹配,然后撤销偏量锁,升级到轻量级锁

image.png 可以观察到对象头的最后三位是000,代表轻量级锁

重量级的锁自旋

重量级锁竞争的时候,还可以使用自选来进行优化,如果当前线程自旋成功,也就是说在同步代码快中的线程执行完毕,此时当前线程可以避免阻塞,一把来说自旋时间会设置为少于线程上下文切换的时间。当然,锁的自旋我们并不能控制