三、架构师-高并发与多线程-synchronized、各种锁

请先看 二、架构师-高并发与多线程-java对象内存布局

synchronized应用

synchronized有三种方式来加锁,分别是:方法锁,对象锁synchronized(this),类锁synchronized(Demo.Class)。其中在方法锁层面可以有如下3种方式:

  1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

synchronized括号后面的对象:
synchronized扩号后面的对象是一把锁,在java中任意一个对象都可以成为锁,简单来说,我们把object比喻是一个key,拥有这个key的线程才能执行这个方法,拿到这个key以后在执行方法过程中,这个key是随身携带的,并且只有一把。如果后续的线程想访问当前方法,因为没有key所以不能访问只能在门口等着,等之前的线程把key放回去。所以,synchronized锁定的对象必须是同一个,如果是不同对象,就意味着是不同的房间的钥匙,对于访问者来说是没有任何影响的。

synchronized原理和锁升级过程

锁信息是放在对象头的mark word中的,下图是mark word的数据结构,将围绕着这个图进行分析锁的原理。
Hotspot32位:
在这里插入图片描述

锁升级

锁的4中状态(级别从低到高):无锁态 --> 偏向锁 --> 轻量级锁 (无锁,乐观锁,自旋锁,自适应自旋锁) —> 重量级锁。
hotspot的实现为例(64位):

https://img2020.cnblogs.com/blog/1692410/202004/1692410-20200429084041363-1585548718.png

  • 分代年龄:对象被GC的次数,4个bit,能表示16个数,最大值是15。CMS垃圾回收器默认是6,其他回收器默认16。
  • hashCode:只有调用了hashCode()方法之后,对象的hashCode才会存在这里面,没调用则是空。
  • mark word的低三位表示锁状态,其中一位表示偏向锁,另两位表示其他锁。
  • lock record:持有displaced word和锁住对象的元数据;解释器使用lock record来检测非法的锁状态;隐式地充当锁重入机制的计数器。

偏向锁

因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,在竞争不激烈的时候,常常是一个线程多次获得同一个锁;因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。
线程A获取锁后,会将mark word的高54bit设置成指向线程A的指针。偏向锁不会主动释放锁,当线程A再获取锁的时候,会检查高54bit的线程指针;如果是指向A的,若A还是存活则直接进入,若A不存活则将锁设置为无锁态。如果高54bit指向的不是A,不是则锁撤销偏向锁状态升级为轻量级锁。

偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用-XX:BiasedLockingStartUpDelay=0;
如果不想要偏向锁,那么可以通过-XX:-UseBiasedLocking = false来设置。

轻量级锁

轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。

当字节码解释器执行monitorenter字节码轻量锁住一个对象时,就会在获取锁的线程的栈上显式或者隐式分配一个lock record,里面记录的锁对象的信息。同时锁对象的高62位指向了获得该锁线程的lock record。多个线程通过CAS来修改锁对象的高62位来获得锁。

CAS有一定的次数限制,来避免自旋过度消耗CPU,比如10次或者cpu超过1/2如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

重量级锁

重量级锁需要向操作系统申请资源,会挂起没有获得锁的线程,进入等待队列。等待操作系统的调度,然后再映射回用户空间。

总结

注意:为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。一句话就是锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态

锁消除

Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。
例如:

    public void addString(String s1, String s2){
        StringBuffer sb = new StringBuffer();
        sb.append(s1).append(s2);
    }

StringBuffer是线程安全的,因为他的关键方法都是被synchronized修饰的,但是我们看上面的代码会发现:sb这个引用只会在add方法里被使用,不可能被其他线程引用(局部变量,栈私有),因此sb是不可能共享的资源,JVM会自动消除StringBuffer对象内部的锁。

锁粗化

按理来说,同步块的作用范围应该尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗。
锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。
例如:

	StringBuffer sb = new StringBuffer();
	
    public String addString(String s1){
        for (int i = 0; i < 1000; i++) {
            sb.append(s1);
        }
        return sb.toString();
    }

JVM会检测到这样一连串的操作会对同一个对象加锁(while循环内1000次执行append,没有锁粗化的话就要进行100次加锁/解锁),此时JVM就会将加锁的范围粗化到这一连串的操作外部,使得这一连串操作只加一次锁即可。

synchronized底层实现分析

首先了解一下JIT(Just In Time Compiler)计时编译器,这是针对解释性语言而言的,并非虚拟机必须,是一种优化手段。Hotspot虚拟机就是用的这种技术手段。Hotspot虚拟机的执行引擎执行java代码是可采用解释执行编译执行两种方式。Hotspot中的编译器是javac,他的工作是将源代码编译成字节码。有了字节码,就有解释器来进行解释执行,即将字节码解释成机器码;但是Hotspot会把执行频率较高的方法或语句块通过JIT直接编译成本地机器码,提高了代码执行的效率。

synchronized底层也是用lock comexchg…指令实现的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值