Java Synchronized 锁升级

Java 中的锁共有4种状态,级别从低到高依次为:无状态锁偏向锁轻量级锁重量级锁,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级

为什么会有锁升级?

常用的 synchronized 是重量级锁(也是悲观锁),每次在要进行锁的请求的时候,如果当前资源被其他线程占有要将当前的线程阻塞加入到阻塞队列,然后清空当前线程的缓存,等到锁释放的时候再通过 notify()notifyAll() 唤醒当前的线程,并让其处于就绪状态。

这样线程的来回切换是非常消耗系统资源的,有些时候线程刚挂起资源就释放了。Java 的线程是映射到操作系统的原生线程之上的,++每次线程的阻塞或者唤醒都要经过用户态到核心态或者核心态到用户态的转化++,这样是十分浪费资源的,这样就会造成性能上的降低。

因此 JVM 对 synchronized 进行了优化,将 synchronized 分为三种锁的级别:偏向锁轻量级锁重量级锁

其中锁在 Java 对象头 MarkWord 中的标志位分别是:偏向锁(01),轻量级锁(00),重量级锁(10)。

偏向锁(乐观锁)

偏向锁的核心思想就是锁会偏向第一个获取它的线程,在接下来的执行过程中该锁没有其他的线程获取,则持有偏向锁的线程永远不需要再进行同步。

当锁对象第一次被线程获取的时候,会将锁对象的对象头中的锁标志位设置成为01,偏向锁标记设置为1,线程通过CAS的方式将自己的ID值放置到对象头中(因为在这个过程中一旦有竞争就会升级为轻量级锁了)。这样每次再进入该锁对象的时候不用进行任何的同步操作,直接比较当前锁对象的对象头是不是该线程的ID,如果是就可以直接进入

偏向锁使用一种等待竞争出现才释放锁的机制,所以当有其他线程尝试获得锁时,才会释放锁。偏向锁的撤销,需要等到安全点。它首先会暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果不处于活动状态,则将对象头设置为无锁状态;如果依然活动,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的 MarkWord 要么重新偏向其他线程,要么恢复到无锁或者标记对象不合适作为偏向锁,最后唤醒暂停的线程。

轻量级锁(乐观锁)

偏向锁是一种无竞争锁,一旦出现了竞争大多数情况下就会升级为轻量级锁。

现在我们假设有线程1持有偏向锁,线程2来竞争偏向锁会经历以下几个过程:

  1. 线程2会先检查偏向锁标记,如果是1,说明当前是偏向锁,那么 JVM 会先查看线程1是否还存活
  2. 如果线程1已经执行完毕,就是说线程1不存在了(线程1自己不会去释放偏向锁),那么先将偏向锁置为0,对象头设置成为无锁的状态,用CAS的方式尝试将线程2的ID放入对象头中,还是偏向锁
  3. 如果线程1存活,先暂停线程1,将锁标志位变成00(升级为轻量级锁)。然后在线程1的栈帧中开辟出一块空间(Display Mark Word),将对象头的 Mark Word 置换到线程一的栈帧当中,而对象头中此时存储的是指向当前线程栈帧的指针。此时就变成了轻量级锁。继续执行线程1,然后线程2采用CAS的方式尝试获取锁。

轻量级锁与偏向锁的不同?

轻量级锁对于获取锁对象采用CAS的同步方式而偏向锁直接是把整个同步过程给取消。

轻量级锁如何获取锁对象?

轻量级锁是通过CAS也就是自旋的方式尝试获取锁对象,一旦失败会先检查,对象头中存储的是否是指向当前线程栈帧的指针,如果是,就可以获取对象,如果不是说明存在竞争,那么就要膨胀为重量级锁。

轻量级锁解锁时,同样通过CAS操作将对象头换回来。如果成功,则表示没有竞争发生。如果失败,说明有其他线程尝试过获取该锁,锁同样会膨胀为重量级锁。在释放锁的同时,唤醒被挂起的线程。

重量级锁(悲观锁)

重量级锁是将程序运行交出控制权,将线程挂起,由操作系统来负责线程间的调度,负责线程的阻塞和执行。这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,消耗大量的系统资源,导致性能低下。

轻量级锁膨胀为重量级锁

一旦有两条以上的线程竞争锁,轻量级锁膨胀为重量级锁,锁的状态变成10,此时对象头中存储的就是指向重量级锁的栈帧的指针。而且其他等待锁的线程要进入阻塞状态,等待重量级锁释放再来被唤醒然后去竞争。

锁升级过程示意图:


References:

https://blog.csdn.net/tjreal/article/details/80548662

Add a Comment

电子邮件地址不会被公开。 必填项已用*标注

5 × 2 =