synchronized是Java程序员最常用的同步工具,但是很多人对它的用法和实现原理都一知半解,以至于还有很多人认为synchronized是重量级的锁,性能很差,应该尽量少用。但不可否认的是,synchronized依然是并发的首选工具,甚至连volatile、CAS、ReentrantLock都无法撼动synchronized的地位。同步是求职面试中的一项基本技能。今天我就跟随一灯深入分析一下synchronized底层做了哪些优化?synchronized是用来加锁的,而锁是给对象加的,所以要先说说JVM中对象的组成。1.对象的组成一个Java对象在JVM内存中由三个区域组成:对象头、实例数据和对齐填充。对象头又分为:MarkWord(标记字段)、ClassPointer(类型指针)、数组长度(如果是数组)。实例数据是对象实际有效的信息,包括本类信息和父类信息。对齐填充没有特殊意义,因为虚拟机要求对象的起始地址必须是8字节的整数倍,作用只是字节对齐。ClassPointer是对象指向其类元数据的指针,虚拟机通过这个指针来判断该对象是哪个类的实例。重点关注对象头中的MarkWord,里面存放了对象的hashcode、锁状态标识、持有锁的线程id、GC分代年龄等。在32位虚拟机中,MarkWord的组成如下:2.synchronized锁优化从JDK1.6开始,synchronized的实现机制有了很大的调整,包括使用JDK1.5引入的CAS自旋、AdaptiveCAS自旋、锁淘汰、锁粗化、偏向锁、轻量级锁等优化策略也已添加。由于synchronized的性能有很大的提升,同时语义清晰,操作简单,不需要手动关闭,所以建议尽量使用这个关键字,并且有此关键字在性能方面的优化空间。锁主要有四种状态,分别是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,性能由高到低。锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。但是锁的升级是单向的,也就是说只能从低升级到高,不会出现锁降级的情况。在JDK1.6中,默认启用偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking禁用偏向锁。2.1自旋锁线程的挂起和回收需要CPU从用户态切换到内核态。频繁的阻塞和唤醒对于CPU来说是一个沉重的负担,必然会对系统的并发性能带来很大的影响。压力。同时我们发现,在很多应用中,对象锁的锁状态只会持续很短的时间,为了这短时间频繁的阻塞和唤醒线程是不值得的。自旋锁是指当一个线程试图获取锁时,如果锁已经被其他线程占用,则一直循环检查锁是否被释放,而不是进入线程挂起或睡眠状态。自旋锁适用于锁保护的临界区非常小的情况。如果临界区很小,锁占用的时间就很短。自旋等待不能代替阻塞。虽然可以避免线程切换的开销,但是会占用CPU处理器时间。如果持有锁的线程快速释放锁,自旋的效率是非常好的。相反,自旋线程会白白消耗处理资源,不会做任何有意义的工作,反而会造成性能浪费。因此,旋转等待时间(旋转次数)必须有一个限制。如果自旋超过了定义的时间,仍然没有获取到锁,就应该挂起。自旋锁在JDK1.4.2引入,默认关闭,但可以通过-XX:+UseSpinning开启,JDK1.6默认开启。同时默认旋转次数为10次,可以通过参数-XX:PreBlockSpin进行调整。2.2自适应自旋锁JDK1.6引入了一种更加智能的自旋锁,即自适应自旋锁。自适应的意思是自旋次数不再是固定的,它是由同一把锁上一次自旋的时间和锁拥有者的状态决定的。那么它是如何进行自适应旋转的呢?如果线程自旋成功,那么下次自旋的次数会更多,因为虚拟机认为自从上次成功之后,很有可能这次自旋会再次成功,所以会让自旋一直等待.更多次。反之,如果某个锁很少有自旋能够成功,那么以后需要这把锁的时候,就会减少甚至省略自旋的次数,以免浪费CPU资源。有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁状态的预测会越来越准确,虚拟机也会越来越智能。2.3锁消除在JIT编译过程中,JVM扫描运行上下文并进行逃逸分析。如果某段代码不存在竞争或共享的可能,就会消除这段代码的锁,以提高程序的运行效率。publicvoidmethod(){finalObjectLOCK=newObject();synchronized(LOCK){//dosomething}}比如上面代码中的lock在方法中是private和immutable的,所以根本不需要加锁。所以JVM会进行锁消除。2.4锁粗化从逻辑上讲,同步块的范围应该越小越好,只在共享数据的实际范围内进行同步。这样做的目的是尽量减少需要同步的操作数,缩短阻塞时间,如果存在锁竞争,等待锁的线程可以尽快拿到锁。但是加锁和解锁也会消耗资源。如果有一系列连续的加锁和解锁操作,可能会造成不必要的性能损失。锁粗化是将多个连续的加锁和解锁操作连接在一起,扩展成一个更大的锁,避免频繁的加锁和解锁操作。publicvoidmethod(ObjectLOCK){synchronized(LOCK){//dosomething1}synchronized(LOCK){//dosomething2}}比如上面方法中两个加锁的代码块可以合并为一个,减少频繁添加lock和unlock带来的开销提高了程序运行的效率。2.5偏向锁为什么要引入偏向锁?因为经过HotSpot作者的大量研究,发现大部分时候是没有锁竞争的。通常,一个线程会多次获取同一个锁。因此,如果每次都要去竞争锁,会增加很多不必要的成本。偏向锁的引入是为了降低获取锁的成本。2.6轻量级锁轻量级锁考虑的场景是竞争锁对象的线程不多,线程不会长时间持有锁。因为阻塞线程需要CPU从用户态切换到内核态,所以成本比较高。如果在阻塞后不久就释放了锁,这个代价是得不偿失的。因此,这个时候干脆不要阻塞线程,让它自旋即可。(CAS)这等待锁被释放。加锁过程:当代码进入同步块时,如果同步对象处于无锁状态,则当前线程会在栈帧中创建一个锁记录(LockRecord)区域,并复制对象头中的MarkWord锁对象到锁记录,然后尝试使用CAS将MarkWord更新为指向锁记录的指针。如果更新成功,则当前线程已经获得了锁。解锁过程:轻量级锁的解锁过程也是使用CAS实现的,CAS会尝试用被锁对象的MarkWord替换锁记录。如果替换成功,则意味着整个同步操作完成。如果失败,则意味着其他线程正在尝试获取锁。这个时候挂起的线程就会被唤醒(此时已经扩展为重量级锁)。2.7重量级锁是通过对象内部的监听来同步的是通过监听锁(Monitor)来实现的。但是监听锁的本质是依赖底层操作系统的互斥锁(MutexLock)来实现的。重量级锁的工作流程:当系统检查到锁是重量级锁时,会阻塞等待获取锁的线程,被阻塞的线程不会消耗cpu。但是阻塞或者唤醒一个线程的时候,需要操作系统帮忙,这就需要从用户态切换到内核态,而切换状态需要花费很多时间,可能比用户需要的时间还要长执行代码,所以权重级锁的开销还是很高的。在锁竞争激烈,锁持有时间长的场景下,还是适合使用重量级锁。2.8锁的升级过程2.9锁的优缺点对比锁的性能从低到高依次为无锁、偏向锁、轻量级锁、重量级锁。不同的锁只适用于不同的场景,大家可以根据实际场景来选择。3.总结经过多次迭代优化,同步锁没有以前那么重了。在JDK1.8的ConcurrentHashMap源码中,synchronized已经广泛用于同步控制。您可以放心地在日常开发中使用它们。
