java中的synchronized只是重量级锁吗?聊一聊synchronized锁升级流程

synchronized这个关键字,原来的印象就是一个重量级锁,也就是悲观锁,直接锁住代码段,剩余的线程进入到阻塞队列中,效率极低,实际上呢,在jdk1.6之后,synchronized的内部进行了优化,它不再是一个简单的重量级锁,它为了试用所有的情况,有了一个锁升级流程:无锁 -》 偏向锁  -》 轻量级锁 -》 重量级锁,接下来我们仔细的聊一下所谓的锁升级流程。

 

首先,现来看一下,synchronized的使用方法:

1、对一个对象进行加锁

synchronized(this){
    //代码
}

2、对一个方法进行加锁

public synchornized void test(){
    //代码
}

实际上,无论是对一个对象进行加锁还是对一个方法进行加锁,实际上,都是对对象进行加锁。也就是说,对于方式2,实际上虚拟机会根据synchronized修饰的是实例方法还是类方法,去取对应的实例对象或者Class对象来进行加锁。

所以既然是对对象加锁,我们是不是应该了解下对象的结构呢,楼主的上一篇文章有关于对象结构的,不再多说。锁的信息都是存在对象头中的MarkWord中的。结构如下:

 

 一、无锁状态

当一个对象被创建出来时,为无锁状态,所标记位位01,是否偏向锁位0。

 

二、偏向锁

当一个线程执行到锁相关的代码段时,就会将是否偏向锁置为1,此时MarkWord结构如下:

bit fields 是否偏向锁锁标志位
threadId epoch 1 01

 

 

 

此时有线程占据了这个锁,这也就是偏向锁。

这也就到了偏向锁,这个锁会偏向于第一个获得它的线程,在接下来的执行过程中,假如该锁没有被其他线程所获取,没有其他线程来竞争该锁,那么持有偏向锁的线程将永远不需要进行同步操作。

假如,这时候来了一个线程,也来抢占该锁,处理流程如下:

1、判断线程id是否和MarkWord中的线程id相同,如果相同,那么直接执行代码块,否则执行下一步。

2、查看对象是否为可偏向,如果为0,那么进行CAS操做,将MarkWord中的否偏向锁置为1,并记录id。如果不是,执行下一步骤。

3、这时候就出现了竟争锁的情况,新线程,会尝试CAS操作,来更新线程id,如果失败,就进行锁的撤销或升级为轻量级锁。

 

如果失败,就去撤销锁,首先需要判断,MarkWord中存放的线程id是否还存活,如果已经死亡,就撤销锁,然后新线程,获取锁,否则升级为轻量级锁。

 

其中简单聊一下撤销锁:

锁的对象头中偏向着线程1,因为它不知道线程1什么时候来,所以一直偏向着,就算线程1已经死亡了。所以撤销锁的时候,先检查对象头所指向的线程是否存活,如果不存活,那么偏向锁撤销为无锁,线程2就拿到了锁,如果存在,那么线程1目前没有拿着锁而在干别的事情,这样锁就在不同时间段被不同线程访问了升级为轻量级锁。

 

所以,如果程序肯定是两个线程竟争,我们可以一开始就把偏向锁这个默认功能给关闭,否则浪费大量的资源。

偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用-XX:BiasedLockingStartUpDelay=0;
如果不想要偏向锁,那么可以通过-XX:-UseBiasedLocking = false来设置;

三、轻量级锁

简单描述下偏向锁升级为轻量级锁的过程:

1、线程在自己的线程栈中,新建一个锁记录LockRecord,官方称为Displaced Mark Word。

2、将LockRecord中的对象指针,指向锁对象,然后将锁对象的MarkWord复制到LockRecord中。

3、将锁对象的MarkWord替换为为指向LockRecord的指针。

为什么要拷贝mark word?
原因是为了不想在lock与unlock这种底层操作上再加同步,如果每个线程进来都不拷贝,直接对内容进行更改的话,可能会出错。

将LockRecord中的对象指针,指向锁对象,是为了识别,哪个对象被锁住了。

将锁对象的MarkWord替换为为指向LockRecord的指针,是为了让其他线程知道,这个锁被获取了。

然后MarkWord更新为:

bit fields锁标志位
指向LockRecord的指针 00

 

 

 

然后简单聊聊轻量级锁。

轻量级锁分为自旋锁和自适应自旋锁

1、自旋锁

自旋锁也就是当一个线程占据着锁时,这时候另一个线程来了,发现锁被占用,就开始进行不停的尝试CAS操作,也就是不停的执行for循环,来不停的尝试如果线程尝试获取锁的时候,轻量锁正被其他线程占有,就会不停的自旋获取锁,如果超过次数获取锁,那么它就会修改MarkWord,修改重量级锁,表示该进入重量锁了,知道获取锁之后,结束。

当线程太多之后,就会出现一个问题,假如有100个线程竟争资源,有99个在不停的执行for循环,这个cpu的消耗是非常可怕的。所以,线程太多了,就需要让线程阻塞,然后执行了,默认情况下,自旋的次数为10次,用户可以通过-XX:PreBlockSpin来进行更改,为了优化引入了自适应自旋锁,超过了次数,就升级为重量级锁。

2、自适应自旋(此操作为了防止长时间的自旋,在自旋操作上加了一些限制条件)。

比如一开始给线程自旋的时间是10秒,如果线程在这个时间内获得了锁,那么就认为这个线程比较容易获得锁,就会适当的加长它的自旋时间。

如果这个线程在规定时间内没有获得到锁,并且阻塞了。那么就认为这个线程不容易获得锁,下次当这个线程进行自旋的时候会减少它的自旋时间。

 

轻量级锁解锁操作

1、轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头。

2、如果成功,则表示没有竞争发生。成功替换,等待下一个线程获取锁。

3、如果失败,表示当前锁存在竞争(因为自旋失败的线程已经将对象头中的轻量级锁00改变为了10),锁就会膨胀成重量级锁。

4、因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。

 

四、重量级锁

升级为重量级锁之后,MarkWord中发生变化,如下:

bit fields锁标志位
指向Mutex的指针 10

 

 

 

重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁。

重量级锁的工作流程如下:当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。所以重量级锁的开销还是很大的。

 

这就是synchronized中的锁升级流程。

posted @ 2021-01-03 18:49  码在江湖  阅读(1965)  评论(1编辑  收藏  举报