synchronized底层原理

https://www.jb51.net/article/183984.htm
https://blog.csdn.net/mulinsen77/article/details/88635558
https://www.yuque.com/wanghuaihoho/gobbon/szufr0
https://blog.csdn.net/tongdanping/article/details/79647337
https://www.bilibili.com/video/BV1tz411q7c2?p=1

1、用户态与内核态

现在的操作系统(linux,windows),一般分为内核态kernel和用户态(普通的用户程序),所有用户态的程序要访问硬盘,内存,网卡等,必须经过内核态的允许,内核态可以执行所有指令,用户程序要执行指令必须经过内核态来调用,JVM对于操作系统来说也就是一个普通程序,因此,如果jvm要操作内存必须经过内核态的允许。

早期的synchronized是重量级锁,对于某个资源加锁的时候,需要经过操作系统的老大 - 内核态的允许(锁属于操作系统的核心资源,准确的说是互斥锁mutex),经过内核态的线程调度,才能拿到锁,这把锁叫重量级锁,重是因为要经过内核态允许的这一个步骤。

后面对synchronized进行了优化,在某些特定条件下,不需要经过内核态的允许,只需要在用户空间就能解决,比如:cas只是从内存中读取值,然后进行比较,因此,没必要经过内核态的允许这一步骤,因此,cas叫轻量级锁。

因此,早期synchronized效率低的是因为要经过内核态。

2、使用工具查看对象的内存布局

主要用来观察对象内部怎么实现。
依赖:

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

实例1:

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

结果:
在这里插入图片描述
实例2:如何读取VALUE

public class HelloJol {
    public static void main(String[] args) {
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        //锁定某个对象,实际上是修改对象的MarkWork内容 
        synchronized (o){
             System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

在这里插入图片描述
header中前8个字节按照平时习惯的从高位到低位的展示为:
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 后两位为01,前面又是0,对照表(在下面),是无锁
00000000 00000000 00000000 00000000 00000011 00001001 11110100 10101000 后两位为00,是轻量级锁

普通对象加synchronized,直接升级为轻量级锁。

3、synchronized字节码

在这里插入图片描述

  • 当执行monitorenter指令时,当前线程将试图获取对象锁所对应的monitor的持有权,当该对象的monitor
    的计数器为0,那线程可以成功取得monitor,并将计数器值设置为1,取锁成功。
  • 如果当前线程已经拥有monitor的持有权,那它可以重入这个monitor,重入时计数器的值也会加 1。
  • 倘若其他线程已经拥有该对象的monitor的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放monitor(锁)并设置计数器值为 0 ,其他线程将有机会持有monitor

4、监视器对象(管程对象或Monitor)

JVM中的同步是基于进入与退出监视器对象(管程对象)(Monitor)来实现的,每个对象实例都会有一个Monitor对象,Monitor对象会和Java对象一同创建并销毁。Monitor对象是由C++来实现的。

这就是为什么任何对象都可以作为锁,线程在获取锁的时候,实际上就是获得一个监视器对象 (monitor) ,monitor 可以认为是一个同步对象,所有的Java对象是天生携带monitor

Monitor源码:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //重入时的计数器
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;  //_owner指向持有ObjectMonitor对象的线程
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

1、EntryList阻塞队列

当多个线程同时访问一段同步代码时,这些线程会被放到一个EntryList集合中,处于阻塞状态的线程都会被放到该列表当中。接下来,当线程获取到对象的Monitor时,Monitor是依赖于底层操作系统的mutex lock(互斥锁)来实现互斥的,线程获取mutex成功,则会持有该mutex,这时其他线程就无法再获取到该mutex

2、WaitSet等待集合

如果线程调用了wait方法,那么该线程就会释放掉所持有的mutex,并且该线程会进入到WaitSet集合(等待集合)中,等待下一次被其他线程调用notify/notifyAll唤醒。如果当前线程顺利执行完毕方法,那么它也会释放掉所持有的mutex

5、锁升级

1、JDK对锁的优化

JDK 1.5开始,并发包引入了Lock锁。

  • Lock同步锁是基于Java来实现的,因此Lock锁的获取与释放都是通过Java代码来实现与控制的;
  • synchronized是基于底层操作系统的Mutex Lock来实现的,每次对锁的获取与释放动作都会带来用户态与内核态之间的切换,这种切换会极大地增加系统的负担;在并发量较高时,也就是说锁的竞争比较激烈时,synchronized锁在性能上的表现就非常差。

JDK 1.6开始,synchronized锁的实现发生了很大的变化;JVM引入了相应的优化手段来提升synchronized锁的性能,这种提升涉及到偏向锁、轻量级锁及重量级锁等,从而减少锁的竞争所带来的用户态与内核态之间的切换;这种锁的优化实际上是通过Java对象头中的一些标志位来去实现的;对于锁的访问与改变,实际上都与Java对象头息息相关。

2、锁对象头 - Mark Work部分

对象实例在堆当中会被划分为三个组成部分:对象头、实例数据与对齐填充。

对象头主要也是由3块内容来构成:Mark Word (标记词)、指向类的指针、数组长度
Mark Word 包含:锁信息,GC分代年龄,hashCode

Mark Word :8字节,64位,一个字节八位
在这里插入图片描述

  • 无锁:没有锁对资源进行锁定,所有线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。其他修改失败的线程会不断重试,直到修改成功,如CAS原理和应用是无锁的实现。
  • 偏向锁:偏向锁是指一段同步代码一直被一个线程访问,那个该线程会自动获取锁,降低获取锁的代价。当前线程把自己的id贴到了markwork上。
  • 轻量级锁(也叫自旋锁):是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。通过cas操作和自旋来解决加锁问题,自旋超过一定的次数或者已经有一个线程在自旋,又来一个线程获取锁时,轻量级锁会升级为重量级锁。
  • 重量级锁:升级为重量级锁,等待锁的线程都会进入阻塞状态。锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。

偏向锁、自旋锁都是用户空间完成
重量级锁需要向内核态申请

3、synchornized锁的升级

对于synchronized锁来说,锁的升级主要都是通过Mark Word中的锁标志位与是否是偏向锁标志位来达成的;

synchronized关键字所对应的锁都是先从偏向锁开始,随着锁竞争的不断升级,逐步演化至轻量级锁,最后则变成了重量级锁。
锁的升级是单向的,不会出现锁的降级。

对于锁的演化来说,它会经历如下阶段:无锁 -> 偏向锁 -> 轻量级锁(自旋锁) -> 重量级锁

1、无锁状态
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }

在这里插入图片描述

hash值为中间31位:01111011001000111110110010000001
在这里插入图片描述

2、偏向锁

偏向锁主要作用就是优化同一个线程多次获取一个锁的情况;

偏向锁由两个字段来控制:
• 偏向锁标记字段
• Thread ID(线程ID)字段

偏向锁的加锁:

  • 一个synchronized方法被一个线程访问,那么这个方法所在的对象就会在其Mark Word中将偏向锁进行标记,同时还会有一个字段来存储该线程的ID
  • 当这个线程再次访问同一个synchronized方法时,它会检查这个对象的Mark Word的偏向锁标记以及是否指向了其线程ID,如果是的话,那么该线程就无需再去进入管程(Monitor)了,而是直接进入到该方法体中。

偏向锁的jvm参数设置?
jvm是默认开启偏向锁,但有5s的延迟。
开启偏向锁:-XX:+UseBiasedLocking
设置偏向锁延迟为0,即关闭延迟:-XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking

演示偏量级锁:

//-XX:BiasedLockingStartupDelay=0
public class HelloJol {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("当前线程id:"+Thread.currentThread().getId());
		
		//匿名偏向锁
        //0000000000000000000000000000000000000000000000
        //0
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
		//偏向锁
        //000000000000000000000000000000000000001011010001010000
        //46160
        synchronized (o){
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

第一次打印为匿名偏向,第二次偏向锁指向了main线程
在这里插入图片描述
偏向锁线程id000000000000000000000000000000000000001011010001010000转成十进制46160,打印的结果是显示是线程id=1,怎么回事呢?
代码Thread.currentThread().getId()获取的threadId其实是jvm里的线程id,和我们常说的linux系统线程id不一样

更多演示:https://www.jb51.net/article/183984.htm

3、轻量级锁

偏量级锁升级为轻量级锁只需要两个线程,第一个线程已经获取到了当前对象的锁并且是偏向锁,第二个线程在抢时,发现Mark Word里面存储的线程ID并不是自己(是第一个线程),那么它会进行CAS(Compare and Swap),从而获取到锁,这里面存在两种情况:

  1. 获取锁成功:那么它会直接将Mark Word中的线程ID由第一个线程变成自己(偏向锁标记位保持不变),这样该对象依然会保持偏向锁的状态。
  2. 获取锁失败:(自旋次数达到界限值)则表示这时可能会有多个线程同时在尝试争抢该对象的锁,那么这时偏向锁就会进行锁撤销,升级为轻量级锁。
  3. 如果是线程1获取到轻量级锁会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(官方称为DisplacedMarkWord),然后使用CAS把对象头中的指针指向线程1存储的锁记录空间的地址;别的线程试图抢锁的时候,发现MarkWord上的指针已经被换了,CAS失败,那么线程就尝试使用自旋锁来等待线程1释放锁。
  4. JVM对于自旋次数的选择,jdk1.5默认为10次,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间。
4、重量级锁

JDK1.5之前默认的锁的形态。
如果多个线程竞争锁,轻量级锁要膨胀为重量级锁,在这种情况下,无法获取到锁的线程都会进入到Monitor
重量级锁通过对象内部的监视器(Monitor)实现,其中 Monitor 的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

6、锁消除技术与逃逸分析

JIT编译器在动态编译同步块的时候,可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。 如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。

代码示例:

/**
 * 同步省略说明
 */
public class SynchronizedTest {
    public void f() {
        Object hollis = new Object();
        synchronized(hollis) {
            System.out.println(hollis);
        }
    }
    //代码中对hollis这个对象进行加锁,但是hollis对象的生命周期只在f()方法中
    //并不会被其他线程所访问控制,所以在JIT编译阶段就会被优化掉。
    //优化为 ↓
    public void f2() {
        Object hollis = new Object();
        System.out.println(hollis);
    }
}

https://blog.csdn.net/weixin_42412601/article/details/107533660

7、锁粗化

JIT编译器在执行动态编译时,若发现前后相邻的synchronized块使用的是同一个锁对象,那么它就会把这几个synchronized块给合并为一个较大的同步块,这样做的好处在于线程在执行这些代码时,就无需频繁申请与释放锁了,从而达到申请与释放锁一次,就可以执行完全部的同步代码块,从而提升了性能。

public class MyTest5 {
    private Object object = new Object();
    public void method() {
        synchronized (object) {
            System.out.println("hello world");
        }
        synchronized (object) {
            System.out.println("welcome");
        }
        synchronized (object) {
            System.out.println("person");
        }
    }
}

8、synchronized的执行过程

检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁
如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1
如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁
如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
如果自旋成功则依然处于轻量级状态。
如果自旋失败,则升级为重量级锁。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值