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

背景

想要弄清楚这些问题,须要弄清楚其余的不少问题。
好比,对象,而对象自己又能够延伸出不少其余的问题。php

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

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

由于只有了解对象,特别是深入的理解对象,才能理解同步的问题。c++

对象

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

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

数据,就是数据咯。多线程

空白填充,是一段没有使用的内存。为何要没有使用的内存,不是浪费吗?由于不少场合,须要申请的内存是什么的倍数,好比,对象须要是8个字节的倍数。因此,内存不够,填充空白字节便可,直到达到8个字节的倍数要求为止。并发


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

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

线程排队集合,状况以下。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…

相关文章
相关标签/搜索