java面试锁的问题,锁升级

经典面试题回顾

image-20200810075245140

基础原理回顾

CAS 原理

image-20200810075432289

简单看下jdk中一个cas原理的运用 : unsafe.class

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

compareAndSwapInt 就是cas 比较和交换

面试回答cas:
什么是CAS:

CAS:compare and swap 多线程访问,在没有加锁的情况下,保证线程一致性的去改变某个值,比如有个变量初始值0,第一个线程读取过来,想加1,在把1往回写的时候需要先去读最新的值,看看还是不是0,如果是,则把值改成1,如果原来的值已经被别的线程动了,改成2了,那么此时cas失败,值还是2。此时第一个线程虽然cas失败了,但是并不会被挂起,而是自旋,他会把最新的2读取,然后+1,再把3写回去的时候依然去判断原来的值是否被别的线程改变,如果改变了继续重复上述步骤。

自旋锁的优缺点

优点

  1. 尽可能的减少线程的阻塞,对于锁竞争不激烈,并且占用锁时间较短的代码来说,适合用自旋锁,因为他的开销相比重量级锁比如synchronized的线程wait和唤醒来说,消耗比较小,重量级锁在睡眠和唤醒需要切换两次上下文。

    因为cas同时只能有一个线程获取到锁,其他都会失败,失败了就自旋,自旋是不会释放cpu资源的,这样就会有大量线程占用cpu资源,进而会影响整体系统的性能,所以自旋周期的选择就比较重要了,Jdk1.5这个限度是一定的写死的,在1.6引入了适应性自旋锁

    自适应自旋锁
    • 由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。
    • 如果有半数cpu数个线程正在自旋,则后来线程直接阻塞

缺点

  1. 容易造成cpu资源浪费

用的是汇编指令:lock 是保证原子性的,

也就防止在写回去比较的过程中别的线程再次把值修改掉!

lock cmpxchg=cas修改的变量值

再往硬件层面走就是lock锁定了北桥芯片的一个电信号,这就没啥意义了

CAS里面的ABA问题如何解决?
什么是ABA问题

image-20200810080855171

比如,第一个线程把0改为1准备往回写的时候,去读取原来的值,这个值已经被另外一个线程读取过去先改为2然后再改为0放回去,那么此时第一个线程去判断的0其实已经不是原来的那个0了

如何解决ABA问题?

很简单,在原来那个0上加一个版本号,任何值的改动都需要更新版本号,当比较的时候除了比较值,还要比较版本号,jdk1.5之后,引入AtomicStampedReference类来解决ABA问题。
这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果二者都相等,才使用CAS设置为新的值和标志。

如何手写一个自旋锁?

备注:jvm只是一个协议,具体的实现有我很多:

image-20200810104908233

描述一下对象在内存中的布局

new 一个object对象,main方法就是一个栈针,里面有一个object对象的引用,指向堆内存中的object对象

image-20200810110243830

java object layout

  • pom依赖

    <dependency>
        <groupId>org.openjdk.jol</groupId>
        <artifactId>jol-core</artifactId>
        <version>0.9</version>
     </dependency>
    
  • 业务代码

    public static void main(String[] args) {
            int size = 10;
            List<Integer> list = new ArrayList<Integer>(size);
            for (int i = 0; i < size; i++) {
                list.add(i);
            }
            //打印实例内部的占用
            System.out.println(ClassLayout.parseInstance(list).toPrintable());
    }
    

    输出结果:offset起始位

image-20200810141910187

image-20200810141739144

对象头

对象头用于存储对象的元数据信息

分为两块:1. 存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁等。

             2. *类型指针*,指向它的类元数据的指针,用于判断对象属于哪个类的实例,比如person类,对象头就存储着这个对象的类型的指针,另外,如果对像是一个数组,那在对象头中还必须有一块用于记录<u>数组长度</u> 的数据

实例数据

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义各种类型的字段内容

对齐填充

对齐填充并不是必然存在的,由于hotspot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话,就是对象的大小必须是8字节的整数倍。而对象头正好是8字节的倍数。因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全

拿一个空的object对象来说:

一个空的object对象,占多少字节?

image-20200810150856596

  1. 开启classPoint的压缩情况下:

    因为java jvm本身开启了classPoint的压缩,总的占16个字节,

​ markWord 8个字节

​ jvm 在64位操作系统中占8个字节,默认开启了classpoint的压缩,压缩完占4个字节。

​ 8+4=12不能被整除,则补位对齐4个字节,总的是16字节 。

  1. 如果没有开启压缩其实也是占16个字节

    markWord 8个字节

    classPoint没有压缩 8个字节

    没有成员变量 0字节

    右对齐吗,没有,两个因为16已经可以被整除了,所以总的也是占16个字节

拓展:USER对象,有两个属性,在内存中占多少字节?

User user = new User();
user.name="hdx";
user.age=16;
//打印实例内部的占用
System.out.println(ClassLayout.parseInstance(user).toPrintable());

image-20200810155247481

image-20200810160326036

印证一下对象上的锁,锁的具体位置

在对象的markword上面

image-20200810161131184

一个对象经过一个GC之后分代计数就会增加1,当大于当前垃圾回收器的阈值之后就会到老年代,cms回收器默认是6次

所以markword里面也记录了GC的信息

image-20200810161541787

上面要注意锁标志位的code

锁升级的过程

*锁的种类:*无锁——>偏向锁——>轻量级锁(自旋锁)——>重量级锁

锁概念的解释:

偏向锁(01)

经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁,不加锁岂不是更好,为啥要多此一举?

比如A对象,他在获取锁的时候并没有去申请操作系统那个比较重的锁,而是把当前线程指针的Id放到markword中,作为一个标记,

偏向锁的适用场景

始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作;
在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用

偏向锁的获取过程
  1. 访问Mark Word中偏向锁位的标识是否设置成1,锁标志位是否为01(未锁定时的标志位),确认为可偏向状态
  2. 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3
  3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4
  4. 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)
  5. 执行同步代码。

安全点:

应用程序线程可以被安全地停止掉的那个时间点,就叫做安全点。这一术语也通常用来指代SWT暂停。

查看安全点日志:

要查看安全点停顿,可以打开安全点日志,通过设置JVM参数 -XX:+PrintGCApplicationStoppedTime 会打出系统停止的时间,添加-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 这两个参数会打印出详细信息,可以查看到使用偏向锁导致的停顿,时间非常短暂,但是争用严重的情况下,停顿次数也会非常多;

注意:安全点日志不能一直打开:

如果在生产系统上要打开,再再增加下面四个参数:
-XX:+UnlockDiagnosticVMOptions -XX: -DisplayVMOutput -XX:+LogVMOutput -XX:LogFile=/dev/shm/vm.log

JVM开启和关闭安全点:

  • 开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
  • 关闭偏向锁:-XX:-UseBiasedLocking
偏向锁的释放

只有遇到锁竞争的时候,持有偏向锁的线程才会释放锁,否则线程不会主动去释放偏向锁。偏向锁的撤销时需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态

偏向锁如何升级为轻量级锁(00)?

当偏向锁发生锁竞争的时候,偏向锁释放,然后相互竞争的线程在自己的线程栈内就会生成Lock Record 锁记录,然后每个线程之间通过自旋的方式竞争把这个锁记录写到mark Word里面,哪个线程最先修改成功 ,谁就获取到这把锁,这就是轻量级锁,也叫自旋锁。

解释下为什么需要把轻量级锁升级到重量级锁?

轻量级锁是处于用户态的,不需要向内核申请,所以比较快,但是重量级锁是放在内核态的,

因为自旋锁是很消耗cpu的,如果获取到锁的那个线程一直在执行,不释放,这时候锁是需要升级的,

升级成重量级锁的好处是什么?

image-20200810182258548

所以,关键就是重量级锁的队列里面,可以是wait状态,阻塞的,不消耗cpu资源,什么时候可以执行了,再唤醒

轻量锁如何升级成为重量级锁(10)?

竞争进一步加剧:自旋超过10次:-XXPreBlockSpin,或者cpu核数的一半,jdk1.6之后有了自旋自适应,jdk自己会控制

面试回答锁升级的过程?
  • new对象,此时默认是加了偏向锁,第一个访问该对象的线程会把当前线程id放到这个对象的mark word的对象头里面作为一个标记
  • 出现线程竞争,则通过CAS操作竞争锁,如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,继续执行代码,竞争失败,jvm让拥有偏向锁的线程挂起,等到了安全点的时候stw,释放锁,锁升级为轻量级锁,锁标志位变为00,每个线程之间通过自旋的方式吧自己线程栈里面的lock record写入到mark word,谁修改成功谁就获取到锁——自旋锁。
  • 竞争进一步加剧,出现了大量的线程在自旋,或者自旋超过一定的次数,这时候cpu空转比较厉害,为了节省cpu资源,jvm开始向操作系统申请重量级锁——synchronized、ReentrantLock,
有个上面的基础,可以解释synchronized的执行过程?
  1. 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁

  2. 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1

  3. 如果失败,则说明有锁竞争,撤销偏向锁,进而升级为轻量级锁。

  4. 当前线程使用CAS将对象头的Mark Word替换为当前线程的lock record,如果成功,当前线程获得锁 ,如果失败,则自旋不停的获取锁。

  5. 当自旋一定次数之后获取成功了,还是用轻量级锁,如果失败了,锁再次升级为重量级锁,之前自旋的线程进入wait状态,等待cpu分配时间片后再次执行

    如果线程争用激烈,那么应该禁用偏向锁。

锁消除

image-20200811150932672

总结:对于不可能共享的资源,比如局部变量,在执行的时候jvm会把对象锁消除,比如一个方法里面的stringBuffer的append()

锁粗话

image-20200811151152175

总结:假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值