前言
随着JDK的不断迭代,synchronized锁的性能得到了极大的提升,它早已经不是以前那把笨重的锁了。在JDK1.6前,synchronized只是一把重量级的锁,而jdk1.6后,实现了偏向锁,轻量锁等,引入锁升级的机制,使得synchronized更加高效,性能更好,现在就来讲讲synchronized升级的过程。
对象头
首先我们要先知道什么是对象头,因为synchronized锁信息是存放在对象头的。
一个对象是由对象头,实例数据,填充字节组成。
对象头包括:
1:Mark Word。记录着对象和锁的有关信息。前面说的synchronized的锁信息就是记录在这里的。
锁的信息在不同位数的虚拟机存储的情况是不一样的。它的结构是这样:
其中,锁标志位无锁和偏向锁是01,轻量级锁是00,重量级锁是10。而无锁和偏向锁又专门用1bit来区分。0代表无锁,1代表偏向锁。
2:指向类的指针。
3:数组长度(是数组对象的话)。
实例数据:就是对象的属性和值
对齐填充字节:用于补齐对象内存长度的。因为JVM要求java代码的对象必须是8bit的倍数。
锁升级过程
JDK1.6之前,锁是没有升级这一说,所有的线程一开始就获取到重量级的synchronize锁。但是,HotSpot的作者大量的研究发现,其实在很多时候,是不存在锁竞争,经常都是同一个线程去获取同一把锁,这样每次都去进行锁竞争会浪费很多不必要的性能开销。对此,引入了偏向锁,并且将synchronized锁改进为锁升级的机制。
锁升级的过程是
无锁->偏向锁->轻量级锁->重量级锁
锁升级是不可逆,也即重量级锁没法变回轻量级锁,轻量级锁没法变回偏向锁。
无锁->偏向锁
偏向锁里存着获取锁的线程ID,并且是否偏向标志的为1。
当synchronized代码块初次被执行时,通过cas操作,把MarkWord的是否偏向锁标志位的值至为1,将无锁状态变成偏向锁状态。操作成功后,把MarkWord的线程ID指向自己。其实,偏向锁的字面意思就是偏向获得自己的线程的锁。同步代码块释放完后,偏向锁不会主动释放。当相同的线程第二次走到这段同步代码块后,通过线程ID判断这把偏向锁是不是自己,是自己的就继续往下执行。因为没有释放偏向锁,所以也根本不需要加锁。
如果,自始至终偏向锁都是只要一个线程访问,那么基本没有额外的开销,性能很高。
偏向锁默认是开启,但是是延迟启动,可用通过XX:BiasedLockingStartUpDelay=0将取消延迟启动。
如果不想要偏向锁,可以通过XX:-UseBiasedLocking = false来设置。
来看看具体的步骤:
偏向锁->轻量级锁
当偏向锁发生锁竞争时,偏向锁会撤销然后升级成轻量级锁。轻量级锁其实就是自旋锁。线程进行锁竞争的时候,没有竞争到的线程并不会进入阻塞阶段,而是通过自旋等待锁释放。自旋的获取锁的步骤是其实就是通过CAS修改锁的标志位。先查看标志位是否是释放,是的话就设置为锁定,然后把线程ID指向自己。
长时间的自旋锁很消耗性能,因为自旋就是让cpu原地忙活,执行不了其他事情。其实这是一种折中的思想,让短时间的忙取代锁上下文切换的开销。
获取轻量级锁有两种方式:
1:当偏向锁被设置为不启动,那么直接获取轻量级锁
2:偏向锁撤销,升级到轻量级锁
轻量级锁->重量级锁
如果自旋次数过长,达到临界值(默认是10,可以通过XX:PreBlockSpin设置),会膨胀成重量级的锁。重量级的锁会把在自旋的线程都阻塞。
重量级的锁是依赖monitor锁来实现的,monitor又依赖操作系统的MutexLock(互斥锁)来实现的。重量级的锁开销比较大,这时因为阻塞或者唤起线程都需要操作系统进行帮忙,会导致系统在用户态和内核态来回进行切换,非常影响性能。字节码文件如下:
锁升级的整体步骤可以通过这张图: