Synchonized锁做了哪些优化?
昨天面试的时候被面试官问到这个问题,但是不够了解,所以在面试结束后查阅资料,在此作下总结。
Synchonized锁的原理以及作用此次不再过多介绍,可以参考此前文章Java多线程及Synchonized关键字;
Synchronized锁在JDK1.6的时候做了大量优化,从而实现高效并发,如下:
1、自适应的自旋锁
在原有的Synchronized锁中,如果线程B想要获得object对象的锁,但是发现已经被线程A占有,则会选择阻塞,等到线程A释放锁的时候,线程B被唤醒,再次去尝试获取锁。
因为需要频繁切换后续线程的状态,从而造成一定的性能损失。这时候引入了自旋锁,线程B在发现对象锁已经被A占有的时候,不会立刻进入阻塞,而是进行自旋(循环),在指定自旋次数内,如果A释放了锁,那么B就可以直接去尝试获得锁,不必进行线程状态切换。在达到最大自旋次数后(默认为10),如果A还没有释放,则进入阻塞。因为线程自旋期间也是需要占用系统资源的,如果大量线程在自旋后仍然没有等到释放锁,就会得不偿失。因此自旋锁适用于短任务比较多的场景,同时也可以使用-XX:PreBlockSpin命令设置合理的最大自旋次数。
同时引入了自适应功能,自适应代表着自旋的时间不再固定。线程B等待释放锁的自旋时间,会根据当前占用锁的线程A是否有在自旋期间成功获得锁而动态调整,如果A是在等待释放锁的自旋期间成功获取到了锁,那么就可以认定B也会大概率在自旋期间获得锁,B的自旋时间就会增加。
2、锁消除
锁消除是在虚拟机编译器在运行时候,会将那些代码上要求同步,但是经过检测(逃逸分析和数据支持)不可能存在资源竞争的锁进行消除,从而消除不必要的同步,提高运行效率。
3、锁粗化
在我们日常需要加锁的场景中,总是强调锁的粒度要尽可能小,减少加锁期间的计算量,从而减少单个线程的占锁时间。
大部分情况下这都是对的,但是在一些特殊场景中,比如循环,伪代码如下:
//小粒度锁
for(int a=0;a<100;a++){
synchronized(this){
//some sentence
}
}
//大粒度锁
synchronized(this){
for(int a=0;a<100;a++){
//some sentence
}
}
在上述两个代码中,小粒度锁会因为进行一百次加锁释放锁操作,从而执行效率上没有大粒度锁高。
对此进行锁粗化操作,当虚拟机探测到一系列连续操作都是对同一个对象反复加锁和解锁,就不把加锁同步范围粗化(扩展)到整个操作序列的外围,也就是说上述代码中你实际写的代码是第一种,但是执行结果极大可能是第二种。
同时我们在实际开发过程中,也要根据实际需求,合理设置锁的粒度。
4、轻量级锁
在这里轻量级锁的主要目的是,在没有多线程竞争的前提下,较少传统的重量级锁使用操作系统互斥量产生的性能消耗,实现过程中主要是基于CAS思想,因为过程较为复杂此处不再详细介绍。
ps:CAS是一个很重要的设计思想,在Java的AQS,数据库等乐观锁等,都有应用到CAS设计思想。
5、偏向锁
偏向锁的目的是在数据没有竞争的情况下消除同步操作,
这个偏的偏就是偏心的意思,比如现在占有对象锁的的线程A,那么在线程A释放锁后,没有别的线程来占有锁,那么下次线程A来占有对象锁的时候就不再需要同步操作了,这个偏向锁,偏向的就是上一个占有锁的线程。