再有人说synchronized是重量级锁,就把这篇文章扔给他看

synchronized作为Java程序员最常用同步工具,很多人却对它的用法和实现原理一知半解,以至于还有不少人认为synchronized是重量级锁,性能较差,尽量少用 。
但不可否认的是synchronized依然是并发首选工具,连volatile、CAS、ReentrantLock都无法动摇synchronized的地位 。synchronized是工作面试中的必备技能,今天就跟着一灯一块深入剖析synchronized底层到底做了哪些优化?
synchronized是用来加锁的,而锁是加在对象上面,所以需要先聊一下JVM中对象构成 。
1. 对象的构成Java对象在JVM内存中由三块区域组成:对象头、实例数据和对齐填充 。
对象头又分为:Mark Word(标记字段)、Class Pointer(类型指针)、数组长度(如果是数组) 。
实例数据是对象实际有效信息,包括本类信息和父类信息等 。
对齐填充没有特殊含义,由于虚拟机要求 对象起始地址必须是8字节的整数倍,作用仅是字节对齐 。
Class Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例 。
重点关注一下对象头中Mark Word,里面存储了对象的hashcode、锁状态标识、持有锁的线程id、GC分代年龄等 。
在32为的虚拟机中,Mark Word的组成如下:

再有人说synchronized是重量级锁,就把这篇文章扔给他看

文章插图
2. synchronized锁优化从JDK1.6开始 , 就对synchronized的实现机制进行了较大调整,包括使用JDK1.5引进的CAS自旋之外,还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁等优化策略 。由于使得synchronized性能极大提高,同时语义清晰、操作简单、无需手动关闭,所以推荐在允许的情况下尽量使用此关键字 , 同时在性能上此关键字还有优化的空间 。
【再有人说synchronized是重量级锁,就把这篇文章扔给他看】锁主要存在四种状态 , 依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,性能依次是从高到低 。锁可以从偏向锁升级到轻量级锁,再升级的重量级锁 。但是锁的升级是单向的 , 也就是说只能从低到高升级,不会出现锁的降级 。
在 JDK 1.6 中默认是开启偏向锁和轻量级锁的,可以通过-XX:-UseBiasedLocking来禁用偏向锁 。
2.1 自旋锁线程的挂起与恢复需要CPU从用户态转为内核态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力 。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的 。
自旋锁就是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态 。
自旋锁适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短 。自旋等待不能替代阻塞 , 虽然它可以避免线程切换带来的开销,但是它占用了CPU处理器的时间 。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好 , 反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作 , 这样反而会带来性能上的浪费 。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起 。
自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启,在JDK1.6中默认开启 。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整 。
2.2 自适应自旋锁JDK 1.6引入了更加智能的自旋锁 , 即自适应自旋锁 。自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定 。那它如何进行适应性自旋呢?
线程如果自旋成功了,那么下次自旋的次数会更加多 , 因为虚拟机认为既然上次成功了 , 那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多 。反之,如果对于某个锁 , 很少有自旋能够成功,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程 , 以免浪费CPU资源 。
有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明 。

推荐阅读