面试题-自旋锁,以及jvm对synchronized的优化

背景

想要弄清楚这些问题,需要弄清楚其他的很多问题。
比如,对象,而对象本身又可以延伸出很多其他的问题。

我们平时不过只是在使用对象而已,怎么使用?就是new 对象。这只是语法层面的使用,相当于会了一门编程语言而已。

对象更深层次的问题如下:
1.源码实现
2.内存
3.字节码

因为只有了解对象,特别是深刻的理解对象,才能理解同步的问题。

对象

内存
包括三部分
1.头
2.数据
3.空白填充

头是对象的元数据,也就是,除了数据以外的数据。就像各种协议一样,每一种协议,都有头,比如,tcp/ip协议 http协议,都有头字段。
我们这里主要关心的是锁字段。除了锁字段,其实还有,对象的hashcode(唯一标识一个对象),线程等等。

数据,就是数据咯。

空白填充,是一段没有使用的内存。为什么要没有使用的内存,不是浪费吗?因为很多场合,需要申请的内存是什么的倍数,比如,对象需要是8个字节的倍数。所以,内存不够,填充空白字节即可,直到达到8个字节的倍数要求为止。


源码实现
这里也只讲,与同步相关的部分。
比如,c++源码里,与同步关键字synchronized相关的代码是对象监视器ObjectMonitor.cpp。与此相关的逻辑,主要包含以下几个字段:
1.哪个线程拥有这个锁
2.计数器
3.线程排队集合
4.线程等待集合

计数器,情况如下。初始值是0,1.第一次获取,加1 2.如果当前线程再次获取,每次自增1 3.如果是否锁,减1。
同一个线程,再次获取,这就是锁的可重入特性。

线程排队集合,情况如下。1.如果获取到锁,那么持有锁 2.如果没有,那么进入排队集合。

等待集合,情况如下。1.线程获取到锁 2.调用wait()方法,释放锁。加入等待集合。3.等待被别的线程唤醒,即调用notify或notifyAll()方法。


字节码
1.魔数这些东西
2.锁的进入指令enterLock和退出指令exitLock


jvm-如何访问对象


总结
java是由c++实现的。
若要理解java语法是怎么实现的?阅读c++源码,即jvm是如何实现的。

内置锁

内置锁主要包含两个方面
1.哪个锁
2.锁哪个数据


锁的本质
锁,其实就是对象。我们在说哪个锁,其实本质上是在问使用哪个对象。对象就是锁,锁就是对象。每个对象都有一个内置锁,二者一一对应,且互相只有一个。


锁哪个数据?
锁的目的是,要锁住哪个数据。即多线程不能篡改数据。

但是,具体的表现形式是,锁住的是一个代码块。不过,代码块的本质,也是访问数据/操作数据,所以,最终仍然还是为了锁数据。


内置锁的源码实现
1.对象
2.计数


对象
就是锁。


计数 每个对象,关联一个计数器


锁的可重入性
可重入指的是同一个对象/锁,可以重复获取,谁来获取?当然是线程。

获取锁的粒度是线程!只有线程才能获取锁!


锁和要保护的变量之间的关系
一一对应。也就是说,最好保护一个变量就只使用那一个锁来保护那个变量。而不要一个锁同时保护多个变量,那么可能出问题。

显式锁

显式锁和内置锁,本质上是一样的。
只不过,一个是使用关键字(jvm实现了获取锁和释放锁的方法),一个是使用锁封装类的lock()和unlock()方法。

synchronized-源码实现

其实就是上文提到的监视器对象MonitorObject.cpp

jvm对synchronized的优化

1.锁可重入
当前线程可重新获取同一个对象的锁。即同一个锁。
2.自旋锁
自旋锁就是循环获取锁。
锁的源码实现类,封装了lock()和unlock()方法。获取锁的实现,就是循环获取。
3.重量级锁
即普通的对象锁。


一步步是如何优化的,如何进入到下一步
先是锁可重入。这个是针对同一个线程。

其次,如果是不同的线程,那么此刻是自旋锁。即循环指定次数(几十次)的获取锁。

最后,不行,就正常的普通的锁,也就是所谓的重量级锁。该线程进入排队队列。


自旋锁的源码实现
1.普通的锁
获取锁的时候,只获取一次。

2.自旋锁
获取锁的时候,获取多次。

自旋锁和普通锁的唯一区别,就是获取锁的时候,获取几次的问题。


自旋锁的使用
具体使用的时候,自旋锁和普通锁,没有任何区别。所有的显式锁,都是以下两步:
1.获取锁lock()
2.释放锁unlock()

自旋锁,我们自己也可以实现。其实就是一个自旋锁类,封装了两个方法1.获取锁2.释放锁。

synchronized的自旋锁底层实现,也是同一个道理。


代码实现
1.普通锁

lock(){
    获取锁 //只获取一次,不行,就进入排队队列
}
复制代码

2.自旋锁

lock(){
    while(次数){
        获取锁 //获取锁的代码是一样的。主要是修改两个字段1.哪个线程持有锁2.计数器
    }
}
复制代码

为什么要弄一个自旋锁,因为普通锁,获取一次,获取不到,该线程就进入线程排队队列。再次执行该线程的时候,会发生线程上下文的切换,因为线程进入排队队列的期间,对应的cpu就去执行其他的线程去了。现在又要重新回来执行这个线程,这个过程就发生了至少两次线程上下文切换。而线程上下文切换,是非常耗资源的,具体耗费资源的原因就是,需要在用户态和内核态之间来回切换,线程上下文切换就是由内核来实现上下文切换这个操作的。

互斥锁和非互斥锁

在通常情况下我们说的锁都指的是“互斥”锁,因为在还存在一些特殊的锁,比如“读写锁”,不完全是互斥的。这篇文章说的锁专指互斥锁。


读写锁


读写锁如何实现

延伸-内存溢出和内存泄露的区别:内存泄露是内存溢出的一种

内存问题,说白了,就是不够用的问题。

所以,所有的内存问题,都是因为内存不够用导致的。

内存溢出,就是对象/数据不断地增多。而内存有限。所以,才会溢出嘛。

而内存泄露,其实,本质上,也是内存溢出/不够。只不过有一点细微的区别,就是内存泄露是因为已有的对象,没有做到很好的释放掉。比如,链表里的数据,只释放了第一个数据的内存,后面的数据都没有释放内存。这个时候,会导致剩下的所有数据,都不能被释放。

锁的本质是等待

先理解一下什么是自旋,所谓自旋就是线程在不满足某种条件的情况下,一直循环做某个动作。所以对于自旋锁来锁,当线程在没有获取锁的情况下,一直循环尝试获取锁,直到真正获取锁。

在聊聊高并发(三)锁的一些基本概念 我们提到锁的本质就是等待,那么如何等待呢,有两种方式

  1. 线程阻塞

  2. 线程自旋

阻塞的缺点显而易见,线程一旦进入阻塞(Block),再被唤醒的代价比较高,性能较差。自旋的优点是线程还是Runnable的,只是在执行空代码。当然一直自旋也会白白消耗计算资源,所以常见的做法是先自旋一段时间,还没拿到锁就进入阻塞。JVM在处理synchrized实现时就是采用了这种折中的方案,并提供了调节自旋的参数。


锁的本质是等待,等待的本质是线程阻塞
不管是锁,还是线程阻塞,还是等待。最终落实到源码层面,就是监视器对象MonitorObject的1.线程排队集合2.线程等待集合(即调用了wait()方法)。


Object.wait()方法
Wait方法的本质,也是线程加入到线程等待集合。因为,最底层,jvm还是要调用MonitorObject.cpp的wait()方法,把线程加入到线程等待集合。


线程阻塞的本质是什么?
待补充。


参考
www.jianshu.com/p/f4454164c…
www.jianshu.com/p/3256473f5…
blog.csdn.net/raintungli/…
coderbee.net/index.php/c…

这些参考文章都写的不好,仅供参考。

参考

baijiahao.baidu.com/s?id=161214…

blog.csdn.net/u010372981/…

blog.csdn.net/hellozhxy/a…
blog.csdn.net/iter_zc/art…

www.cnblogs.com/YDDMAX/p/56…

转载于:https://juejin.im/post/5cfddf48e51d455d877e0d04

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值