synchronized的锁升级过程和详细的代码演示

在了解这个知识之前先了解Java对象的内容布局

一、锁升级的过程

在JDK1.0、1.2synchronized就是一个重量级的锁,效率不高,后来JDK对synchronized做了优化,在上锁的时候有一个锁升级的过程,
偏向锁-》轻量级锁(无锁,自旋锁、自适应自旋)-》重量级锁
如果大家不理解以上锁的类型可以看这篇文章重量级锁、自旋锁、轻量级锁、偏向锁、悲观、乐观锁等各种锁(转)

  1. 偏向锁:顾名思义就是偏向于第一个访问的线程。也就是说在无竞争的环境下,第一个线程访问的同步代码块,那么就会,会采用CAS机制,把这个同步代码块标记为有人在执行了并把这个线程ID写到锁对象的MarkWord中,这个锁就会偏向这个线程,下次有线程访问的时候就会判断是否有人在执行了,并且线程的ID是是之前访问过的线程访问,那么他就会直接进进入同步代码块中并执行,这样就会少一次cas的开销。偏向锁会有一个延迟,程序刚启动的大约三四秒内不会出现偏向锁。 解决方法有两个:
    (1)加sleep 5s再试
    (2)-XX:BiasedLockingStartupDelay=0 关闭关闭延迟开启偏向锁,JDK6默认开启偏向锁
    计算过hashcode值的对象不会加偏向锁,因为对象头没有空间放线程id了。
  2. 轻量级锁:轻量级锁体现轻量的点就在于自旋。当有任意的一个线程与持有偏向锁的线程进行资源竞争,那么锁就会升级成轻量级锁。每个线程的线程栈中都有一个lock record对象,哪个线程把自己的lock record的指针记录在锁对象头中,哪个线程就持有轻量级,抢的方式就是自旋(CAS)。如果自旋一定的次数还没拿到锁,jdk1.6之前默认有的线程自旋超过10次或者在那自旋等着的线程数超过整个cpu核数的二分之一,那么锁就会升级成重量级锁。jdk1.6之后加入了自适应自旋,自旋次数JVM自己控制。
  3. 重量级锁:jvm层面的两个标识,加锁解锁都会阻塞其他线程。

二、synchronized加锁的锁状态在markword中的布局

在这里插入图片描述
(1)无锁状态

public class NoLock {
    private static Object o = new Object();
    public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

在这里插入图片描述
问:可能有人问了,为什么上面图中锁状态最后两位表示锁标记位而打印出来的是前面的两位?
答:这就涉及到了大端模式和小端模式了,大端模式是高位字节存储在底地址段,低位字节存储在高地址段,小端模式是低位字节存储在底地址段,高位字节存储在高地址段,如:

0x 00 00 00 00 00 00 00 01 表示MarkWord的8个字节,最左边就是高位字节,最右边就是地位字节
小端模式在内存中存储顺序:
0x 01 00 00 00 00 00 00 00
大端模式在内存中存储顺序:
0x 00 00 00 00 00 00 00 01 

大家实在不懂的可以看一下这篇文章字节序–大端模式和小端模式

问:分代年龄最多可以改到多少?
答:我们从上图可以看到分代年龄(就是一个对象被垃圾回收多少次就会被移到老年代)占4bit,所以网上很多人说把分代年龄改成30多,20多来防止老年代被快速填满是错误的,因为4bit最大是15,所以分代年龄最大可以被调到15次。

如果锁对象在无锁状态下调用hashcode方法,MardWord中会存储hashcode的信息:

public class NoLock {
    private static Object o = new Object();
    public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        o.hashCode();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

在这里插入图片描述

(2)偏向锁状态

public class DeflectionLock {
    private static Object o = new Object();
    public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        lock();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
    private static void lock() {
        synchronized (o) {

        }
    }
}

在这里插入图片描述
问:根据上面我们的分析,应该出现偏向锁的,为什么没有出现呢?
答:偏向锁会有一个延迟,程序刚启动的三四秒内不会出现偏向锁

public class DeflectionLock {
    private static Object o;

    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        lock();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }

    private static void lock() {
        synchronized (o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

在这里插入图片描述
问:为什么全局静态锁对象o要在mian方法里面初始化而不是在创建的时候初始化,而且还没进入到同步代码块中,相应的锁状态就已经是偏向锁了?
答:jdk6默认开启偏向锁,但是是输入延时开启,也就是说,程序刚启动创建的对象是不会开启偏向锁的,几秒后后创建的对象才会开启偏向锁的。当类加载器将类加载到JVM中的时候就会创建静态变量,及对象是不会开启偏向锁。而且我们可以看到当加完偏向锁后,MardWord中的线程ID是不为空的。

public class DeflectionLock {
    private static Object o;

    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        o = new Object();
        o.hashCode();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        lock();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }

    private static void lock() {
        synchronized (o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

在这里插入图片描述
我们可以看到 计算过hashcode值的对象不会加偏向锁,而是直接加了轻量级锁。

(3)轻量级锁

public class LightweightLocking {
    private static Object o;
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000L);
        o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        new Thread(LightweightLocking::lock).start();
        // 加个睡眠是怕上面的线程没有执行完成而形成线程争用而升级为重量级锁
        Thread.sleep(3000L);
        lock();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }

    private static void lock() {
        synchronized (o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

在这里插入图片描述
在这里插入图片描述
在第一个线程获取到偏向锁并把自己的线程ID写到锁对象的MarkWord中,当主线程进入同步代码块发现不是自己的线程ID,锁就升级到了轻量级锁,这里我只是简单的来说,其实还是很复杂的,过程大家可以看下面的这张图,或者看这篇作者的文章偏向锁。退出synchronized代码块之后又会变成无锁的状态,因为轻量级锁会有一个锁撤销的过程
在这里插入图片描述
(4)重量级锁

public class HeavyweightLock {
    private static Object o;
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000L);
        o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        new Thread(HeavyweightLock::lock).start();
        lock();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }

    private static void lock() {
        synchronized (o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

在这里插入图片描述
在这里插入图片描述

以上就是synchronized的锁升级的详细过程,如果大家感觉有所收获的话,麻烦素质三连哈~

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值