其实很多优化方案无外乎就是保存状态做参考,让计算机更加智能,所以在一定程度上就相当于优化了,比如说动态规划也是这样,优化就是保存之前的状态,从之前的状态中推理,省略掉不必要的计算;
简述
synchronize 是JVM层次上的手段,通常是直接加排它锁,没有获得锁的线程就会从运行态变成阻塞的状态,因为线程的切换都要从用户态转化到内核态,阻塞的实现是一个重量级的操作,这些操作给并发性能带来了极大的影响,为了针对这种情况,synchronize 在后版本中进行了优化,具体如下
1、自旋锁和自适应锁
意思很好理解,就是自旋,假设此刻该线程没获得锁,不会立即进入阻塞状态,交出CPU的执行权,它会while一下,等过一会再来申请一下锁,说不定就申请到了呢?对吧,
如果锁占用的时间很短的话,这种优化的效果显而易见,毕竟不用切换线程带来的开销,只不过会占用一点CPU的时间自旋而已,但是如果锁占用的时间很长的话,那自旋的线程自会白白消耗资源........所以说如何控制自旋的时间变得尤为重要,并且如果自旋到一定的次数(默认是10次)之后还没有获得锁的话就必须挂起线程了。
自适应锁是什么呢?就是自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,比如上一次自旋成功了,那么JVM会认为你这次也可能会成功,所以会让你多自旋几次,假设你上几次都没有成功,那么JVM觉得你这次也不会成功,所以不让你自旋很多次,直接就让你挂起了....
2、锁消除
字面意思,就是不加锁,哎,明明加了锁(这个锁可能是程序员加的,也可能不是),你为什么不给我加锁呢?JVM不加锁肯定有理由啦,如果它根据逃逸分析判断你这个是线程安全的,就不会给你加锁了,也就避免了开销,这也算得上是一种优化 关于这个逃逸分析我也没仔细看,好像就是判断在这个代码块中堆上的对象能不能被其他线程访问到,如果访问不到的话就可以把他当做栈上的数据,相当于私有的,就不会加锁了。
3、偏向锁
偏向锁其实就是一种无锁状态,其存在目的就是在于只有一个线程执行同步代码块时提高性能。
每个对象的头部(MarkWord)有一部分存着一些与对象本生数据无关的东西,例如说GC的分代年龄、对象hashcode等,最重要的是下面几个
- 指向锁记录的指针 -------轻量级锁的时候指向对应线程栈中的锁记录(拥有该锁的线程)
- 指向重量级锁的指针 -------锁膨胀
-
偏向线程ID -------01表示可偏向,也相当于没被占用...
64位的jvm.png
当一个线程访问同步代码块时,会在MarkWord里存储锁偏向的线程ID,也就是当前线程。在线程再次进入和退出同步代码块的时候,不需要通过CAS操作来加锁和解锁,所以偏向锁只进行了一次CAS操作。
注意
线程不会主动释放偏向锁,偏向锁只有遇到其他线程竞争时才会升级到轻量级锁
4、轻量级锁
轻量级锁的目的是为了解决在没有多线程竞争的情况下,减少传统重量级锁使用操作系统互斥量带来的开销。
简明扼要的讲一下轻量级锁的过程:当进入到同步代码块的时候,如果对象没有被锁定(01),使用CAS修改锁状态标记为00,如果成功就继续执行,如果不成功就表示有人在同时间抢这个对象,而且还抢赢了,这时候把锁状态变成10,升级为重量级锁,以后来的线程都阻塞。
解锁过程也是一样,CAS把线程栈中所记录置换回来,如果成功,就表示执行完成,不成功就说明现在有线程尝试获取过该锁,在释放锁的同时要唤醒它们。
获取和释放分别进行一次CAS,发生锁竞争还会存在互斥操作的开销。
4、偏向锁
偏向锁的目的是消除数据在无竞争情况下的同步,是在轻量级锁上的优化,只需要一次CAS。
大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁
偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,只需要在Mark Word中CAS记录owner,如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁。
总结 几个锁的试用场景
- 偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
- 轻量级锁:无实际竞争,多个线程交替使用锁;
- 重量级锁:有实际竞争,且锁竞争时间长。
- 自旋锁:有实际竞争,锁占用时间很短
另外,我在网上看到一个很好的锁膨胀过程,可以看看,很直观
image.png
网友评论