• 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中的锁升级流程。

  • 相关阅读:
    SQL查询效率100w数据查询只要1秒
    超级实用且不花哨的js代码大全 (四) JavaScript[对象.属性]集锦
    Sql Server实用操作维护小技巧集合
    asp.net截取字符串方法
    自己整理的asp.net 缓存 相关资料
    【译】初识SSRS 通向报表服务的阶梯系列(一)
    【译】无处不在的数据 通向报表服务的阶梯系列(三)
    【译】SSRS基础 通向报表服务的阶梯系列(二)
    浅谈SQL Server中的事务日志(三)在简单恢复模式下日志的角色
    SQL Server中生成测试数据
  • 原文地址:https://www.cnblogs.com/mcjhcnblogs/p/14226505.html
Copyright © 2020-2023  润新知