八股
1.计网
- OSI 七层协议 TCP/IP五层协议
物理层,数据链路层,网络层,运输层,会话层,表示层, 应用层
物理层,数据链路层,网络层,运输层,应用层
- TCP与UDP区别
TCP | UDP |
---|---|
字节流 | 报文 |
可靠,面向连接 | 不可靠,不需要连接 |
点对点通信 | 点对点,多播,广播 |
滑动窗口,拥塞控制 | 无 |
头部最小20字节 | 最小8字节,更高效 |
从连接后发送数据的角度,全双工 | 每次发送是单向的,所以不是 |
- 为何TCP可靠
TCP有三次握手建立连接,四次挥手关闭连接的机制。
有滑动窗口和拥塞控制算法。
超时重传的机制。
对于每份报文也存在校验,保证每份报文可靠性。
为何UDP不可靠
UDP面向数据报无连接的,数据报发出去,不保留数据备份。没有重传,拥塞控制等
UDP报文过长的话交给IP切成小段,如果某段报文错误整个重传。
- 简述TCP粘包现象
TCP是面向字节流协议因此会将多个小尺寸数据被封装在一个tcp报文中发出去的可能性。
可以简单的理解成客户端调用了两次send,服务器端一个recv就把信息都读出来了。
TCP粘包现象处理方法
固定发送信息长度,或在两个信息之间加入分隔符。
- 简述TCP协议的滑动窗口
滑动窗口是传输层进行流量控制的一种措施,接收方通过通告发送方自己的窗口大小,从而控制发送方的发送速度,防止发送方发送速度过快而导致自己被淹没。
- 简述TCP协议的拥塞控制
拥塞是指一个或者多个交换点的数据报超载,TCP又会有重传机制,导致过载。
为了防止拥塞窗口cwnd增长过大引起网络拥塞,还需要设置一个慢开始门限ssthresh状态变量.
当cwnd < ssthresh 时,使用慢开始算法。
当cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。
当cwnd = ssthresh 时,即可使用慢开始算法,也可使用拥塞避免算法。
慢开始:由小到大逐渐增加拥塞窗口的大小,每接一次报文,cwnd指数增加。
拥塞避免:cwnd缓慢地增大,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1。
快恢复之前的策略:发送方判断网络出现拥塞,就把ssthresh设置为出现拥塞时发送方窗口值的一 半, 继续执行慢开始,之后进行拥塞避免。
快恢复:发送方判断网络出现拥塞,就把ssthresh设置为出现拥塞时发送方窗口值的一半,并把 cwnd设置为ssthresh的一半,之后进行拥塞避免。
- 简述快重传
如果在超时重传定时器溢出之前,接收到连续的三个重复冗余ACK,发送端便知晓哪个报文段在传输过程中丢失了,于是重发该报文段,不需要等待超时重传定时器溢出再发送该报文。
- TCP三次握手过程
-
第一次握手:客户端将标志位SYN置为1,随机产生一个值序列号seq=j,并将该数据包发送给服务 端,客户端进入syn_sent状态,等待服务端确认。
-
第二次握手:服务端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务端将标志位SYN 和 ACK都置为1,ack=j+1,随机产生一个值seq=k,并将该数据包发送给客户端以确认连接请求,服务 端进入syn_rcvd状态。
-
第三次握手:客户端收到确认后检查,如果正确则将标志位ACK为1,ack=k+1,并将该数据包发送给 服务端,服务端进行检查如果正确则连接建立成功,客户端和服务端进入established状态,完成三次握手,随后客户端和服务端之间可以开始传输数据了
-
为什么TCP握手需要三次,两次行不行?
- 三次握手才可以阻止重复历史连接的初始化(主要原因)
- 三次握手才可以同步双方的初始序列号
主要是因为在两次握手的情况下,「被动发起方」没有中间状态给「主动发起方」来阻止历史连接,导致「被动发起方」可能建立一个历史连接,造成资源浪费。
- 简述半连接队列
TCP握手中,当服务器处于SYN_RCVD 状态,服务器会把此种状态下请求连接放在一个队列里,该队列称为半连接队列。
- 简述SYN攻击
SYN攻击利用TCP协议缺陷,发送大量的半连接请求,占用半连接队列,耗费CPU和内存资源。
优化方式: 1. 缩短SYN Timeout时间 2. 记录IP,若连续受到某个IP的重复SYN报文,从这个IP地址来的包会被一概丢弃。
- TCP四次挥手过程
-
第一次:客户端发送一个FIN,用来关闭客户端到服务端的数据传送,客户端进入fin_wait_1状态。
-
第二次:服务端收到FIN,发送一个ACK给客户端,确认序号收到序号+1,服务端进入Close_wait状态。此时TCP连接处于半关闭状态,即客户端已经没有要发送的数据了,但服务端若发送数据,则客户端仍要接收。
-
第三次:服务端发送一个FIN,用来关闭服务端到客户端的数据传送,服务端进入Last_ack状态。
-
第四次:客户端收到FIN后,客户端进入Time_wait状态,接着发送一个ACK给服务端,确认后,服务端进入Closed状态,完成四次挥手。
-
为什么TCP挥手需要4次
主要原因是当服务端收到客户端的 FIN 数据包后,服务端可能还有数据没发完,不会立即close。 所以服务端会先将 ACK 发过去告诉客户端我收到你的断开请求了,但请再给我一点时间,这段时间用来发送剩下的数据报文,发完之后再将 FIN 包发给客户端表示现在可以断了。之后客户端需要收到 FIN 包后发送 ACK 确认断开信息给服务端。
- 为什么四次挥手释放连接时需要等待2MSL
MSL即报文最大生存时间。设置2MSL可以保证上一次连接的报文已经在网络中消失,不会出现与新 TCP连接报文冲突的情况。
避免最后一个ACK没收到重发FIN报文,与新连接冲突。
- 简述DNS解析过程
1、客户机发出查询请求,在本地计算机缓存查找,若没有找到,就会将请求发送给dns服务器
2、本地dns服务器会在自己的区域里面查找,找到即根据此记录进行解析,若没有找到,就会在本地的 缓存里面查找
3、本地服务器没有找到客户机查询的信息,就会将此请求发送到根域名dns服务器
4、根域名服务器解析客户机请求的根域部分,它把包含的下一级的dns服务器的地址返回到客户机的 dns服务器地址
5、客户机的dns服务器根据返回的信息接着访问下一级的dns服务器
6、这样递归的方法一级一级接近查询的目标,最后在有目标域名的服务器上面得到相应的IP信息
7、客户机的本地的dns服务器会将查询结果返回给我们的客户机
8、客户机根据得到的ip信息访问目标主机,完成解析过程
- DNS 服务器来自客户端的查询消息包含
(a)域名 :服务器、邮件服务器(邮件地址中 @ 后面的部分)的名称
(b) Class :Class 的值永远是代表互联网的 IN
(c)记录类型
表示域名对应何种类型的记录。当类型为 A 时,表示域名对应的是 IP 地址;当类型为 MX 时,表示域名对应的是邮件服务器。
- 简述HTTP协议
http协议是超文本传输协议。它是基于TCP协议的应用层传输协议,即客户端和服务端进行数据传输的 一种规则。该协议本身HTTP 是一种无状态的协议。
- 简述cookie, session
HTTP 协议本身是无状态的,为了使其能处理更加复杂的逻辑,HTTP/1.1 引入 Cookie 来保存状态信息。 Cookie是由服务端产生的,再发送给客户端保存,当客户端再次访问的时候,服务器可根据cookie辨识客户端是哪个,以此可以做个性化推送,免账号密码登录等等。
session用于标记特定客户端信息,存在服务器的一个文件里。 一般客户端带Cookie对服务器进行访问,可通过cookie中的session id从整个session中查询到服务器记 录的关于客户端的信息。
- 简述http状态码和对应的信息
1XX:接收的信息正在处理
2XX:请求正常处理完毕
3XX:重定向
4XX:客户端错误
5XX:服务端错误
常见错误码:
301:永久重定向 302:临时重定向 304:资源没修改,用之前缓存就行 400:客户端请求的报文有错误 403:表示服务器禁止访问资源 404:表示请求的资源在服务器上不存在或未找到
- 转发和重定向的区别
转发是服务器行为。服务器直接向目标地址访问URL,将相应内容读取之后发给浏览器,用户浏览器地址栏URL不变,转发页面和转发到的页面可以共享request里面的数据。 快
重定向是利用服务器返回的状态码来实现的,如果服务器返回301或者302,浏览器收到新的消息后自动跳转到新的网址重新请求资源。用户的地址栏url会发生改变,而且不能共享数据。 慢
Request对象的作用是与客户端交互,收集客户端的Form、Cookies、超链接,或者收集服务器端的环境变量。
- http1.1的改进
HTTP1.1默认开启长连接,在一个TCP连接上可以传送多个HTTP请求和响应。使用 TCP 长连接的方式改善了 HTTP/1.0 短连接造成的性能开销。
支持管道(pipeline)网络传输,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。
- Get与Post区别
Get:指定资源请求数据,刷新无害,Get请求的数据会附加到URL中,传输数据的大小受到url的限制。
Post:向指定资源提交要被处理的数据。刷新会使数据会被重复提交。post在发送数据前会先将请求头发送给服务器进行确认,然后才真正发送数据。
- 消息头的头字段
- REST API
REST API全称为表述性状态转移(Representational State Transfer,REST)即利用HTTP中get、 post、put、delete以及其他的HTTP方法构成REST中数据资源的增删改查操作:
Create : POST
Read : GET
Update : PUT/PATCH
Delete: DELETE
-
浏览器中输入一个网址后,具体发生了什么
-
进行DNS解析操作,根据DNS解析的结果查到服务器IP地址
-
通过ip寻址和arp,找到服务器,并利用三次握手建立TCP连接
-
浏览器生成HTTP报文,发送HTTP请求,等待服务器响应
-
服务器处理请求,并返回给浏览器
-
根据HTTP是否开启长连接,进行TCP的挥手过程
-
浏览器根据收到的静态资源进行页面渲染
-
格式
2.jvm
- JVM内存模型
线程私有的运行时数据区: 程序计数器、Java 虚拟机栈、本地方法栈。
线程共享的运行时数据区:Java 堆、方法区。
- 程序计数器
程序计数器表示当前线程所执行的字节码的行号指示器。
程序计数器不会产生StackOverflowError和OutOfMemoryError。
- 虚拟机栈
Java 虚拟机栈用来描述 Java 方法执行的内存模型。
线程创建时就会分配一个栈空间,线程结束后栈空间被回收。
栈中元素用于支持虚拟机进行方法调用,每个方法在执行时都会创建一个栈帧存储方法的局部变量表、 操作栈、动态链接和返回地址等信息。
虚拟机栈会产生两类异常:
StackOverflowError:线程请求的栈深度大于虚拟机允许的深度抛出。
OutOfMemoryError:如果 JVM 栈容量可以动态扩展,虚拟机栈占用内存超出抛出。
- 本地方法栈
本地方法栈与虚拟机栈作用相似,不同的是虚拟机栈为虚拟机执行 Java 方法服务,本地方法栈为本地方法服务。可以将虚拟机栈看作普通的java函数对应的内存模型,本地方法栈看作由native关键词修饰的函数对应的内存模型。
本地方法栈会产生两类异常:
StackOverflowError:线程请求的栈深度大于虚拟机允许的深度抛出。
OutOfMemoryError:如果 JVM 栈容量可以动态扩展,虚拟机栈占用内存超出抛出。
- 堆
堆主要作用是存放对象实例,Java 里几乎所有对象实例都在堆分配内存,堆也是内存管理中最大的一 块。Java的垃圾回收主要就是针对堆这一区域进行。 可通过 -Xms 和 -Xmx 设置堆的最小和最大容量。
堆会抛出 OutOfMemoryError异常。
- 方法区
方法区用于存储被虚拟机加载的类信息、常量、静态变量等数据。 JDK6之前使用永久代实现方法区,容易内存溢出。JDK7 把放在永久代的字符串常量池、静态变量等移出到堆,JDK8 中抛弃永久代,改用在本地内存中实现的元空间来实现方法区,把 JDK 7 中永久代内容移到元空间。
方法区会抛出 OutOfMemoryError异常。
- 运行时常量池
运行时常量池存放常量池表,用于存放编译器生成的各种字面量与符号引用。一般除了保存 Class 文件中描述的符号引用外,还会把符号引用翻译的直接引用也存储在运行时常量池。
- 直接内存
直接内存也称为堆外内存,就是把内存对象分配在JVM堆外的内存区域。这部分内存不是虚拟机管理, 而是由操作系统来管理。 Java通过通过DriectByteBuffer对其进行操作,避免了在 Java 堆和 Native堆来回复制数据。
-
java创建对象的过程
-
检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查引用代表的类是否已被加载、 解析和初始化,如果没有就先执行类加载。
-
通过检查通过后虚拟机将为新生对象分配内存。
-
完成内存分配后虚拟机将成员变量设为零值
-
设置对象头,包括哈希码、GC 信息、锁信息、对象所属类的类元信息等。
-
执行 init 方法,初始化成员变量,执行代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。
-
JVM给对象分配内存的策略
-
指针碰撞:内存中放一个指针作为分界指示器将使用过的内存放在一边,空闲的放在另 一边,通过指针挪动完成分配。
-
空闲列表: 对于 Java 堆内存不规整的情况,虚拟机必须维护一个列表记录哪些内存可用,在分 配时从列表中找到一块足够大的空间划分给对象并更新列表记录。
-
内存分配是如何保证线程安全的
-
对分配内存空间采用CAS机制,配合失败重试的方式保证更新操作的原子性。该方式效率低。
-
本地线程分配缓冲(TLAB):每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块"私 有"内存中分配。一般采用这种策略。
-
对象的内存布局
对象在堆内存的存储布局可分为对象头、实例数据和对齐填充。
对象头主要包含两部分数据: MarkWord、类型指针。
MarkWord 用于存储哈希码(HashCode)、GC 分代年龄、锁状态标志位、线程持有的锁、偏向线程ID等信息。
类型指针即对象指向他的类元数据指针,如果对象是一个 Java 数组,会有一块用于记录数组长度的数据
实例数据存储代码中所定义的各种类型的字段信息。 对齐填充起占位作用。HotSpot 虚拟机要求对 象的起始地址必须是8的整数倍,因此需要对齐填充。
- 如何判断对象是否是垃圾
引用计数法:设置引用计数器,对象被引用计数器加 1,引用失效时计数器减 1,如果计数器为 0 则被标记为垃圾。会存在对象间循环引用的问题,一般不使用这种方法。
可达性分析:通过 GC Roots 的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,如果某个对象没有被搜到,则会被标记为垃圾。
可作为 GC Roots 的对象包括虚拟机栈和本地方法栈中引用的对象、类静态属性引用的对象、常量引用的对象。
- 引用类型
强引用: 被强引用关联的对象不会被回收。一般采用 new 方法创建强引用。
软引用:只被软引用关联的对象,会在系统将要发生内存溢出前,将这些对象列进行回收范围内进行二次回收。一般采用 SoftReference 类来创建软引用。
弱引用:只能活到下一次垃圾收集发生。当垃圾收集器开始工作后,都会回收只被弱引用关联的对象。一般采用 WeakReference 类来创建弱引用。
虚引用: 无法通过该引用获取对象。唯一目的就是为了能在对象被回收时收到一个系统通知。虚引用必须与引用队列联合使用。
- 标记清除算法、标记整理算法和标记复制算法
标记清除算法:先标记需清除的对象,之后统一回收。这种效率不高,会产生大量不连续的碎片。
标记整理算法:先标记存活对象,然后让所有存活对象向一端移动,之后清理端边界以外的内存
标记复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当使用的这块空间用完了,就将存活对象复制到另一块,再把已使用过的内存空间一次清理掉。
- 简述分代收集算法
根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将堆分为新生代和老年代,对这两块采用不同的算法。
新生代使用:标记复制算法 老年代使用:标记清除或者标记整理算法
- Serial + Serial Old垃圾收集器
单线程串行收集器。垃圾回收的时候,必须暂停其他所有线程。新生代使用标记复制算法,老年代使用标记整理算法。简单高效。
ParNew垃圾收集器
可以看作Serial垃圾收集器的多线程版本,新生代使用标记复制算法
Parallel Scavenge垃圾收集器
一款基于标记-复制算法的新生代收集器,支持多线程收集。与其他收集器不同的是其更关注达到 一个可控制的吞吐量(处理器用于运行用户代码的时间与总时间的比值)。可通过参数设置最大停顿时间和吞吐量大小。并且其支持自适应调整(新生代的大小比例等各种参数)。
CMS垃圾收集器
注重最短时间停顿。CMS垃圾收集器为最早提出的并发收集器,垃圾收集线程与用户线程同时工 作。采标记清除算法。
- 初始标记(STW):仅仅标记GC Roots能直接关联的对象,速度很快。
- 并发标记:从直接关联对象遍历整个对象图,耗时较长,但可以和用户线程并发运行。
- 重新标记(STW):修正并发标记期间,产生变动的对象的标记记录(增量更新法)。
- 并发清除:由于采用清除算法,可以并发运行。
G1垃圾收集器
和之前收集器不同,该垃圾收集器把堆划分成多个大小相等的独立区域(Region),新生代和老年 代不再物理隔离。通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。
- 初始标记(Initial Marking):只标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。
- 并发标记(Concurrent M arking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆
里的对象图。当对象图扫描完成以后,重新处理SATB记录下的在并发时有引用变动的对象。
- 最终标记(Final M arking):处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
- 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划, 可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。
- Minor GC
指发生在新生代的垃圾收集,Java 对象大多存活时间短,所以 Minor GC 非常频繁,一般回收速度也比较快。
Full GC
Full GC 是清理整个堆空间—包括年轻代和永久代。调用System.gc(),老年代空间不足,空间分配担保失败,永生代空间不足,G1,CMS分配空间不足会产生full gc。
- 内存分配策略
大多数情况下对象在新生代 Eden 区分配,当 Eden 没有足够空间时将发起一次 Minor GC。 大对象需要大量连续内存空间,直接进入老年代区分配。
如果经历过第一次 Minor GC 仍然存活且能被 Survivor 容纳,该对象就会被移动到 Survivor 中并将年龄设置为 1,并且每熬过一次 Minor GC 年龄就加 1 ,当增加到一定程度(默认15)就会被晋升到老年代。
如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 的一半,年龄不小于该年龄的对象就可以直接进入老年代。
空间分配担保。MinorGC 前虚拟机必须检查老年代最大可用连续空间是否大于新生代对象总空间,如果满足则说明这次 Minor GC 确定安全。如果不,JVM会查看HandlePromotionFailure 参数是否允许担保失败,如果允许会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果满足将Minor GC,否则改成一次 FullGC。
- JVM常见调优参数
-Xms 初始堆大小
-Xmx 最大堆大小
-XX:NewSize 年轻代大小
-XX:MaxNewSize 年轻代最大值
-XX:PermSize 永生代初始值
-XX:MaxPermSize 永生代最大值
-XX:NewRatio 新生代与老年代的比例
- JVM类加载过程
加载: 1. 通过全类名获取类的二进制字节流. 2. 将类的静态存储结构转化为方法区的运行时数据结构。 3. 在堆中生成类的Class对象,作为方法区数据的入口。
链接:
验证:对文件格式,元数据,字节码,符号引用等验证正确性。
准备:在方法区内为类变量分配内存并设置为0值。
解析:将符号引用转化为直接引用。
初始化:执行类构造器clinit方法,真正初始化。
- JVM中的类加载器
BootstrapClassLoader启动类加载器:加载/lib下的jar包和类。C++编写。 ExtensionClassLoader 扩展类加载器: /lib/ext目录下的jar包和类。java编写。
AppClassLoader应用类加载器,加载当前classPath下的jar包和类。java编写。
- 双亲委派机制
一个类加载器收到类加载请求之后,首先判断当前类是否被加载过。已经被加载的类会直接返回,如果没有被加载,首先将类加载请求转发给父类加载器,一直转发到启动类加载器,只有当父类加载器无法完成时才尝试自己加载。
加载类顺序:BootstrapClassLoader->ExtensionClassLoader->AppClassLoader->CustomClassLoader (自定义加载器)
检查类是否加载顺序: CustomClassLoader->AppClassLoader->ExtensionClassLoader->BootstrapClassLoader
-
避免类的重复加载。相同的类被不同的类加载器加载会产生不同的类,双亲委派保证了java程序 的稳定运行。
-
保证核心API不被修改。
-
如何破坏双亲委派机制
重载loadClass()方法,即自定义类加载器
-
如何构建自定义类加载器
-
新建自定义类继承自java.lang.ClassLoader
-
重写findClass、loadClass、defineClass方法
3.Java基础
-
Java语言具有哪些特点?
-
Java为纯面向对象的语言。
-
具有平台无关性。java利用Java虚拟机运行字节码,无论是在Windows、Linux还是MacOS等其它平台对Java程序进行编译,编译后的程序可在其它平台运行。
-
Java提供了很多内置类库。如对多线程支持,对网络通信支持,提供垃圾回收器。
-
Java具有较好的安全性和健壮性。Java提供了异常处理和垃圾回收机制,去除了C++中难以理解的指针特性。
-
Java语言提供了对Web应用开发的支持。
-
面向对象的三大特性?
-
继承:对象的一个新类可以从现有的类中派生,派生类可以从它的基类那继承方法和实例变量,且派生类可以修改或新增新的方法使之更适合特殊的需求。
-
封装:将客观事物抽象成类,每个类可以把自身数据和方法只让可信的类或对象操作,对不可信的进行信息隐藏。
-
多态:允许不同类的对象对同一消息作出响应。不同对象调用相同方法即使参数也相同,最终表现行为是不一样的。
-
字节序定义以及Java属于哪种字节序?
字节序是指多字节数据在计算机内存中存储或网络传输时个字节的存储顺序。
通常由小端和大端两组方 式。
-
小端:低位字节存放在内存的低地址端,高位字节存放在内存的高地址端。
-
大端:高位字节存放在内存的低地址端,低位字节存放在内存的高地址端。
Java语言的字节序是大端。
-
JDK与JRE有什么区别?
-
JDK:Java开发工具包(Java Development Kit),提供了Java的开发环境和运行环境。
-
JRE:Java运行环境(Java Runtime Environment),提供了Java运行所需的环境。 JDK包含了JRE。如果只运行Java程序,安装JRE即可。要编写Java程序需安装JDK.
-
简述Java访问修饰符
default: 默认访问修饰符,在同一包内可见
private: 在同一类内可见,不能修饰类
protected : 对同一包内的类和所有子类可见,不能修饰类
public: 对所有类可见
- 构造方法、成员变量初始化以及静态成员变量三者的初始化顺序?
先后顺序:静态成员变量、成员变量、构造方法。
详细的先后顺序:父类静态变量、父类静态代码块、子类静态变量、子类静态代码块、父类非静态变 量、父类非静态代码块、父类构造函数、子类非静态变量、子类非静态代码块、子类构造函数
- 接口和抽象类的相同点和区别?
相同点:
-
都不能被实例化。
-
接口的实现类或抽象类的子类需实现接口或抽象类中相应的方法才能被实例化。
不同点:
-
接口只能有方法定义,不能有方法的实现,而抽象类可以有方法的定义与实现。
-
实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,只能继承一个抽象类。
-
当子类和父类之间存在逻辑上的层次结构,推荐使用抽象类,有利于功能的累积。当功能不需要, 希望支持差别较大的两个或更多对象间的特定交互行为,推荐使用接口。使用接口能降低软件系统的耦合度,便于日后维护或添加删除方法。
-
为什么Java语言不支持多重继承?
-
为了程序的结构能够更加清晰从而便于维护。假设Java语言支持多重继承,类C继承自类A和类B, 如果类A和B都有自定义的成员方法f(),那么当代码中调用类C的f()会产生二义性。Java语言通过实现多个接口间接支持多重继承,接口由于只包含方法定义,不能有方法的实现,类C继承接口A与接口B时即使它们都有方法f(),也不能直接调用方法,需实现具体的f()方法才能调用,不会产生二义性。
-
多重继承会使类型转换、构造方法的调用顺序变得复杂,会影响到性能。
-
重载与覆盖的区别?
-
覆盖是父类与子类之间的关系,是垂直关系;重载是同一类中方法之间的关系,是水平关系。
-
覆盖只能由一个方法或一对方法产生关系;重载是多个方法之间的关系。
-
覆盖要求参数列表相同;重载要求参数列表不同。
-
覆盖中,调用方法体是根据对象的类型来决定的,而重载是根据调用时实参表与形参表来对应选择方法体。
-
重载方法可以改变返回值的类型,覆盖方法不能改变返回值的类型。
-
final、finally和finalize的区别是什么?
-
final用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖、类不可继承。
-
finally作为异常处理的一部分,只能在try/catch语句中使用,finally附带一个语句块用来表示这个语句最终一定被执行,经常被用在需要释放资源的情况下。
-
finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的finalize()方法。当垃圾回收器准备好释放对象占用空间时,首先会调用finalize()方法,并在下一次垃圾回收动作发生时真正回收对象占用的内存。
-
出现在Java程序中的finally代码块是否一定会执行?
当遇到下面情况不会执行。
-
当程序在进入try语句块之前就出现异常时会直接结束。
-
当程序在try块中强制退出时,如使用System.exit(0),也不会执行finally块中的代码。
其它情况下,在try/catch/finally语句执行的时候,try块先执行,当有异常发生,catch和finally进行处理后程序就结束了,当没有异常发生,在执行完finally中的代码后,后面代码会继续执行。值得注意的是,当try/catch语句块中有return时,finally语句块中的代码会在return之前执行。如果try/catch/finally块中都有return语句,finally块中的return语句会覆盖try/catch模块中的return语句。
- Java语言中关键字static的作用是什么?
static的主要作用有两个:
-
为某种特定数据类型或对象分配与创建对象个数无关的单一的存储空间。
-
使得某个方法或属性与类而不是对象关联在一起,即在不创建对象的情况下可通过类直接调用方法或使用类的属性。
具体而言static又可分为4种使用方式:
-
修饰成员变量。用static关键字修饰的静态变量在内存中只有一个副本。只要静态变量所在的类被加载,这个静态变量就会被分配空间,可以使用''类.静态变量''和''对象.静态变量''的方法使用。 2. 修饰成员方法。static修饰的方法无需创建对象就可以被调用。static方法中不能使用this和super关键字,不能调用非static方法,只能访问所属类的静态成员变量和静态成员方法。
-
修饰代码块。JVM在加载类的时候会执行static代码块。static代码块常用于初始化静态变量。static代码块只会被执行一次。
-
修饰内部类。static内部类可以不依赖外部类实例对象而被实例化。静态内部类不能与外部类有相同的名字,不能访问普通成员变量,只能访问外部类中的静态成员和静态成员方法。
-
Java代码块执行顺序
-
父类静态代码块(只执行一次)
-
子类静态代码块(只执行一次)
-
父类构造代码块 (类中)
-
父类构造函数
-
子类构造代码块
-
子类构造函数
-
普通代码块 (方法中)
-
Java中一维数组和二维数组的声明方式?
一维数组的声明方式: 1. type arrayName[] 2. type[] arrayName
二维数组的声明方式: 1. type arrayName[][] 2. type[][] arrayName 3. type[] arrayName[]
其中type为基本数据类型或类,arrayName为数组名字
- 判等运算符==与equals的区别?
== 比较的是引用,equals比较的是内容。
-
如果变量是基础数据类型,== 用于比较其对应值是否相等。如果变量指向的是对象,== 用于比较两个对象是否指向同一块存储空间。
-
equals是Object类提供的方法之一,每个Java类都继承自Object类,所以每个对象都具有equals这个方法。Object类中定义的equals方法内部是直接调用 == 比较对象的。但通过覆盖的方法可以让它不是比较引用而是比较数据内容。
-
为什么要把String设计为不变量?
-
节省空间:字符串常量存储在JVM的字符串池中可以被用户共享。
-
提高效率:String会被不同线程共享,是线程安全的。在涉及多线程操作中不需要同步操作。
-
安全:String常用于用户名、密码、文件名等,由于其不可变,可避免黑客行为对其恶意修改。
-
序列化是什么?
序列化是一种将对象转换成字节序列的过程,用于解决在对对象流进行读写操作时所引发的问题。序列化可以将对象的状态写在流里进行网络传输,或者保存到文件、数据库等系统里,并在需要的时候把该流读取出来重新构造成一个相同的对象。
实现:实现Serializable接口,或实现Externalizable接口中writeExternal()与readExternal()方法。
- 简述Java中Class对象
java中对象可以分为实例对象和Class对象,每一个类都有一个Class对象,其包含了与该类有关的信息。
获取Class对象的方法:
Class.forName(“类的全限定名”)
实例对象.getClass()
类名.class
基本数据类型.class Class integerClass = int.class;
包装类. TYPE Class type = Integer.TYPE;
- Java反射机制是什么?
Java反射机制是指在程序的运行过程中可以构造任意一个类的对象、获取任意一个类的成员变量和成员方法、获取任意一个对象所属的类信息、调用任意一个对象的属性和方法。反射机制使得Java具有动态获取程序信息和动态调用对象方法的能力。
可以通过以下类调用反射API。
Class类:可获得类属性方法
Field类:获得类的成员变量
Method类:获取类的方法信息
Construct类:获取类的构造方法等信息
- 简述注解
Java 注解用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。其可以用于提供信息给编译器,在编译阶段时给软件提供信息进行相关的处理,在运行时处理写相应代码,做对应操作。
- 简述元注解
元注解可以理解为注解的注解,即在注解中使用,实现想要的功能。
其具体分为:
@Retention: 注解存在阶段是保留在源码,还是在字节码(类加载)或者运行期(JVM中运行)。
@Target:表示注解作用的范围。
@Documented:将注解中的元素包含到 Javadoc 中去。
@Inherited:一个被@Inherited注解了的注解修饰了一个父类,如果他的子类没有被其他注解修 饰,则它的子类也继承了父类的注解。
@Repeatable:被这个元注解修饰的注解可以同时作用一个对象多次,但是每次作用注解又可以代 表不同的含义。
- 简述Java异常的分类
Java异常分为Error(程序无法处理的错误)和Exception(程序本身可以处理的异常)。
这两个类均继承Throwable。
Error常见的有StackOverFlowError,OutOfMemoryError等等。
Exception可分为运行时异常和非运行时异常。对于运行时异常,可以利用try catch的方式进行处理,也可以不处理。对于非运行时异常,必须处理,不处理的话程序无法通过编译。
- 简述throw与throws的区别
throw一般是用在方法体的内部,由开发者定义当程序语句出现问题后主动抛出一个异常。
throws一般用于方法声明上,代表该方法可能会抛出的异常列表。
- 简述泛型
泛型,即“参数化类型”,解决不确定对象具体类型的问题。在编译阶段有效。在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型在类中称为泛型类、接口中称为泛型接口和方法中称为泛型方法。
- 简述Java基本数据类型
byte: 占用1个字节,取值范围-128 ~ 127
short: 占用2个字节,取值范围-2^15 ~ 2^15 -1
char: 占用2个字节
int:占用4个字节,取值范围-2^31 ~ 2^31 -1
float:占用4个字节
long:占用8个字节
double:占用8个字节
boolean:占用大小根据实现虚拟机不同有所差异
- 简述自动装箱拆箱
对于Java基本数据类型,均对应一个包装类。 装箱就是自动将基本数据类型转换为包装器类型,如int->Integer 拆箱就是自动将包装器类型转换为基本数据类型,如Integer->int
- 简述重载与重写的区别
重写即子类重写父类的方法,方法对应的形参和返回值类型都不能变。
重载即在一个类中,方法名相同,参数类型或数量不同。
- 简述java的多态
Java多态可以分为编译时多态和运行时多态。
编译时多态主要指方法的重载,即通过参数列表的不同来区分不同的方法。
运行时多态主要指继承父类和实现接口时,可使用父类引用指向子类对象。 运行时多态的实现:主要依靠方法表,方法表中最先存放的是Object类的方法,接下来是该类的父类的方法,最后是该类本身的方法。如果子类改写了父类的方法,那么子类和父类的那些同名方法共享一个方法表项,都被认作是父类的方法。因此可以实现运行时多态。
- 简述抽象类与接口的区别
抽象类:体现的是is-a的关系,如对于man is a person,就可以将person定义为抽象类。
接口:体现的是can的关系。是作为模板实现的。如设置接口fly,plane类和bird类均可实现该接口。 一个类只能继承一个抽象类,但可以实现多个接口。
-
简述Object类常用方法
-
hashCode:通过对象计算出的散列码。用于map型或equals方法。需要保证同一个对象多次调用该方法,总返回相同的整型值。
-
equals:判断两个对象是否一致。需保证equals方法相同对应的对象hashCode也相同。
-
toString: 用字符串表示该对象
-
clone:深拷贝一个对象
-
简述内部类及其作用
成员内部类:作为成员对象的内部类。可以访问private及以上外部类的属性和方法。外部类想要访 问内部类属性或方法时,必须要创建一个内部类对象,然后通过该对象访问内部类的属性或方法。 外部类也可访问private修饰的内部类属性。
局部内部类:存在于方法中的内部类。访问权限类似局部变量,只能访问外部类的final变量。
匿名内部类:只能使用一次,没有类名,只能访问外部类的final变量。
静态内部类:类似类的静态成员变量。
- 简述String/StringBuffer与StringBuilder
String类采用利用final修饰的字符数组进行字符串保存,因此不可变。如果对String类型对象修改,需要新建对象,将老字符和新增加的字符一并存进去。
StringBuilder,采用无final修饰的字符数组进行保存,因此可变。但线程不安全。
StringBuffer,采用无final修饰的字符数组进行保存,可理解为实现线程安全的StringBuilder。
- 简述JAVA的List
List是一个有序队列,在JAVA中有两种实现方式:
ArrayList 使用数组实现,是容量可变的非线程安全列表,随机访问快,集合扩容时会创建更大的数组, 把原有数组复制到新数组。
LinkedList 本质是双向链表,与 ArrayList 相比插入和删除速度更快,但随机访问元素很慢。
- Java中线程安全的基本数据结构有哪些
HashTable: 哈希表的线程安全版,效率低
ConcurrentHashMap:哈希表的线程安全版,效率高,用于替代HashTable
Vector:线程安全版Arraylist
Stack:线程安全版栈
BlockingQueue及其子类:线程安全版队列
- 简述JAVA的Set
Set 即集合,该数据结构不允许元素重复且无序。
JAVA对Set有三种实现方式:
HashSet 通过 HashMap 实现,HashMap 的 Key 即 HashSet 存储的元素,Value系统自定义一个名为 PRESENT 的 Object 类型常量。判断元素是否相同时,先比较hashCode,相同后再利用equals比较, 查询O(1)
LinkedHashSet 继承自 HashSet,通过 LinkedHashMap 实现,使用双向链表维护元素插入顺序。 TreeSet 通过 TreeMap 实现的,底层数据结构是红黑树,添加元素到集合时按照比较规则将其插入合适 的位置,保证插入后的集合仍然有序。查询O(logn)
- 简述JAVA的HashMap
JDK8 之前底层实现是数组 + 链表,JDK8 改为数组 + 链表/红黑树。
主要成员变量包括存储数据的 table 数组、元素数量 size、加载因子 loadFactor。
HashMap 中数据以键值对的形式存在,键对应的 hash 值用来计算数组下标,如果两个元素 key 的 hash 值一样,就会发生哈希冲突,被放到同一个链表上。
table 数组记录 HashMap 的数据,每个下标对应一条链表,所有哈希冲突的数据都会被存放到同一条链表,
Node(继承与Entry)/Entry 节点包含四个成员变量:key、value、next 指针(链表)和 hash 值。在JDK8后链表超过8会转化为红黑树。 若当前数据/总数据容量>负载因子,Hashmap将执行扩容操作。 默认初始化容量为 16,扩容容量必须是 2 的幂次方、最大容量为 1<< 30 、默认加载因子为 0.75。
- 为何HashMap线程不安全
在JDK1.7中,HashMap采用头插法插入元素,因此并发情况下会导致环形链表,产生死循环。虽然JDK1.8采用了尾插法解决了这个问题,但是并发下的put操作也会使前一个key被后一个key覆盖。 HashMap有扩容机制存在,也存在A线程进行扩容后,B线程执行get方法出现失误的情况。
- java的TreeMap
TreeMap是底层利用红黑树实现的Map结构,底层实现是一棵平衡的排序二叉树,由于红黑树的插入、 删除、遍历时间复杂度都为O(logN),所以性能上低于哈希表。但是哈希表无法提供键值对的有序输出,红黑树可以按照键的值的大小有序输出。
-
Collection和Collections有什么区别?
-
Collection是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如List、Set等。
-
Collections是一个包装类,包含了很多静态方法、不能被实例化,而是作为工具类使用,比如提供的排序方法: Collections.sort(list);提供的反转方法:Collections.reverse(list)。
-
ArrayList、Vector和LinkedList有什么共同点与区别?
-
ArrayList、Vector和LinkedList都是可伸缩的数组,即可以动态改变长度的数组。
-
ArrayList和Vector都是基于存储元素的Object[] array来实现的,它们会在内存中开辟一块连续的空间来存储,支持下标、索引访问。但在涉及插入元素时可能需要移动容器中的元素,插入效率较 低。当存储元素超过容器的初始化容量大小,ArrayList与Vector均会进行扩容。
-
Vector是线程安全的,其大部分方法是直接或间接同步的。ArrayList不是线程安全的,其方法不具有同步性质。LinkedList也不是线程安全的。
-
LinkedList采用双向列表实现,对数据索引需要从头开始遍历,因此随机访问效率较低,但在插入元素的时候不需要对数据进行移动,插入效率较高。
-
HashMap和Hashtable有什么区别?
-
HashMap是Hashtable的轻量级实现,HashMap允许key和value为null,但最多允许一条记录的key为null.而HashTable不允许。
-
HashTable中的方法是线程安全的,而HashMap不是。在多线程访问HashMap需要提供额外的同步机制。
-
Hashtable使用Enumeration进行遍历,HashMap使用Iterator进行遍历。
-
fail-fast和fail-safe迭代器的区别是什么?
-
fail-fast直接在容器上进行,在遍历过程中,一旦发现容器中的数据被修改,就会立刻抛出 ConcurrentModificationException异常从而导致遍历失败。常见的使用fail-fast方式的容器有 HashMap和ArrayList等。
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。
集合在被遍历期间如果内容发生变化,就会改变modCount的值。
每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
Tip:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。
-
fail-safe这种遍历基于容器的一个克隆。因此对容器中的内容修改不影响遍历。常见的使用fail-safe 方式遍历的容器有ConcurrentHashMap和CopyOnWriteArrayList。
-
HashSet中,equals与hashCode之间的关系?
equals和hashCode这两个方法都是从object类中继承过来的,equals主要用于判断对象的内存地址引用 是否是同一个地址;hashCode根据定义的哈希规则将对象的内存地址转换为一个哈希码。HashSet中存储的元素是不能重复的,主要通过hashCode与equals两个方法来判断存储的对象是否相同:
-
如果两个对象的hashCode值不同,说明两个对象不相同。
-
如果两个对象的hashCode值相同,接着会调用对象的equals方法,如果equlas方法的返回结果为 true,那么说明两个对象相同,否则不相同。
4.数据库
1.Mysql
- 简述数据库三大范式
第一范式是最基本的范式。如果数据库表中的所有字段值都是不可分解的原子值,就说明该数据库表满足了第一范式。
数据库第二范式:关系模式必须满足第一范式,并且所有非主属性都完全依赖于主码。注意,符合第二范式的关系模型可能还存在数据冗余、更新异常等问题。关系模型(学号,姓名,专业编号,专业名称)中,学号->姓名,而专业编号->专业名称,不满足数据库第二范式
数据库第三范式:关系模型满足第二范式,所有非主属性对任何候选关键字都不存在传递依赖。即每个属性都跟主键有直接关系而不是间接关系。接着以学生表举例,对于关系模型(学号,姓名,年龄,性别,所在院校,院校地址,院校电话)院校地址,院校电话和学号不存在直接关系,因此不满足第三范式。
- 简述MySQL的架构
service层: 连接器,查询缓存,分析器,优化器,执行器
存储引擎层 负责mysql中数据的存储和提取。存储引擎有InnoDB(安全,支持事务)MyISAM(文件分为格式文件,数据文件,索引文件,支持压缩)、Memory(数据在内存中,快)
-
简述执行SQL语言的过程
-
客户端首先通过连接器进行身份认证和权限相关
-
如果是执行查询语句的时候,会先查询缓存,但MySQL 8.0 版本后该步骤移除。
-
没有命中缓存的话,SQL 语句就会经过解析器,分析语句,包括语法检查等等。
-
通过优化器,将用户的SQL语句按照 MySQL 认为最优的方案去执行。
-
执行语句,并从存储引擎返回数据。
-
简述MySQL的共享锁排它锁
共享锁也称为读锁,相互不阻塞,多个客户在同一时刻可以同时读取同一个资源而不相互干扰。
排他锁也称为写锁,会阻塞其他的写锁和读锁,确保在给定时间内只有一个用户能执行写入并防止其他用户读取正在写入的同一资源。
- 简述MySQL中的按粒度的锁分类
表级锁: 对当前操作的整张表加锁,实现简单,加锁快,但并发能力低。
行锁: 锁住某一行,如果表存在索引,那么记录锁是锁在索引上的,如果表没有索引,那么 InnoDB 会创建一个隐藏的聚簇索引加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。
Gap 锁:也称为间隙锁: 锁定一个范围但不包括记录本身。其目的是为了防止同一事物的两次当前读出现幻读的情况。
Next-key Lock: 行锁+gap锁。
-
如何解决数据库死锁
-
预先检测到死锁的循环依赖,并立即返回一个错误。
-
当查询的时间达到锁等待超时的设定后放弃锁请求。
-
简述乐观锁和悲观锁
乐观锁:对于数据冲突保持一种乐观态度,操作数据时不会对操作的数据进行加锁,只有到数据提交的时候才通过一种机制来验证数据是否存在冲突。
悲观锁:数据冲突保持一种悲观态度,修改数据之前把数据锁住,然后再对数据进行读写,在释放锁之前任何人都不能对其数据进行操作,直到前面一个人把锁释放后下一个人数据加锁才可对数据进行加锁,然后才可以对数据进行操作,一般数据库本身锁的机制都是基于悲观锁的机制实现的。
- 简述InnoDB存储引擎
InnoDB 是 MySQL 的默认事务型引擎,支持事务,表是基于聚簇索引建立的。支持表级锁和行级锁, 支持外键,适合数据增删改查都频繁的情况。 InnoDB 采用 MVCC 来支持高并发,并且实现了四个标准的隔离级别。其默认级别是 REPEATABLE READ,并通过间隙锁策略防止幻读,间隙锁使 InnoDB 不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定防止幻行的插入。
- 简述MyISAM存储引擎
MySQL5.1及之前,MyISAM 是默认存储引擎。MyISAM不支持事务,Myisam支持表级锁,不支持行级锁,表不支持外键,该存储引擎存有表的行数,count运算会更快。适合查询频繁,不适合对于增删改要求高的情况
- 简述Memory存储引擎
Memory存储引擎将所有数据都保存在内存,不需要磁盘 IO。支持哈希索引,因此查找速度极快。 Memory 表使用表级锁,因此并发写入的性能较低。
- 索引是什么?
索引是存储引擎中用于快速找到记录的一种数据结构。在关系型数据库中,索引具体是一种对数据库中 一列或多列的值进行排序的存储结构。
- 为什么引入索引?
为了提高数据查询的效率。索引对数据库查询良好的性能非常关键,当表中数据量越来越大,索引对性能的影响越重要。
- 索引的分类
功能逻辑
- 主键索引(唯一且非空)
- 唯一索引(唯一可为空)
- 普通索引(普通字段的索引)
- 全文索引(一般是varchar,char,text类型建立的,但很少用)
- 组合索引(多个字的建立的索引)
物理实现
- 聚簇索引
- 非聚簇索引
底层实现
- B树
- B+树
- Hash
- 二叉查找树
- 红黑树
- 简述B-Tree与B+树
B-Tree 是一种自平衡的多叉树。每个节点都存储关键字值。其左子节点的关键字值小于该节点关键字值,且右子节点的关键字值大于或等于该节点关键字值。
B+树也是是一种自平衡的多叉树。其基本定义与B树相同,不同点在于数据只出现在叶子节点,所有叶 子节点增加了一个链指针,方便进行范围查询。 B+树中间节点不存放数据,所以同样大小的磁盘页上可以容纳更多节点元素,访问叶子节点上关联的数据也具有更好的缓存命中率。并且数据顺序排列并且相连,所以便于区间查找和搜索。
B树每一个节点都包含key和value,查询效率不稳定。
- 简述Hash索引
哈希索引对于每一行数据计算一个哈希码,并将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。只有 Memory 引擎显式支持哈希索引。 Hash索引不支持范围查询,无法用于排序,也不支持部分索引列匹配查找。
- 简述自适应Hash索引
InnoDB对于频繁使用的某些索引值,会在内存中基于 B+索引之上再创键一个哈希索引,这也被称为自适应Hash索引。
- 简述聚集索引和稀疏索引
聚集索引按每张表的主键构建一棵B+树,数据库中的每个搜索键值都有一个索引记录,每个数据页通过双向链表连接。表数据访问更快,但表更新代价高。
稀疏索引不会为每个搜索关键字创建索引记录。搜索过程需要,我们首先按索引记录进行操作,并按顺序搜索,直到找到所需的数据为止。
- 简述辅助索引与回表查询
辅助索引是非聚集索引,叶子节点不包含记录的全部数据,包含了一个书签用来告诉InnoDB哪里可以找到与索引相对应的行数据。 通过辅助索引查询,先通过书签查到聚集索引,再根据聚集索引查对应的值,需要两次,也称为回表查询。
- 简述联合索引和最左匹配原则
联合索引是指对表上的多个列的关键词进行索引。
对于联合索引的查询,如果精确匹配联合索引的左边连续一列或者多列,则mysql会一直向右匹配直到遇到范围查询(>,<,between,like)就停止匹配。Mysql会对第一个索引字段数据进行排序,在第一个字 段基础上,再对第二个字段排序。
- 简述覆盖索引
覆盖索引指一个索引包含或覆盖了所有需要查询的字段的值,不需要回表查询,即索引本身存了对应的值。
- 简述MySQL使用EXPLAIN 的关键字段
explain关键字用于分析sql语句的执行情况,可以通过他进行sql语句的性能分析。
type:表示连接类型,从好到差的类型排序为
system:系统表,数据已经加载到内存里。
const:常量连接,通过索引一次就找到。
eq_ref:唯一性索引扫描,返回所有匹配某个单独值的行。
ref:非主键非唯一索引等值扫描,const或eq_ref改为普通非唯一索引。
range:范围扫描,在索引上扫码特定范围内的值。
index:索引树扫描,扫描索引上的全部数据。
all:全表扫描。
key:显示MySQL实际决定使用的键。
key_len:显示MySQL决定使用的键长度,长度越短越好
Extra:额外信息
Using filesort:MySQL使用外部的索引排序,很慢需要优化。
Using temporary:使用了临时表保存中间结果,很慢需要优化。
Using index:使用了覆盖索引。
Using where:使用了where。
-
简述MySQL优化流程
-
通过慢日志定位执行较慢的SQL语句
-
利用explain对这些关键字段进行分析
-
根据分析结果进行优化
-
简述MySQL中的日志log
redo log: 存储引擎级别的log(InnoDB有,MyISAM没有),该log关注于事务的恢复.在重启mysql服务的时候,根据redo log进行重做,从而使事务有持久性。
undo log:是存储引擎级别的log(InnoDB有,MyISAM没有)保证数据的原子性,该log保存了事务发生之前的数据的一个版本,可以用于回滚,是MVCC的重要实现方法之一。
bin log:数据库级别的log,关注恢复数据库的数据。
- 简述事务
事务就是现实中抽象出来一种逻辑操作,要么都执行,要么都不执行,不能存在部分执行的情况。
事务满足如下几个特性:
原子性(Atomicity): 一个事务中的所有操作要么全部完成,要么全部不完成。
一致性(Consistency): 事务执行前后数据库的状态保存一致。
隔离性(Isolation) 多个并发事务对数据库进行操作,事务间互不干扰。
持久性(Durability) 事务执行完毕,对数据的修改是永久的,即使系统故障也不会丢失
- 数据库中多个事务同时进行可能会出现什么问题?
丢失修改
脏读:当前事务可以查看到别的事务未提交的数据。
不可重读:在同一事务中,使用相同的查询语句,同一数据资源莫名改变了。
幻读:在同一事务中,使用相同的查询语句,莫名多出了一些之前不存在的数据,或莫名少了一些 原先存在的数据。
- SQL的事务隔离级别有哪些?
读未提交: 一个事务还没提交,它做的变更就能被别的事务看到。
读提交: 一个事务提交后,它做的变更才能被别的事务看到。
可重复读: 一个事务执行过程中看到的数据总是和事务启动时看到的数据是一致的。在这个级别下事务未提交,做出的变更其它事务也看不到。
串行化: 对于同一行记录进行读写会分别加读写锁,当发生读写锁冲突,后面执行的事务需等前面执行的事务完成才能继续执行。
- 什么是MVCC?
MVCC为多版本并发控制,即同一条记录在系统中存在多个版本。其存在目的是在保证数据一致性的前提下提供一种高并发的访问性能。对数据读写在不加读写锁的情况下实现互不干扰,从而实现数据库的隔离性,在事务隔离级别为读提交和可重复读中使用到。
在InnoDB中,事务在开始前会向事务系统申请一个事务ID,该ID是按申请顺序严格递增的。每行数据具有多个版本,每次事务更新数据都会生成新的数据版本,而不会直接覆盖旧的数据版本。数据的行结构中包含多个信息字段。其中实现MVCC的主要涉及最近更改该行数据的事务ID(DB_TRX_ID)和可以找到历史数据版本的指针(DB_ROLL_PTR)。InnoDB在每个事务开启瞬间会为其构造一个记录当前已经开启但未提交的事务ID的视图数组。通过比较链表中的事务ID与该行数据的值与对应的 DB_TRX_ID,并通过DB_ROLL_PTR找到历史数据的值以及对应的DB_TRX_ID来决定当前版本的数据是否应该被当前事务所见。最终实现在不加锁的情况下保证数据的一致性。
- 读提交和可重复读都基于MVCC实现,有什么区别?
在可重复读级别下,只会在事务开始前创建视图,事务中后续的查询共用一个视图。
而读提交级别下每个语句执行前都会创建新的视图。
因此对于可重复读,查询只能看到事务创建前就已经提交的数据。而对于读提交,查询能看到每个语句启动前已经提交的数据。
- InnoDB如何保证事务的原子性、持久性和一致性?
利用undo log保障原子性。该log保存了事务发生之前的数据的一个版本,可以用于回滚,从而保证事务原子性。
利用redo log保证事务的持久性,该log关注于事务的恢复在重启mysql服务的时候,根据redo log进行重做,从而使事务有持久性。
利用undo log+redo log保障一致性。事务中的执行需要redo log,如果执行失败,undo log 回滚。
- MySQL是如何保证主备一致的?
MySQL通过binlog(二进制日志)实现主备一致。
binlog记录了所有修改了数据库或可能修改数据库的语句,而不会记录select、show这种不会修改数据库的语句。备份的过程中,主库A有一个专门的线程将主库A的binlog发送给备库B进行备份。
其中binlog有三种记录格式:
-
statement:记录对数据库进行修改的语句本身,有可能会记录一些额外的相关信息。优点是binlog日志量少,IO压力小,性能较高。缺点是由于记录的信息相对较少,在不同库执行时由于上下文的环境不同可能导致主备不一致。
-
row:记录对数据库做出修改的语句所影响到的数据行以及对这些行的修改。比如当修改涉及多行数据,会把涉及的每行数据都记录到binlog。优点是能够完全的还原或者复制日志被记录时的操作。缺点是日志量占用空间较大,IO压力大,性能消耗较大。
-
mixed:混合使用上述两种模式,一般的语句使用statment方式进行保存,如果遇到一些特殊的函 数,则使用row模式进行记录。MySQL自己会判断这条SQL语句是否可能引起主备不一致,如果有 可能,就用row格式,否则就用statement格式。但是在生产环境中,一般会使用row模式。
-
redo log与binlog的区别?
-
redo log是InnoDB引擎特有的,只记录该引擎中表的修改记录。binlog是MySQL的Server层实现 的,会记录所有引擎对数据库的修改。
-
redo log是物理日志,记录的是在具体某个数据页上做了什么修改;binlog是逻辑日志,记录的是这个语句的原始逻辑。
-
redo log是循环写的,空间固定会用完;binlog是可以追加写入的,binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
-
crash-safe能力是什么?
InnoDB通过redo log保证即使数据库发生异常重启,之前提交的记录都不会丢失
- WAL技术是什么?
WAL的全称Write-Ahead Logging,先写日志,再写磁盘。事务在提交写入磁盘前,会先写到redo log里面去。如果直接写入磁盘涉及磁盘的随机I/O访问,涉及磁盘随机I/O访问是非常消耗时间的一个过程,相比之下先写入redo log,后面再找合适的时机批量刷盘能提升性能。
- 两阶段提交是什么?
为了保证binlog和redo log两份日志的逻辑一致,最终保证恢复到主备数据库的数据是一致的,采用两阶段提交的机制。
-
执行器调用存储引擎接口,存储引擎将修改更新到内存中后,将修改操作记录redo log中,此时 redo log处于prepare状态。
-
存储引擎告知执行器执行完毕,执行器生成这个操作对应的binlog,并把binlog写入磁盘。
-
执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交commit状态,更新完成。
-
只靠binlog可以支持数据库崩溃恢复吗?
不可以。 历史原因:
- InnoDB在作为MySQL的插件加入MySQL引擎家族之前,就已经是一个提供了崩溃恢复和事务支持的引擎了。InnoDB接入了MySQL后,发现既然binlog没有崩溃恢复的能力,那引入InnoDB原有的redo log来保证崩溃恢复能力。
实现原因:
-
binlog没有记录数据页修改的详细信息,不具备恢复数据页的能力。binlog记录着数据行的增删改, 但是不记录事务对数据页的改动,这样细致的改动只记录在redo log中。当一个事务做增删改时, 其实涉及到的数据页改动非常细致和复杂,包括行的字段改动以及行头部以及数据页头部的改动, 甚至b+tree会因为插入一行而发生若干次页面分裂,那么事务也会把所有这些改动记录下来到redo log中。因为数据库系统进程crash时刻,磁盘上面页面镜像可以非常混乱,其中有些页面含有一些正在运行着的事务的改动,而一些已提交的事务的改动并没有刷上磁盘。事务恢复过程可以理解为是要把没有提交的事务的页面改动都去掉,并把已经提交的事务的页面改动都加上去这样一个过程。这些信息,都是binlog中没有记录的,只记录在了存储引擎的redo log中。
-
操作写入binlog可细分为write和fsync两个过程,write指的就是指把日志写入到文件系统的page cache,并没有把数据持久化到磁盘,fsync才是将数据持久化到磁盘的操作。通过参数设置 sync_binlog为0的时候,表示每次提交事务都只write,不fsync。此时数据库崩溃可能导致部分提交的事务以及binlog日志由于没有持久化而丢失。
-
简述MySQL主从复制
MySQL提供主从复制功能,可以方便的实现数据的多处自动备份,不仅能增加数据库的安全性,还能进行读写分离,提升数据库负载性能。
主从复制流程:
- 事务完成之前,主库binlog记录这些改变,完成binlog写入过程后,主库通知存储引擎提交事务2. 从库将主库的binlog复制到对应的中继日志,即开辟一个I/O工作线程,I/O线程在主库上打开一个普通的连接,然后开始binlog dump process,将这些事件写入中继日志。从主库的binlog中读取事件,如果已经读到最新了,线程进入睡眠并等待ma主库产生新的事件。
读写分离:即只在MySQL主库上写,只在MySQL从库上读,以减少数据库压力,提高性能
2.Redis
- Nosql分类
(1)面向高性能并发读写的key-value数据库
(2)面向海量数据访问的面向文档数据库
(3)面向可拓展的分布式数据库
(4)列(Wide Column Store/Column-Family)存储
(5)图形(Graph-Oriented)存储
- 什么是Redis
Redis是一个由ANSI C语言编写,性能优秀、支持网络、可持久化的Key-Value内存的NoSQL数 据库,并提供多种语言的API。
-
单线程的redis为什么这么快?
- 纯内存操作
- 单线程操作,避免了频繁的上下文切换
- 采用了非阻塞I/O 多路复用机制
- 数据结构简单,数据操作简单,Redis不使用表,存储结构是键值对,存取时间复杂度为O(1)。
-
事务
- MULTI 标记事务块的开始。重复输入报错不会结束事务状态,依次入列,命令入列成功后会返回
QUEUED
- EXEC 在一个事务中执行所有先前放入队列的命令,然后恢复正常的连接状态。
- MULTI 标记事务块的开始。重复输入报错不会结束事务状态,依次入列,命令入列成功后会返回
使用WATCH命令时,只有当受监控的键没有被修改时,EXEC命令才会执行事务的命令(CAS)
- DISCARD清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态。
- WATCH当某个事务需要按条件执行时,就要使用这个命令将给定的键设置为受监控的。
watch
命令只能开启事务之前执行,在事务中执行watch
命令会引发错误,但不会造成失败
- UNWATCH清除所有先前为一个事务监控的键。
-
为什么事务不支持回滚?
- 认为 Redis 事务的执行时,错误通常都是编程错误造成的,这种错误通常只会出现在开发环境中,而很少会在实际的生产环境中出现,所以他认为没有必要为 Redis 开发事务回滚功能;
- 不支持事务回滚是因为这种复杂的功能和 Redis 追求的简单高效的设计主旨不符合。
-
事务的错误处理
- 语法错误 ****指命令不存在或者命令参数的个数不对。
EXEC后包括正确的命令都不会执行。Redis 2.6.5之前会忽略错误的执行。
- 运行错误 指在命令执行时出现的错误,比如使用散列类型的命令操作集合类型的键
事务里其他的命令依然会继续执行(包括出错命令之后的命令)
-
如何实现消息队列
- List 队列
List 底层的实现就是一个「链表」,在头部和尾部操作元素,时间复杂度都是 O(1)
不支持重复消费;消息丢失
- 发布/订阅模型:Pub/Sub
Redis 专门是针对「发布/订阅」这种队列模型设计的。
- 支持发布 / 订阅,支持多组生产者、消费者处理消息
- 消费者下线,数据会丢失
- 不支持数据持久化,Redis 宕机,数据也会丢失
- 消息堆积,缓冲区溢出,消费者会被强制踢下线,数据也会丢失
- 趋于成熟的队列:Stream
解决上述问题,但基于Redis本身原因,依旧有消息丢失,内存积压的问题
- 如何判断一个键是否过期
在 redisDb 结构的 expire 字典(过期字典)保存了所有键的过期时间
过期字典的键是一个指向键空间中的某个键对象的指针
过期字典的值保存了键所指向的数据库键的过期时间
过期键的判断
- 通过查询过期字典,检查下面的条件判断是否过期
- 检查给定的键是否在过期字典中,如果存在就获取键的过期时间
- 检查当前 UNIX 时间戳是否大于键的过期时间,是就过期,否则未过期
- 为什么不采用定时删除
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.
- 定期删除+惰性删除是如何工作的
定期删除,redis每一段时间就检查是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查。如果过期键的比例超过 25% ,重复步骤。为了保证扫描不会出现循环过度,导致线程卡死现象,还增加了扫描时间的上限
惰性删除。在获取某个key的时候,redis会expireIfNeeded 方法对键做过期检查,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
- 主从服务器对过期键的策略
主服务器在删除一个过期键后,会显示地向所有从服务器发送一个 del 命令,告知从服务器删除这个过期键
从服务器收到在执行客户端发送的读命令时,即使碰到过期键也不会将其删除,只有在收到主服务器的 del 命令后,才会删除,这样就能保证主从服务器的数据一致性
- 内存淘汰机制
1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
4)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
5)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
6)volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。
- 什么是RDB持久化
RDB 持久化(也称作快照持久化)是指将内存中的数据生成快照保存到磁盘里面,适合数据的容灾备份与恢复,恢复数据库耗时较短
-
RDB手动触发保存
- SAVE 命令
SAVE 是一个同步式的命令,它会阻塞 Redis 服务器进程,直到 RDB 文件创建完成为止。在服务器进程阻塞期间,服务器不能处理任何其他命令请求。
- BGSAVE 命令
异步式命令,派生出一个子进程,由子进程负责创建 RDB 文件,服务器进程处理客户的命令。
- 自动触发保存
服务器每隔一段时间自动执行一次 BGSAVE命令
只要满足以下 3 个条件中的任意一个,BGSAVE 命令就会被自动执行:
服务器在 900 秒之内,对数据库进行了至少 1 次修改。
服务器在 300 秒之内,对数据库进行了至少 10 次修改。
服务器在 60 秒之内,对数据库进行了至少 10000 次修改。
- AOF 持久化
AOF(Append Only File)会把 Redis 服务器每次执行的写命令记录到一个日志文件中,当服务器重启时再次执行 AOF 文件中的命令来恢复数据。
- AOF 文件的写入流程
命令追加
Redis 使用单线程处理客户端命令,为避免每次有写命令就直接写入磁盘,导致磁盘 IO 成为 Redis 的性能瓶颈,Redis 先把执行的写命令追加到一个 aof_buf 缓冲区,而不是直接写入文件。
文件写入和文件同步
相关策略涉及到操作系统的 write() 函数和 fsync() 函数
appendfsync always:每执行一次命令保存一次
appendfsync no:不保存(大约30秒)
appendfsync everysec:每秒钟保存一次
- 文件重写
随着命令不断写入 AOF,文件会越来越大,导致文件占用空间变大,数据恢复时间变长。为了解决这个问题,Redis 引入了重写机制来对 AOF 文件中的写命令进行合并,进一步压缩文件体积。
AOF 文件重写指的是把 Redis 进程内的数据转化为写命令,同步到新的 AOF 文件中,然后使用新的 AOF 文件覆盖旧的 AOF 文件,这个过程不对旧的 AOF 文件的进行任何读写操作。
- 5种基本数据结构
String、Hash、List、Set、ZSet
String类型的数据结构存储方式有三种int、raw、embstr。
字符串是一个字符串值并且长度大于44个字节就会使用SDS,编码为raw,小于编码为embstr
Hash对象的实现方式有两种分别是ziplist、hashtable
List使用ziplist
和linkedlist
Set的底层实现是「ht和intset」,Set是一个特殊的value为空的Hash。
ZSet的底层实现是ziplist
和skiplist
- ht的rehash
一开始有ht[0]
和ht[1]
两个对象
四个属性是分别是哈希表数组、hash表大小、计算索引值,总是等于size-1、hash表已有节点数
ht[0]最开始存储数据的,当要进行扩展或者收缩时,ht[0]的大小就决定了ht[1]的大小,ht[0]中的所有的键值对就会重新散列到ht[1]中。ht[1]大小是比当前 ht[0].used 值的二倍大的第一个 2 的整数幂
当ht[0]上的所有的键值对都rehash到ht[1]中,会重新计算所有的数组下标值,当数据迁移完后ht[0]就会被释放,然后将ht[1]改为ht[0],并新创建ht[1],为下一次的扩展和收缩做准备。
- 渐进式rehash
在渐进式rehash的过程「更新、删除、查询会在ht[0]和ht[1]中都进行」,比如更新一个值先更新ht[0],然后再更新ht[1]。
而新增操作直接就新增到ht[1]表中,ht[0]不会新增任何的数据,这样保证「ht[0]只减不增,直到最后的某一个时刻变成空表」,这样rehash操作完成。
ziplist
压缩列表不是以某种压缩算法进行压缩存储数据,它表示一组连续的内存空间的使用,节省空间
压缩列表中每一个节点表示的含义如下所示:
zlbytes
:4个字节的大小,记录压缩列表占用内存的字节数。zltail
:4个字节大小,表尾节点距离起始地址的偏移量,用于快速定位到尾节点的地址。zllen
:2个字节的大小,记录压缩列表中的节点数。entry
:表示列表中的每一个节点。zlend
:表示压缩列表的特殊结束符号'0xFF'
。
- inset
也叫做整数集合,底层为数组。查找为二等查找,用于保存整数值的数据结构类型
- Set集合中必须是64位有符号的十进制整型;
- 元素个数不能超过set-max-intset-entries配置,默认512
- skiplist
也叫做**「跳跃表」**,跳跃表是一种有序的数据结构,它通过每一个节点维持多个指向其它节点的指针,从而达到快速访问的目的。
结构中有head和tail表示指向头节点和尾节点的指针,能后快速的实现定位。level表示层数,len表示跳跃表的长度,BW表示后退指针,在从尾向前遍历的时候使用。
- 什么是缓存穿透
查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
解决办法:
布隆过滤器 : 将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
- 缓存雪崩
缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上
解决方法:
(1)要保证redis的高可用,可以使用主从+哨兵或redis cluster,避免服务器不可用;
(2)设置缓存时间:需要给redis缓存中的key值设置过期时间时,尽量不要设置同一时间,如果业务场景允许可以将缓存时间加个随机数。
- 缓存击穿
缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,大并发的请求可能会瞬间把后端DB压垮。
(1)维护一个定时任务,将快要过期的key重新设置;
(2)可以使用分布式锁,当在缓存中拿不到数据时,使用分布式锁去数据库中拿到数据后,重新设置到缓存
- bigkey
- 字符串类型:它的big体现在单个value值很大,一般认为超过10KB就是bigkey。
- 非字符串类型:哈希、列表、集合、有序集合,它们的big体现在元素个数太多。
危害
1.内存空间不均匀
2.超时阻塞
3.网络拥塞
4.过期删除
5.迁移困难
如何删除
1.字符串一般来说,对于string类型使用del命令不会产生阻塞。
2.每次删除一部分
- Bitmaps
并不是实际的数据类型,而是定义在String类型上的一个面向字节操作的集合
- GEO
基于Sorted Set实现的。该结构保存的数据形式是key-score,即一个元素对应一个分值,默认是根据分值排序的,且可以进行范围查询。
- GeoHash的编码方法
对于经度或纬度来说,GeoHash会将其编码为一个N为的二进制值,其实就是通过N次的分区得到
编码值的长度是2N,其中偶数位上依次是经度的编码值,奇数位上依次是纬度的编码值,0为偶数
- Hyperloglogs
Redis 统计集合的基数一般有HashMap,BitMap 和 HyperLogLog。前两个数据结构在集合的数量级增长时,所消耗的内存会大大增加,但是 HyperLogLog 则不会。
基本原理
n 次伯努利过程,会得到 n 个出现正面的投掷次数值 k1, k2 ... kn , 其中这里的最大值是k_max。
根据一顿数学推导,我们可以得出一个结论: 2^{k_ max} 来作为n的估计值。
- HyperLogLog 是如何模拟伯努利过程
HyperLogLog 在添加元素时,会通过Hash函数,将元素转为64位比特串。比特串中,0 代表了抛硬币落地是反面,1 代表抛硬币落地是正面,如果一个数据最终被转化了 10010000,那么从低位往高位看,这串比特串代表一次伯努利过程,首次出现 1 的位数为5,就是抛了5次才出现正面。
Redis 中 HyperLogLog 一共分了 2^14 个桶,也就是 16384 个桶。每个桶中是一个 6 bit 的数组
64 位比特串的低 14 位单独拿出,它的值就对应桶的序号,然后将剩下 50 位中第一次出现 1 的位置值设置到桶中。50位中出现1的位置值最大为50,每个桶中的 6 位数组正好可以表示该值。
密集存储结构
既然要有 2^14 个 6 bit的桶,用足够多的 uint8_t
字节去表示,此时会涉及到字节位置和桶的转换,因为字节有 8 位,而桶只需要 6 位。
桶就在该字节内,只需要进行倒转就能得到桶的值(小端到大段)
在两个字节时,分别倒转后拼接
稀疏存储结构
- ZERO : 一字节,表示连续多少个桶计数为0,前两位为标志00,后6位表示有多少个桶,最大为64。
- XZERO : 两个字节,表示连续多少个桶计数为0,前两位为标志01,后14位表示有多少个桶,最大为16384。
- VAL : 一字节,表示连续多少个桶的计数为多少,前一位为标志1,四位表示连桶内计数,所以最大表示桶的计数为32。后两位表示连续多少个桶。
Redis从稀疏存储转换到密集存储的条件是:
- 任意一个计数值从 32 变成 33,因为 VAL 指令已经无法容纳,它能表示的计数值最大为 32
- 稀疏存储占用的总字节数超过 3000 字节,这个阈值可以通过参数进行调整。
5.OS
- 什么是操作系统?
操作系统是管理计算机硬件和软件资源的计算机程序,提供一个计算机用户与计算机硬件系统之间的接口。 向上对用户程序提供接口,向下接管硬件资源。 操作系统本质上也是一个软件,作为最接近硬件的系统软件,负责处理器管理、存储器管理、设备管理、文件管理和提供用户接口。
- 操作系统有哪些分类?
批处理操作系统的特点是成批处理,用户不能干预自己作业的远行。
分时系统的特点是多路性、交互性、独占性和及时性。
实时操作系统的特点是能在严格的时间范围内对外部请求做出反应,以及具有高度可靠性。
若一个操作系统兼顾批操作和分时的功能,则称该系统为通用操作系统。 常见的通用操作系统 有:Windows、Linux、MacOS等。
- 什么是内核态和用户态?
为了避免操作系统和关键数据被用户程序破坏,将处理器的执行状态分为内核态和用户态。 内核态是操作系统管理程序执行时所处的状态,能够执行包含特权指令在内的一切指令,能够访问系统内所有的存储空间。
用户态是用户程序执行时处理器所处的状态,不能执行特权指令,只能访问用户地址空间。用户程序运行在用户态,操作系统内核运行在内核态。
- 如何实现内核态和用户态的切换?
处理器从用户态切换到内核态的方法有三种:系统调用、异常和外部中断。
-
系统调用是操作系统的最小功能单位,操作系统提供的用户接口,系统调用本身是一种软中断。 2. 异常,也叫做内中断,是由错误引起的,如文件损坏、缺页故障等。
-
外部中断,是通过两根信号线来通知处理器外设的状态变化,是硬中断。
-
并发和并行的区别
-
并发(concurrency):指宏观上看起来两个程序在同时运行,比如说在单核cpu上的多任务。但是从微观上看两个程序的指令是交织着运行的,指令之间交错执行,在单个周期内只运行了一个指 令。这种并发并不能提高计算机的性能,只能提高效率(如降低某个进程的反应时间)。
-
并行(parallelism):指严格物理意义上的同时运行,比如多核cpu,两个程序分别运行在两个核上,两者之间互不影响,单个周期内每个程序都运行了自己的指令,也就是运行了两条指令。这样说来并行的确提高了计算机的效率。所以现在的cpu都是往多核方面发展
-
什么是进程?
资源分配的基本单位,是独立运行的基本单位。经典定义就是一个执行中程序的实例。
系统中的每个程序都运行在某个进程的上下文(context) 中。 上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
进程一般由以下的部分组成:
-
进程控制块PCB,是进程存在的唯一标志,包含进程标识符PID,进程当前状态,程序和数据地 址,进程优先级、CPU现场保护区(用于进程切换),占有的资源清单等。
-
程序段
-
数据段
-
进程的基本操作
以Unix系统举例:
-
进程的创建:fork()。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用 fork 时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的 PID。fork函数是有趣的(也常常令人迷惑), 因为它只被调用一次,却会返回两次:一次是在调用进程(父进程)中,一次是在新创建的子进程中。在父进程中,fork 返回子进程的 PID。在子进程中,fork 返 回 0。因为子进程的 PID 总是为非零,返回值就提供一个明 确的方法来分辨程序是在父进程还是在子进程中执行。 pid_t fork(void);
-
回收子进程:当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收(reaped)。当父进程回收已终止的子进程 时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程。一个进程可以通过调用 waitpid 函数来等待它的子进程终止或者停止。 pid_t waitpid(pid_t pid, int *statusp, int options); 3. 加载并运行程序:execve 函数在当前进程的上下文中加载并运行一个新程序。
int execve(const char *filename, const char *argv[], const char *envp[]);
-
进程终止: void exit(int status);
-
简述进程间通信方法
主要包括管道、系统IPC(包括消息队列、信号量、信号、共享内存等)、以及套接字 socket。
- 管道进行通信
管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。
有如下特质:
-
其本质是一个伪文件(实为内核缓冲区)
-
由两个文件描述符引用,一个表示读端,一个表示写端。
-
规定数据从管道的写端流入管道,从读端流出。
管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区实现。
管道的局限性:
-
数据自己读不能自己写。
-
数据一旦被读走,便不在管道中存在,不可反复读取。
-
由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。
-
只能在有公共祖先的进程间使用管道。
-
共享内存通信?
它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
特点:
-
共享内存是最快的一种IPC,因为进程是直接对内存进行操作来实现通信,避免了数据在用户空间和内核空间来回拷贝。
-
因为多个进程可以同时操作,所以需要进行同步处理。
-
信号量和共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。
-
信号
一个信号是一条小消息,通知进程系统中发生一个某种类型的事件。 每种信号类型都对应某种系统事件。低层的硬件异常是由内核异常处理程序处理的, 正常情况下,对用户进程而言是不可见的。
信号提供了一种机制,通知用户进程发生了这些异常。
-
发送信号:内核通过更新目的进程上下文中的某个状态,发送(递送)一个信号给目的进程。发送信号可以有如下两种原因: 内核检测到一个系统事件,比如除零错误或者子进程终止。 —个进程调用了kill 函数, 显式地要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。 2. 接收信号:当目的进程被内核强迫以某种方式对信号的发送做出反应时,它就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为信号处理程序的用户层函数捕获这个信号。
-
进程调度的时机
-
当前运行的进程运行结束。
-
当前运行的进程由于某种原因阻塞。
-
执行完系统调用等系统程序后返回用户进程。
-
在使用抢占调度的系统中,具有更高优先级的进程就绪时。
-
分时系统中,分给当前进程的时间片用完。
-
不能进行进程调度的情况
-
在中断处理程序执行时。
-
在操作系统的内核程序临界区内。
-
其它需要完全屏蔽中断的原子操作过程中。
-
进程的调度策略
-
先到先服务调度算法
-
短作业优先调度算法
-
优先级调度算法
-
时间片轮转调度算法
-
高响应比优先调度算法
-
多级反馈队列调度算法
-
进程调度策略的基本设计指标
-
CPU利用率
-
系统吞吐率,即单位时间内CPU完成的作业的数量。
-
响应时间。
-
周转时间。指作业从提交到完成的时间间隔。每个作业的角度看,完成每个作业的时间也很关键 平均周转时间 带权周转时间 平均带权周转时间
-
进程的状态与状态转换
进程在运行时有三种基本状态:就绪态、运行态和阻塞态。
- 运行(running)态:进程占有处理器正在运行的状态。进程已获得CPU,其程序正在执行。在单处理机系统中,只有一个进程处于执行状态; 在多处理机系统中,则有多个进程处于执行状态。 2.就绪(ready)态:进程具备运行条件,等待系统分配处理器以便运行的状态。 当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行,进程这时的状态称为就绪状态。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列。
3.阻塞(wait)态:又称等待态或睡眠态,进程不具备运行条件,正在等待某个时间完成的状态。 各状态之间的转换:
-
就绪→执行 处于就绪状态的进程,当进程调度程序分配了处理机后,该进程便由就绪状态转变 成执行状态。
-
执行→就绪 处于执行状态的进程在其执行过程中,因分配给它的一个时间片已用完而不得不让出处理机,于是进程从执行状态转变成就绪状态。
-
执行→阻塞 正在执行的进程因等待某事件发生而无法继续执行时,从执行状态变成阻塞状态。 4. 阻塞→就绪 阻塞状态的进程,若其等待的事件已经发生,于是进程由阻塞状态转变为就绪状态
-
什么是孤儿进程?僵尸进程?
-
孤儿进程: 父进程退出,子进程还在运行的这些子进程都是孤儿进程,孤儿进程将被init进程(1号进程)所收养,并由init进程对他们完成状态收集工作。
-
僵尸进程: 进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait 或waitpid 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中的这些进程是僵尸进程。
-
什么是线程?
-
是进程划分的任务,是一个进程内可调度的实体,是CPU调度的基本单位,用于保证程序的实时 性,实现进程内部的并发。
-
每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。
-
每个线程完成不同的任务,但是属于同一个进程的不同线程之间共享同一地址空间(同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源。
-
为什么需要线程?
线程产生的原因:进程可以使多个程序能并发执行,以提高资源的利用率和系统的吞吐量;但是其具有一些缺点:
-
进程在同一时刻只能做一个任务,很多时候不能充分利用CPU资源。
-
进程在执行的过程中如果发生阻塞,整个进程就会挂起,即使进程中其它任务不依赖于等待的资 源,进程仍会被阻塞。 引入线程就是为了解决以上进程的不足,
线程具有以下的优点:
-
从资源上来讲,开辟一个线程所需要的资源要远小于一个进程。
-
从切换效率上来讲,运行于一个进程中的多个线程,它们之间使用相同的地址空间,而且线程间彼此切换所需时间远远小于进程间切换所需的时间(时间的差异主要由缓存的大量未命中导致)。 3. 从通信机制上来讲,线程间方便的通信机制。对不同进程来说,它们具有独立的地址空间,要进行 数据的传递只能通过进程间通信的方式进行。线程则不然,属于同一个进程的不同线程之间共享同 一地址空间,所以一个线程的数据可以被其它线程感知,线程间可以直接读写进程数据段(如全局 变量)来进行通信(需要一些同步措施)。
-
线程和进程的区别和联系
-
一个线程只属于一个进程,而一个进程有多个线程,至少有一个线程。线程依赖进程而存在。
-
进程在执行过程中拥有独立的地址空间,而多个线程共享进程的地址空间。(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。)
-
进程是资源分配的最小单位,线程是CPU调度的最小单位。
-
通信:由于同一进程中的多个线程具有相同的地址空间,使它们之间的同步和通信的实现,也变得比较容易。进程间通信 IPC ,线程间可以直接读写进程数据段(如全局变量)来进行通信(需要一 些同步方法,以保证数据的一致性)。
-
进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂。
-
进程间不会相互影响;一个进程内某个线程挂掉将导致整个进程挂掉。
-
进程适应于多核、多机分布;线程适用于多核。
-
多线程模型
-
多对一模型。多个用户级线程映射到一个内核级线程上。该模型下,线程在用户空间进行管理,效率较高。缺点就是一个线程阻塞,整个进程内的所有线程都会阻塞。几乎没有系统使用该模型。
-
一对一模型。将内核线程与用户线程一一对应。优点是一个线程阻塞时,不会影响到其它线程的执行。该模型具有更好的并发性。缺点是内核线程数量一般有上限,会限制用户线程的数量。更多的内核线程数目也给线程切换带来额外的负担。linux和Windows操作系统家族是使用一对一模型。 3. 多对多模型。将多个用户级线程映射到多个内核级线程上。结合多对一模型和一对一模型特点。
-
进程同步的方法
① 临界区
② 同步与互斥
③ 信号量
④ 管程
- 线程同步的方式
临界区
互斥量
信号量
事件(信号)
- 进程同步与线程同步有什么区别
进程之间地址空间不同,不能感知对方的存在,同步时需要将锁放在多进程共享的空间。
而线程之间共享同一地址空间,同步时把锁放在所属的同一进程空间即可。
- 死锁是怎样产生的?
死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的下相互等待的现象。
产生死锁需要满足下面四个条件:
-
互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源。
-
占有并等待条件:进程获得一定的资源后,又对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但该进程不会释放自己已经占有的资源。
-
非抢占条件:进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放。
-
循环等待条件:进程发生死锁后,必然存在一个进程-资源之间的环形链。
-
如何解决死锁问题?
解决死锁的方法即破坏产生死锁的四个必要条件之一,主要方法如下:
-
资源一次性分配,这样就不会再有请求了(破坏请求条件)。
-
只要有一个资源得不到分配,也不给这个进程分配其他的资源(破坏占有并等待条件)。
-
可抢占资源:当进程新的资源未得到满足时,释放已占有的资源,从而破坏不可抢占的条件。
-
资源有序分配法:系统给每类资源赋予一个序号,每个进程按编号递增的请求资源,释放则相反, 从而破坏环路等待的条件
-
直接使用物理内存的问题
内存空间利用率的问题
读写内存的安全性问题
物理内存本身是不限制访问的,任何地址都可以读写,而现代操作系统需要实现不同的页面具有不同的访问权限,例如只读的数据等等
进程间的安全问题
各个进程之间没有独立的地址空间,一个进程由于执行错误指令或是恶意代码都可以直接修改其它进程的数据,甚至修改内核地址空间的数据
内存读写的效率问题
当多个进程同时运行,需要分配给进程的内存总和大于实际可用的物理内存时,需要将其他程序暂时拷贝到硬盘当中,然后将新的程序装入内存运行。由于大量的数据频繁装入装出,内存的使用效率会非常低
- 虚拟内存
虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
TLB:计算机硬件,主要用来解决引入虚拟内存之后寻址的性能问题,加速地址翻译。
- 常见的页面置换算法
先进先出(FIFO)算法:
思路:置换最先调入内存的页面,即置换在内存中驻留时间最久的页面。
实现:按照进入内存的先后次序排列成队列,从队尾进入,从队首删除。
特点:实现简单;性能较差,调出的页面可能是经常访问的
最近最少使用( LRU )算法:
思路: 置换最近一段时间以来最长时间未访问过的页面。根据程序局部性原理,刚被访问的页 面,可能马上又要被访问;而较长时间内没有被访问的页面,可能最近不会被访问。
实现:缺页时,计算内存中每个逻辑页面的上一次访问时间,选择上一次使用到当前时间最长页面 特点:可能达到最优的效果,维护这样的访问链表开销比较大 当前最常采用的就是 LRU 算法。
最不常用算法( Least Frequently Used, LFU )
思路:缺页时,置换访问次数最少的页面
实现:每个页面设置一个访问计数,访问页面时,访问计数加1,缺页时,置换计数最小的页面
特点:算法开销大,开始时频繁使用,但以后不使用的页面很难置换 \
- 写时复制
如果有多个进程读取它们共有资源的副本,那么复制是不必要的。每个进程只要保存一个指向这个资源的指针就可以了。只要没有进程要去修改自己的“副本”,就存在着这样的幻觉:每个进程好像独占那个资源。从而就避免了复制带来的负担。如果一个进程要修改自己的那份资源“副本”,那么就会复制那份资源,并把复制的那份提供给进程。不过其中的复制对进程来说是透明的。 这个进程就可以修改复制后的资源了,同时其他的进程仍然共享那份没有修改过的资源。
写时复制的主要好处在于:如果进程从来就不需要修改资源,则不需要进行复制。
惰性算法的好处就在于它们尽量推迟代价高昂的操作,直到必要的时刻才会去执行。 在使用虚拟内存的情况下,写时复制(Copy-On-Write)是以页为基础进行的。所以,只要进程不修改它全部的地址空间,那么就不必复制整个地址空间。在fork()调用结束后,父进程和子进程都相信它们有一个自己的地址空间,但实际上它们共享父进程的原始页,接下来这些页又可以被其他的父进程或子进程共享。
- 优先级反转是什么?如何解决
由于多进程共享资源,具有最高优先权的进程被低优先级进程阻塞,反而使具有中优先级的进程先于高优先级的进程执行,导致系统的崩溃。(Priority Inversion)。
其实,优先级反转是在高优级(假设为A)的任务要访问一个被低优先级任务(假设为C)占有的资源时,被阻塞.而此时又有优先级高于占有资源的任务(C)而低于被阻塞的任务(A)的优先级的任务(假设为B),于是,占有资源的任务就被挂起(占有的资源仍为它占有),因为占有资源的任务优先级很低,所以,它可能一直被另外的任务挂起.而它占有的资源也就一直不能释放,这样,引起任务A一直没办法执行.而比它优先低的任务却可以执行。
目前解决优先级反转有许多种方法。其中普遍使用的有2种方法:
-
优先级继承(priority inheritance) 优先级继承是指将低优先级任务的优先级提升到等待它所占有的资源的最高优先级任务的优先级.当高优先级任务由于等待资源而被阻塞时,此时资源的拥有者的优先级将会自动被提升。
-
优先级天花板(priority ceilings)指将申请某资源的任务的优先级提升到可能访问该资源的所有任务中最高优先级任务的优先级,然后运行(这个优先级称为该资源的优先级天花板)。
6.JAVA多线程
- 简述java内存模型(JMM)
java内存模型定义了程序中各种变量的访问规则。
其规定所有变量都存储在主内存,线程均有自己的工 作内存。 工作内存中保存被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据。操作完成后,线程的工作内存通过缓存一致性协议将操作完的数据刷回主存。
- 简述as-if-serial
编译器等会对原始的程序进行指令重排序和优化。但不管怎么重排序,其结果和用户原始程序输出预定结果一致。
- 简述happens-before八大原则
程序次序规则:一个线程内写在前面的操作先行发生于后面的。
锁定规则: unlock 操作先行发生于后面对同一个锁的 lock 操作。
volatile 规则:对 volatile 变量的写操作先行发生于后面的读操作。
线程启动规则:线程的 start 方法先行发生于线程的每个动作。
线程中断规则:线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。 线程终止规则:线程中所有操作先行发生于对线程的终止检测。
对象终结规则:对象的初始化先行发生于 finalize 方法。
传递性规则:操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,操作 A 先行发生于操作 C
- as-if-serial 和 happens-before 的区别
as-if-serial 保证单线程程序的执行结果不变
happens-before 保证正确同步的多线程程序的执行结果不变。
- 简述原子性操作
一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
- 简述线程的可见性
可见性指当一个线程修改了共享变量时,其他线程能够立即得知修改。volatile,synchronized,final都能保证可见性。 原子性操作。
- 简述有序性
虽然多线程存在并发和指令优化等操作,在本线程内观察该线程的所有执行操作是有序的。
-
简述java中volatile关键字作用
-
保证变量对所有线程的可见性。 当一条线程修改了变量值,新值对于其他线程来说是立即可以得知的。
-
禁止指令重排序优化。使用 volatile 变量进行写操作,汇编指令带有 lock 前缀,相当于一个内存屏障,编译器不会将后面的指令重排到内存屏障之前。
-
java线程的实现方式
-
实现Runnable接口
-
继承Thread类。
-
实现Callable接口
4.线程池
- 简述java线程的状态
NEW:新建状态,线程被创建且未启动,此时还未调用 start 方法。
RUNNABLE: 运行状态。其表示线程正在JVM中执行,但是这个执行,不一定真的在跑,也可能在排队等CPU。
BLOCKED:阻塞状态。线程等待获取锁,锁还没获得。
WAITING: 等待状态。线程内run方法运行完语句Object.wait()/Thread.join()进入该状态。 TIMED_WAITING:限期等待。在一定时间之后跳出状态。调用Thread.sleep(long) Object.wait(long) Thread.join(long)进入状态。其中这些参数代表等待的时间。
TERMINATED:结束状态。线程调用完run方法进入该状态。
-
简述线程通信的方式
-
volatile 关键词修饰变量,保证所有线程对变量访问的可见性。
-
synchronized关键词。确保多个线程在同一时刻只能有一个处于方法或同步块中。
-
wait/notify方法
-
IO通信
-
简述线程池
没有线程池的情况下,多次创建,销毁线程开销比较大。如果在开辟的线程执行完当前任务后执行接下来任务,复用已创建的线程,降低开销、控制最大并发数。
线程池创建线程时,会将线程封装成工作线程 Worker,Worker 在执行完任务后还会循环获取工作队列中的任务来执行。
将任务派发给线程池时,会出现以下几种情况
-
核心线程池未满,创建一个新的线程执行任务。
-
如果核心线程池已满,工作队列未满,将线程存储在工作队列。
-
如果工作队列已满,线程数小于最大线程数就创建一个新线程处理任务。
-
如果超过大小线程数,按照拒绝策略来处理任务。
-
线程池参数
-
corePoolSize:常驻核心线程数。超过该值后如果线程空闲会被销毁。
-
maximumPoolSize:线程池能够容纳同时执行的线程最大数。
-
keepAliveTime:线程空闲时间,线程空闲时间达到该值后会被销毁,直到只剩下 corePoolSize 个线程为止,避免浪费内存资源。
-
workQueue:工作队列。
-
threadFactory:线程工厂,用来生产一组相同任务的线程。
-
handler:拒绝策略。有以下几种拒绝策略:
AbortPolicy:丢弃任务并抛出异常
CallerRunsPolicy: 重新尝试提交该任务
DiscardOldestPolicy 抛弃队列里等待最久的任务并把当前任务加入队列
DiscardPolicy 表示直接抛弃当前任务但不抛出异常。
-
线程池创建方法
-
newFixedThreadPool,创建固定大小的线程池。
-
newSingleThreadExecutor,使用单线程线程池。
-
newCachedThreadPool,maximumPoolSize 设置为Integer最大值,工作完成后回收工作线程 4. newScheduledThreadPool:支持定期及周期性任务执行,不回收工作线程。
-
newWorkStealingPool:一个拥有多个任务队列的线程池。
-
简述Executor框架
Executor框架目的是将任务提交和任务如何运行分离开来的机制。用户不再需要从代码层考虑设计任务的提交运行,只需要调用Executor框架实现类的Execute方法就可以提交任务。产生线程池的函数 ThreadPoolExecutor也是Executor的具体实现类。
- 简述Executor的继承关系
Executor:一个接口,其定义了一个接收Runnable对象的方法executor,该方法接收一个Runable 实例执行这个任务。
ExecutorService:Executor的子类接口,其定义了一个接收Callable对象的方法,返回 Future 对 象,同时提供execute方法。
ScheduledExecutorService:ExecutorService的子类接口,支持定期执行任务。 AbstractExecutorService:抽象类,提供 ExecutorService 执行方法的默认实现。
Executors:实现ExecutorService接口的静态工厂类,提供了一系列工厂方法用于创建线程池。 ThreadPoolExecutor:继承AbstractExecutorService,用于创建线程池。
ForkJoinPool: 继承AbstractExecutorService,Fork 将大任务分叉为多个小任务,然后让小任务执 行,Join 是获得小任务的结果,类似于map reduce。
ThreadPoolExecutor:继承ThreadPoolExecutor,实现ScheduledExecutorService,用于创建带定时任务的线程池。
- 简述线程池的状态
Running:能接受新提交的任务,也可以处理阻塞队列的任务。
Shutdown:不再接受新提交的任务,但可以处理存量任务,线程池处于running时调用shutdown方 法,会进入该状态。
Stop:不接受新任务,不处理存量任务,调用shutdownnow进入该状态。
Tidying:所有任务已经终止了,worker_count(有效线程数)为0。
Terminated:线程池彻底终止。在tidying模式下调用terminated方法会进入该状态。
- 简述阻塞队列
阻塞队列是生产者消费者的实现具体组件之一。当阻塞队列为空时,从队列中获取元素的操作将会被阻塞,当阻塞队列满了,往队列添加元素的操作将会被阻塞。
具体实现有:
ArrayBlockingQueue:底层是由数组组成的有界阻塞队列。
LinkedBlockingQueue:底层是由链表组成的有界阻塞队列。
PriorityBlockingQueue:阻塞优先队列。
DelayQueue:创建元素时可以指定多久才能从队列中获取当前元素
SynchronousQueue:不存储元素的阻塞队列,每一个存储必须等待一个取出操作 LinkedTransferQueue:与LinkedBlockingQueue相比多一个transfer方法,即如果当前有消费者正 等待接收元素,可以把生产者传入的元素立刻传输给消费者。
LinkedBlockingDeque:双向阻塞队列。
- ThreadLocal
从名字我们可以看出ThreadLocal叫做线程局部变量,意思是ThreadLocal在每个线程中都创建了一个变量的副本,不同线程拥有的副本互不影响。
ThreadLoacl 有一个静态内部类 ThreadLocalMap,其 Key 是 ThreadLocal 对象,值是 Entry 对象,ThreadLocalMap是每个线程私有的。
set 给ThreadLocalMap设置值。
get 获取ThreadLocalMap。
remove 删除ThreadLocalMap类型的对象。
使用场景
①、在进行对象跨层传递的时候,可以避免多次传递,打破层次间的约束;
②、线程间数据隔离;
③、进行事务操作,用于存储线程事务信息;
④、数据库连接,Session会话管理。
存在的问题
-
对于线程池,由于线程池会重用 Thread 对象,因此与 Thread 绑定的 ThreadLocal 也会被重用, 造成一系列问题。
-
内存泄漏。由于 ThreadLocal 是弱引用,但 Entry 的 value 是强引用,因此当 ThreadLocal 被垃圾回收后,value 依旧不会被释放,产生内存泄漏。
- 传递数据:保存每个线程绑定的数据,在需要的地方可以直接获取,避免参数直接传递带来的代码耦合问题
- 线程隔离:各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失
public class Demo {
private static ThreadLocal<Integer> var = new ThreadLocal<>();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
var.set(20);
System.out.println(Thread.currentThread().getName() + ":设置var值为20");
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ":获取var值为" + var.get());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Thread1");
Thread t2 = new Thread(()->{
var.set(15);
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ":获取var值为" + var.get());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Thread2");
t1.start();
t2.start();
}
}
- java并发包下unsafe类的理解
对于 Java 语言,没有直接的指针组件,一般也不能使用偏移量对某块内存进行操作。这些操作相对来 讲是安全(safe)的。 Java 有个类叫 Unsafe 类,这个类类使 Java 拥有了像 C 语言的指针一样操作内存空间的能力,同时也带来了指针的问题。这个类可以说是 Java 并发开发的基础。
- JAVA中的乐观锁与CAS算法
对于乐观锁,开发者认为数据发送时发生并发冲突的概率不大,所以读操作前不上锁。 到了写操作时才会进行判断,数据在此期间是否被其他线程修改。如果发生修改,那就返回写入失败; 如果没有被修改,那就执行修改操作,返回修改成功。
乐观锁一般都采用 Compare And Swap(CAS)算法进行实现。顾名思义,该算法涉及到了两个操作, 比较(Compare)和交换(Swap)。
CAS 算法的思路如下:
-
该算法认为不同线程对变量的操作时产生竞争的情况比较少。
-
该算法的核心是对当前读取变量值 E 和内存中的变量旧值 V 进行比较。
-
如果相等,就代表其他线程没有对该变量进行修改,就将变量值更新为新值 N。
-
如果不等,就认为在读取值 E 到比较阶段,有其他线程对变量进行过修改,不进行任何操作。
-
ABA问题及解决方法简述
CAS 算法是基于值来做比较的,如果当前有两个线程,一个线程将变量值从 A 改为 B ,再由 B 改回为 A ,当前线程开始执行 CAS 算法时,就很容易认为值没有变化,误认为读取数据到执行 CAS 算法的期间,没有线程修改过数据。
juc 包提供了一个 AtomicStampedReference,即在原始的版本下加入版本号戳,解决 ABA 问题。
- 常见的Atomic类
在很多时候,我们需要的仅仅是一个简单的、高效的、线程安全的++或者--方案,使用synchronized关键字和lock固然可以实现,但代价比较大,此时用原子类更加方便。
基本数据类型的原子类有:
AtomicInteger 原子更新整形
AtomicLong 原子更新长整型
AtomicBoolean 原子更新布尔类型
Atomic数组类型有:
AtomicIntegerArray 原子更新整形数组里的元素
AtomicLongArray 原子更新长整型数组里的元素
AtomicReferenceArray 原子更新引用类型数组里的元素。
Atomic引用类型有
AtomicReference 原子更新引用类型
AtomicMarkableReference 原子更新带有标记位的引用类型,可以绑定一个 boolean 标记 AtomicStampedReference 原子更新带有版本号的引用类型
FieldUpdater类型:
AtomicIntegerFieldUpdater 原子更新整形字段的更新器
AtomicLongFieldUpdater 原子更新长整形字段的更新器
AtomicReferenceFieldUpdater 原子更新引用类型字段的更新器
- 简述Atomic类基本实现原理
以AtomicIntger 为例:
方法getAndIncrement:以原子方式将当前的值加1
具体实现为:
-
在 for 死循环中取得 AtomicInteger 里存储的数值
-
对 AtomicInteger 当前的值加 1
-
调用 compareAndSet 方法进行原子更新
-
先检查当前数值是否等于 expect
-
如果等于则说明当前值没有被其他线程修改,则将值更新为 next,
-
如果不是会更新失败返回 false,程序会进入 for 循环重新进行 compareAndSet 操作。
-
CountDownLatch
countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。 是通过一个计数器来实现的,计数器的初始值是线程的数量。只能一次性使用,不能reset。
CountDownLatch(int count); //构造方法,创建一个值为count 的计数器。
await();//阻塞当前线程,将当前线程加入阻塞队列。
await(long timeout, TimeUnit unit);//在timeout的时间之内阻塞当前线程,时间一过则当前线程可以执行,
countDown();//对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。
- 简述CyclicBarrier
CyclicBarrier 主要功能和countDownLatch类似,也是通过一个计数器,使一个线程等待其他线程各自 执行完毕后再执行。但是其可以重复使用(reset)。
- 简述Semaphore
Semaphore即信号量。 Semaphore 的构造方法参数接收一个 int 值,设置一个计数器,表示可用的许可数量即最大并发数。使 用 acquire 方法获得一个许可证,计数器减一,使用 release 方法归还许可,计数器加一。如果此时计 数器值为0,线程进入休眠。
- 简述Exchanger
Exchanger类可用于两个线程之间交换信息。可简单地将Exchanger对象理解为一个包含两个格子的容器,通过exchanger方法可以向两个格子中填充信息。线程通过exchange 方法交换数据,第一个线程执行 exchange 方法后会阻塞等待第二个线程执行该方法。当两个线程都到达同步点时这两个线程就可以交换数据当两个格子中的均被填充时,该对象会自动将两个格子的信息交换,然后返回给线程,从而实现两个线程的信息交换。
- ConcurrentHashMap
JDK7采用锁分段技术。首先将数据分成 Segment 数据段,然后给每一个数据段配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。 get 除读到空值不需要加锁。该方法先经过一次再散列,再用这个散列值通过散列运算定位到 Segment,最后通过散列算法定位到元素。 put 须加锁,首先定位到 Segment,然后进行插入操作,第一步判断是否需要对 Segment 里的 HashEntry 数组进行扩容,第二步定位添加元素的位置,然后将其放入数组。 JDK8的改进
-
取消分段锁机制,采用CAS算法进行值的设置,如果CAS失败使用 synchronized 加锁添加元素 2. 引入红黑树结构,当槽内的元素个数超过8且 Node数组 容量大于 64 时,链表转为红黑树。
-
使用了更加优化的方式统计集合内的元素数量。
-
Synchronized底层实现原理
Java 对象底层都关联一个的 monitor,使用 synchronized 时 JVM 会根据使用环境找到对象的 monitor,根据 monitor 的状态进行加解锁的判断。如果成功加锁就成为该 monitor 的唯一持有者, monitor 在被释放前不能再被其他线程获取。
synchronized在JVM编译后会产生monitorenter 和 monitorexit 这两个字节码指令,获取和释放 monitor。这两个字节码指令都需要一个引用类型的参数指明要锁定和解锁的对象,对于同步普通方法, 锁是当前实例对象;对于静态同步方法,锁是当前类的 Class 对象;对于同步方法块,锁是 synchronized 括号里的对象。
执行 monitorenter 指令时,首先尝试获取对象锁。如果这个对象没有被锁定,或当前线程已经持有锁, 就把锁的计数器加 1,执行 monitorexit 指令时会将锁计数器减 1。一旦计数器为 0 锁随即就被释放。
- 简述java偏向锁
JDK 1.6 中提出了偏向锁的概念。该锁提出的原因是,开发者发现多数情况下锁并不存在竞争,一把锁往往是由同一个线程获得的。偏向锁并不会主动释放,这样每次偏向锁进入的时候都会判断该资源是否是偏向自己的,如果是偏向自己的则不需要进行额外的操作,直接可以进入同步操作。
其申请流程为:
-
首先需要判断对象的 Mark Word 是否属于偏向模式,如果不属于,就进入轻量级锁判断逻辑。 否则继续下一步判断;
-
判断目前请求锁的线程 ID 是否和偏向锁本身记录的线程 ID 一致。如果一致,继续下一步的判断, 如果不一致,跳转到步骤4;
-
判断是否需要重偏向。如果不用的话,直接获得偏向锁;
-
利用 CAS 算法将对象的 Mark Word 进行更改,使线程 ID 部分换成本线程 ID。如果更换成功,则重偏向完成,获得偏向锁。如果失败,则说明有多线程竞争,升级为轻量级锁。
-
简述轻量级锁
其申请流程为:
-
如果同步对象没有被锁定,虚拟机将在当前线程的栈帧中建立一个锁记录空间,存储锁对象目前 Mark Word 的拷贝。
-
虚拟机使用 CAS 尝试把对象的 Mark Word 更新为指向锁记录的指针
-
如果更新成功即代表该线程拥有了锁,锁标志位将转变为 00,表示处于轻量级锁定状态。
-
如果更新失败就意味着至少存在一条线程与当前线程竞争。虚拟机检查对象的 Mark Word 是否指向当前线程的栈帧
-
如果指向当前线程的栈帧,说明当前线程已经拥有了锁,直接进入同步块继续执行
-
如果不是则说明锁对象已经被其他线程抢占。
-
如果出现两条以上线程争用同一个锁,轻量级锁就不再有效,将膨胀为重量级锁,锁标志状态变为 10,此时Mark Word 存储的就是指向重量级锁的指针,后面等待锁的线程也必须自旋后阻塞。
-
锁粗化
锁粗化的思想就是扩大加锁范围,避免反复的加锁和解锁。
- 简述锁优化策略
即自适应自旋、锁消除、锁粗化、锁升级等策略偏。
- Lock与ReentrantLock
Lock 接是 java并发包的顶层接口。
可重入锁 ReentrantLock 是 Lock 最常见的实现,与 synchronized 一样可重入。ReentrantLock 在默认 情况下是非公平的,可以通过构造方法指定公平锁。一旦使用了公平锁,性能会下降。
- AQS AbstractQueuedSynchronizer,抽象队列同步器
AQS是将每一条请求共享资源的线程封装成一个锁队列的一个结点(Node),来实现锁的分配。 AQS是用来构建锁或其他同步组件的基础框架,它使用一个 volatile int state 变量作为共享资源,如果线程获取资源失败,则进入同步队列等待;如果获取成功就执行临界区代码,释放资源时会通知同步队列中的等待线程。
子类通过继承同步器并实现它的抽象方法getState、setState 和 compareAndSetState对同步状态进行更改。
- AQS获取独占锁/释放独占锁原理
获取:(acquire)
-
调用 tryAcquire 方法安全地获取线程同步状态,获取失败的线程会被构造同步节点并通过 addWaiter 方法加入到同步队列的尾部,在队列中自旋。
-
调用 acquireQueued 方法使得该节点以死循环的方式获取同步状态,如果获取不到则阻塞。
释放:(release)
-
调用 tryRelease 方法释放同步状态
-
调用 unparkSuccessor 方法唤醒头节点的后继节点,使后继节点重新尝试获取同步状态。
-
AQS获取共享锁/释放共享锁原理
获取锁(acquireShared)
- 调用 tryAcquireShared 方法尝试获取同步状态,返回值不小于 0 表示能获取同步状态。
释放(releaseShared)
- 释放,并唤醒后续处于等待状态的节点。