├── Java ├── JAVA核心知识点整理.pdf ├── Java开发(一轮复习).md └── v3.0-JavaGuide面试突击版.pdf └── README.md /Java/JAVA核心知识点整理.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pure-xiaojie/JavaInterview/fe204ee9d9b38be59c1d57847f8c3f388351c0da/Java/JAVA核心知识点整理.pdf -------------------------------------------------------------------------------- /Java/Java开发(一轮复习).md: -------------------------------------------------------------------------------- 1 | # Java开发一轮复习 2 | 3 | ## **一、网络篇** 4 | 5 | 优秀博文: 6 | 7 | https://zhuanlan.zhihu.com/p/60305452?utm_source=qq 8 | 9 | 10 | 11 | **1、OSI七层模型与TCP/IP 五层模型** 12 | 13 | ​ OSI七层:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层 14 | 15 | ​ TCP/IP五层:物理层、数据链路层、网络层、传输层、应用层 16 | 17 | 18 | 19 | **2、常见应用层协议和运输层、网络层协议,以及硬件如路由器之类在哪一层** 20 | 21 | ​ 应用层:HTTP、SMTP、DNS、FTP 22 | 23 | ​ 传输层:TCP 、UDP 24 | 25 | ​ 网络层:ICMP 、IP、路由器、防火墙 26 | 27 | ​ 数据链路层:网卡、网桥、交换机 28 | 29 | ​ 物理层:中继器、集线器 30 | 31 | 32 | 33 | **3、TCP与UDP区别和应用场景,基于TCP的协议有哪些,基于UDP的有哪些** 34 | 35 | | 类型 | 特点 | 性能 | 应用过场景 | 首部字节 | | 36 | | ---- | :------------------------: | :--------------------: | :--------------: | :------: | ---- | 37 | | TCP | 面向连接、可靠、字节流 | 传输效率慢、所需资源多 | 文件、邮件传输 | 20-60 | | 38 | | UDP | 无连接、不可靠、数据报文段 | 传输效率快、所需资源少 | 语音、视频、直播 | 8个字节 | | 39 | 40 | ​ **基于TCP的协议:**HTTP、FTP、SMTP 41 | 42 | ​ **基于UDP的协议:**RIP、DNS、SNMP 43 | 44 | 45 | 46 | **4、TCP可靠传输的保证,拥塞控制目的和过程** 47 | 48 | ​ **TCP通过:**应用数据分割、对数据包进行编号、校验和、流量控制、拥塞控制、ARP协议、超时重传等措施保证数据的可靠传输; 49 | 50 | ​ **拥塞控制目的:**为了防止过多的数据注入到网络中,避免网络中的路由器、链路过载 51 | 52 | ​ **拥塞控制过程:**TCP发送将维护一个拥塞窗口的状态变量,该变量随着网络拥塞程度动态变化,通过慢开始、拥塞避免等算法减少网络拥塞的发生。 53 | 54 | 55 | 56 | **5、TCP粘包现象原因和解决方法** 57 | 58 | ​ **TCP粘包是指**:发送方发送的若干包数据到接收方接收时粘成一包 59 | 60 | ​ **发送方原因:** 61 | 62 | ​ TCP默认使用Nagle算法(主要作用:减少网络中报文段的数量),而Nagle算法主要做两件事: 63 | 64 | ​ 只有上一个分组得到确认,才会发送下一个分组 65 | ​ 收集多个小分组,在一个确认到来时一起发送 66 | ​ Nagle算法造成了发送方可能会出现粘包问题 67 | 68 | ​ **接收方原因:** 69 | 70 | ​ TCP接收到数据包时,并不会马上交到应用层进行处理,或者说应用层并不会立即处理。实际上, TCP将接收到的数据包保存在接收缓存里,然后应用程序主动从缓存读取收到的分组。这样一来,如果 TCP 接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程 序就有可能读取到多个首尾相接粘到一起的包。 71 | 72 | ​ **解决粘包问题:** 73 | 74 | ​ 最本质原因在与接收对等方无法分辨消息与消息之间的边界在哪,通过使用某种方案给出边界,例如: 75 | 76 | - 发送定长包。如果每个消息的大小都是一样的,那么在接收对等方只要累计接收数据,直到数据等于一个定长的数值就将它作为一个消息。 77 | - 包尾加上\r\n标记。FTP协议正是这么做的。但问题在于如果数据正文中也含有\r\n,则会误判为消息的边界。 78 | - 包头加上包体长度。包头是定长的4个字节,说明了包体的长度。接收对等方先接收包体长度,依据包体长度来接收包体。 79 | 80 | 81 | 82 | **6、TCP三次握手过程以及每次握手后的状态改变,为什么三次? 为什么两次不行?** 83 | 84 | ​ **三次握手过程:** 85 | 86 | ​ 客户端——发送带有SYN标志的数据包——服务端 **一次握手** Client进入syn_sent状态 87 | 88 | ​ 服务端——发送带有SYN/ACK标志的数据包——客户端 **二次握手** 服务端进入syn_rcvd 89 | 90 | ​ 客户端——发送带有ACK标志的数据包——服务端 **三次握手** 连接就进入Established状态 91 | 92 | ​ **为什么三次:** 93 | 94 | ​ 主要是为了建立可靠的通信信道,保证客户端与服务端同时具备发送、接收数据的能力 95 | 96 | ​ **为什么两次不行?** 97 | 98 | ​ 1、防止已失效的请求报文又传送到了服务端,建立了多余的链接,浪费资源 99 | 100 | ​ 2、 两次握手只能保证单向连接是畅通的。(为了实现可靠数据传输, TCP 协议的通信双方, 都必须维 护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方 相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤;如果只是两次握手, 至多只 有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认) 101 | 102 | 103 | 104 | **7、TCP四次挥手过程以及状态改变,为什么四次?CLOSE-WAIT和TIME-WAIT存在的意义?如何查看TIME-WAIT状态的链接数量?为什么会TIME-WAIT过多?解决方法是怎样的?** 105 | 106 | **四次挥手过程:** 107 | 108 | ​ 客户端——发送带有FIN标志的数据包——服务端,关闭与服务端的连接 ,客户端进入FIN-WAIT-1状态 109 | 110 | ​ 服务端收到这个 FIN,它发回⼀ 个 ACK,确认序号为收到的序号加1,服务端就进入了CLOSE-WAIT状态 111 | 112 | ​ 服务端——发送⼀个FIN数据包——客户端,关闭与客户端的连接,客户端就进入FIN-WAIT-2状态 113 | 114 | ​ 客户端收到这个 FIN,发回 ACK 报⽂确认,并将确认序号设置为收到序号加1,TIME-WAIT状态 115 | 116 | **为什么四次:** 117 | 118 | ​ 因为需要确保客户端与服务端的数据能够完成传输。 119 | 120 | **CLOSE-WAIT:** 121 | 122 | ​ 这种状态的含义其实是表示在等待关闭 123 | 124 | **TIME-WAIT:** 125 | 126 | ​ 为了解决网络的丢包和网络不稳定所带来的其他问题,确保连接方能在时间范围内,关闭自己的连接 127 | 128 | **如何查看TIME-WAIT状态的链接数量?** 129 | 130 | ​ netstat -an |grep TIME_WAIT|wc -l 查看连接数等待time_wait状态连接数 131 | 132 | **为什么会TIME-WAIT过多?解决方法是怎样的?** 133 | 134 | ​ **可能原因:** 高并发短连接的TCP服务器上,当服务器处理完请求后立刻按照主动正常关闭连接 135 | 136 | ​ **解决:**负载均衡服务器;Web服务器首先关闭来自负载均衡服务器的连接 137 | 138 | 139 | 140 | **8、TCP、UDP、IP、以太网报文格式以及重要字段,报文从一端到另一端传递的过程。** 141 | 142 | ​ **TCP报文格式:** 143 | 144 | ​ ![](https://farm1.staticflickr.com/792/27194088468_4cb0141fc8_b.jpg) 145 | 146 | ​ **源端口号和目的端口号**: 147 | 148 | ​ 用于寻找发端和收端应用进程。这两个值加上ip首部源端ip地址和目的端ip地址唯一确定一个tcp连接。 149 | 150 | ​ **序号字段:** 151 | 152 | ​ 序号用来标识从T C P发端向T C P收端发送的数据字节流,它表示在这个报文段中的的第一个数据字节。如果将字节流看作在两个应用程序间的单向流动,则 T C P用序号对每个字节进行计数。序号是32 bit的无符号数,序号到达 2^32-1后又从0开始。 153 | 154 |   当建立一个新的连接时,SYN标志变1。序号字段包含由这个主机选择的该连接的初始序号ISN(Initial Sequence Number)。该主机要发送数据的第一个字节序号为这个ISN加1,因为SYN标志消耗了一个序号 155 | 156 | ​ **确认序号**: 157 | 158 | ​ 既然每个传输的字节都被计数,确认序号包含发送确认的一端所期望收到的下一个序号。因此,确认序号应当是上次已成功收到数据字节序号加 1。只有ACK标志为 1时确认序号字段才有效。发送ACK无需任何代价,因为 32 bit的确认序号字段和A C K标志一样,总是T C P首部的一部分。因此,我们看到一旦一个连接建立起来,这个字段总是被设置, ACK标志也总是被设置为1。TCP为应用层提供全双工服务。这意味数据能在两个方向上独立地进行传输。因此,连接的每一端必须保持每个方向上的传输数据序号。 159 | 160 | ​ **首都长度**: 161 | 162 | ​ 首部长度给出首部中 32 bit字的数目。需要这个值是因为任选字段的长度是可变的。这个字段占4 bit,因此T C P最多有6 0字节的首部。然而,没有任选字段,正常的长度是 2 0字节。 163 | 164 | ​ **标志字段**:在T C P首部中有 6个标志比特。它们中的多个可同时被设置为1. 165 |   URG紧急指针(u rgent pointer)有效 166 |   ACK确认序号有效。 167 |   PSH接收方应该尽快将这个报文段交给应用层。 168 |   RST重建连接。 169 |   SYN同步序号用来发起一个连接。这个标志和下一个标志将在第 1 8章介绍。 170 |   FIN发端完成发送任务。 171 | 172 | ​ **窗口大小**: 173 | 174 | ​ T C P的流量控制由连接的每一端通过声明的窗口大小来提供。窗口大小为字节数,起始于确认序号字段指明的值,这个值是接收端期望接收的字节。窗口大小是一个 16 bit字段,因而窗口大小最大为 65535字节。 175 | 176 | ​ **检验和:** 177 | 178 | ​ 检验和覆盖了整个的 T C P报文段:T C P首部和T C P数据。这是一个强制性的字段,一定是由发端计算和存储,并由收端进行验证。 179 | 180 | ​ **紧急指针**: 181 | 182 | ​ 只有当URG标志置1时紧急指针才有效。紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。 T C P的紧急方式是发送端向另一端发送紧急数据的一种方式。 183 | 184 | ​ **选项**: 185 | 186 | ​ 最常见的可选字段是最长报文大小,又称为 MSS (Maximum Segment Size)。每个连接方通常都在通信的第一个报文段(为建立连接而设置 S Y N标志的那个段)中指明这个选项。它指明本端所能接收的最大长度的报文段。 187 | 188 | 189 | 190 | ​ **UDP报文格式:** 191 | 192 | ​ ![](https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=2977233957,2967577244&fm=26&gp=0.jpg) 193 | 194 | ​ **端口号**: 195 | 196 | ​ 用来表示发送和接受进程。由于 I P层已经把I P数据报分配给T C P或U D P(根据I P首部中协议字段值),因此T C P端口号由T C P来查看,而 U D P端口号由UDP来查看。T C P端口号与UDP端口号是相互独立的。 197 | 198 | ​ **长度**: 199 | 200 | ​ UDP长度字段指的是UDP首部和UDP数据的字节长度。该字段的最小值为 8字节(发送一份0字节的UDP数据报是 O K)。 201 | 202 | ​ **检验和**: 203 | 204 | ​ UDP检验和是一个端到端的检验和。它由发送端计算,然后由接收端验证。其目的是为了发现UDP首部和数据在发送端到接收端之间发生的任何改动。 205 | 206 | 207 | 208 | ​ **IP报文格式:**普通的IP首部长为20个字节,除非含有可选项字段。 209 | 210 | ​ ![](https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=1614312792,1954581760&fm=26&gp=0.jpg) 211 | 212 | ​ **4位版本**: 213 | 214 | ​ 目前协议版本号是4,因此IP有时也称作IPV4. 215 | 216 | ​ **4位首部长度**: 217 | 218 | ​ 首部长度指的是首部占32bit字的数目,包括任何选项。由于它是一个4比特字段,因此首部长度最长为60个字节。 219 | 220 | ​ **服务类型(TOS)**: 221 | 222 | ​ 服务类型字段包括一个3bit的优先权字段(现在已经被忽略),4bit的TOS子字段和1bit未用位必须置0。4bit的TOS分别代表:最小时延,最大吞吐量,最高可靠性和最小费用。4bit中只能置其中1比特。如果所有4bit均为0,那么就意味着是一般服务。 223 | 224 | ​ **总长度**: 225 | 226 | ​ 总长度字段是指整个IP数据报的长度,以字节为单位。利用首部长度和总长度字段,就可以知道IP数据报中数据内容的起始位置和长度。由于该字段长16bit,所以IP数据报最长可达65535字节。当数据报被分片时,该字段的值也随着变化。 227 | 228 | ​ **标识字段**: 229 | 230 | ​ 标识字段唯一地标识主机发送的每一份数据报。通常每发送一份报文它的值就会加1。 231 | 232 | ​ **生存时间**: 233 | 234 | ​ TTL(time-to-live)生存时间字段设置了数据报可以经过的最多路由器数。它指定了数据报的生存时间。TTL的初始值由源主机设置(通常为 3 2或6 4),一旦经过一个处理它的路由器,它的值就减去 1。当该字段的值为 0时,数据报就被丢弃,并发送 ICMP 报文通知源主机。 235 | 236 | ​ **首部检验和**: 237 | 238 | ​ 首部检验和字段是根据 I P首部计算的检验和码。它不对首部后面的数据进行计算。 ICMP、IGMP、UDP和TCP在它们各自的首部中均含有同时覆盖首部和数据检验和码。 239 | 240 | 241 | 242 | ​ **以太网报文格式:** 243 | 244 | ![](https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=2733576797,55677727&fm=26&gp=0.jpg) 245 | 246 | ​ **目的地址和源地址:** 247 | 248 | ​ 是指网卡的硬件地址(也叫MAC 地址),长度是48 位,是在网卡出厂时固化的。 249 | 250 | ​ **数据:** 251 | 252 | ​ 以太网帧中的数据长度规定最小46 字节,最大1500 字节,ARP 和RARP 数据包的长度不够46 字节,要在后面补填充位。最大值1500 称为以太网的最大传输单元(MTU),不同的网络类型有不同的MTU,如果一个数据包从以太网路由到拨号链路上,数据包度大于拨号链路的MTU了,则需要对数据包进行分片fragmentation)。ifconfig 命令的输出中也有“MTU:1500”。注意,MTU 个概念指数据帧中有效载荷的最大长度,不包括帧首部的长度。 253 | 254 | 255 | 256 | **9、浏览器输入URL并回车的过程以及相关协议,DNS查询过程。** 257 | 258 | ​ **过程:**DNS解析、TCP连接、发送HTTP请求、服务器处理请求并返回HTTP报文、浏览器渲染、结束 259 | 260 | | 过程 | 使用的协议 | 261 | | ------------------------------------------------------------ | --------------------- | 262 | | 1、浏览器查找域名DNS的IP地址
DNS查找过程(浏览器缓存、路由器缓存、DNS缓存) | DNS:获取域名对应的ip | 263 | | 2、根据ip建立TCP连接 | TCP:与服务器建立连接 | 264 | | 3、浏览器向服务器发送HTTP请求 | HTTP:发送请求 | 265 | | 4、服务器响应HTTP响应 | HTTP | 266 | | 5、浏览器进行渲染 | | 267 | 268 | 269 | 270 | **10、HTTP1.0、1.1、2.0之间的区别** 271 | 272 | ​ **HTTP1.0:**默认使用Connection:cloose,浏览器每次请求都需要与服务器建立一个TCP连接,服务器处理完成后立即断开TCP连接(无连接),服务器不跟踪每个客户端也不记录过去的请求(无状态)。 273 | 274 | ​ **HTTP1.1:**默认使用Connection:keep-alive(长连接),避免了连接建立和释放的开销;通过Content-Length字段来判断当前请求的数据是否已经全部接受。不允许同时存在两个并行的响应。 275 | 276 | ​ **HTTP2.0:**引入二进制数据帧和流的概念,其中帧对数据进行顺序标识;因为有了序列,服务器可以并行的传输数据。 277 | 278 | ​ **http1.0和http1.1的主要区别如下:** 279 | ​ 1、缓存处理:1.1添加更多的缓存控制策略(如:Entity tag,If-Match) 280 | ​ 2、网络连接的优化:1.1支持断点续传 281 | ​ 3、错误状态码的增多:1.1新增了24个错误状态响应码,丰富的错误码更加明确各个状态 282 | ​ 4、Host头处理:支持Host头域,不在以IP为请求方标志 283 | ​ 5、长连接:减少了建立和关闭连接的消耗和延迟。 284 | 285 | ​ **http1.1和http2.0的主要区别:** 286 | ​ 1、新的传输格式:2.0使用二进制格式,1.0依然使用基于文本格式 287 | ​ 2、多路复用:连接共享,不同的request可以使用同一个连接传输(最后根据每个request上的id号组合成 正常的请求) 288 | ​ 3、header压缩:由于1.X中header带有大量的信息,并且得重复传输,2.0使用encoder来减少需要传输的 hearder大小 289 | ​ 4、服务端推送:同google的SPDUY(1.0的一种升级)一样 290 | 291 | 292 | 293 | **11、HTTP与HTTPS之间的区别,HTTPS链接建立的过程,了解对称加密算法和非对称加密算法不?** 294 | 295 | ​ **HTTP与HTTPS之间的区别:** 296 | 297 | | HTTP | HTTPS | 298 | | :----------------------------: | :--------------------------------------: | 299 | | 默认端口80 | HTTPS默认使用端口443 | 300 | | 明文传输、数据未加密、安全性差 | 传输过程ssl加密、安全性较好 | 301 | | 响应速度快、消耗资源少 | 响应速度较慢、消耗资源多、需要用到CA证书 | 302 | 303 | ​ **HTTPS链接建立的过程:** 304 | 305 | ​ 1.首先客户端先给服务器发送一个请求 306 | 307 | ​ 2.服务器发送一个SSL证书给客户端,内容包括:证书的发布机构、有效期、所有者、签名以及公钥 308 | 309 | ​ 3.客户端对发来的公钥进行真伪校验,校验为真则使用公钥对对称加密算法以及对称密钥进行加密 310 | 311 | ​ 4.服务器端使用私钥进行解密并使用对称密钥加密确认信息发送给客户端 312 | 313 | ​ 5.随后客户端和服务端就使用对称密钥进行信息传输 314 | 315 | ​ **对称加密算法:** 316 | 317 | ​ 双方持有相同的密钥,且加密速度快,典型对称加密算法:DES、AES 318 | 319 | ​ **非对称加密算法:** 320 | 321 | ​ 密钥成对出现(私钥、公钥),私钥只有自己知道,不在网络中传输;而公钥可以公开。相比对称加密速度较慢,典型的非对称加密算法有:RSA、DSA 322 | 323 | 324 | 325 | **12、HTTP请求有哪些。get和Post区别。** 326 | 327 | **HTTP请求:** 328 | 329 | | 方法 | 描述 | 330 | | :------ | ------------------------------------------------------------ | 331 | | GET | 向特定资源发送请求,查询数据,并返回实体 | 332 | | POST | 向指定资源提交数据进行处理请求,可能会导致新的资源建立、已有资源修改 | 333 | | PUT | 向服务器上传新的内容 | 334 | | HEAD | 类似GET请求,返回的响应中没有具体的内容,用于获取报头 | 335 | | DELETE | 请求服务器删除指定标识的资源 | 336 | | OPTIONS | 可以用来向服务器发送请求来测试服务器的功能性 | 337 | | TRACE | 回显服务器收到的请求,用于测试或诊断 | 338 | | CONNECT | HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器 | 339 | 340 | **get和Post区别:** 341 | 342 | | | GET | POST | 343 | | -------- | ------------------------------------------------------------ | ---------------------------------------------------------- | 344 | | 可见性 | 数据在URL中对所有人可见 | 数据不会显示在URL中 | 345 | | 安全性 | 与post相比,get的安全性较差,因为所
发送的数据是URL的一部分 | 安全,因为参数不会被保存在浏览器
历史或web服务器日志中 | 346 | | 数据长度 | 受限制,最长2kb | 无限制 | 347 | | 编码类型 | application/x-www-form-urlencoded | multipart/form-data | 348 | | 缓存 | 能被缓存 | 不能被缓存 | 349 | | | | | 350 | 351 | 352 | 353 | **13、HTTP常见响应状态码,从1xx到5xx** 354 | 355 | ​ 100:Continue --- 继续。客户端应继续其请求。 356 | 357 | ​ 200:OK --- 请求成功。一般用于GET与POST请求。 358 | 359 | ​ 301:Moved Permanently --- 永久重定向。 360 | 361 | ​ 302:Found --- 暂时重定向。 362 | 363 | ​ 400:Bad Request --- 客户端请求的语法错误,服务器无法理解。 364 | 365 | ​ 403:Forbideen --- 服务器理解请求客户端的请求,但是拒绝执行此请求。 366 | 367 | ​ 404:Not Found --- 服务器无法根据客户端的请求找到资源(网页)。 368 | 369 | ​ 500:Internal Server Error --- 服务器内部错误,无法完成请求。 370 | 371 | ​ 502:Bad Gateway --- 作为网关或者代理服务器尝试执行请求时,从远程服务器接收到了无效的响应。 372 | 373 | 374 | 375 | **14、重定向和转发区别** 376 | 377 | ​ **重定向:redirect:** 378 | 379 | ​ 地址栏发生变化 380 | 381 | ​ 重定向可以访问其他站点(服务器)的资源 382 | 383 | ​ 重定向是两次请求。不能使用request对象来共享数据 384 | 385 | ​ **转发:forward:** 386 | 387 | ​ 转发地址栏路径不变 388 | 389 | ​ 转发只能访问当前服务器下的资源 390 | 391 | ​ 转发是一次请求,可以使用request对象共享数据 392 | 393 | 394 | 395 | **15、cookie和session区别。** 396 | 397 | ​ Cookie 和 Session都是用来跟踪浏览器用户身份的会话方式,但两者有所区别: 398 | 399 | ​ Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。 400 | 401 | ​ cookie不是很安全,别人可以分析存放在本地的COOKIE并进行欺骗,考虑到安全应当使用session。 402 | 403 | ​ Cookie ⼀般⽤来保存⽤户信息,Session 的主要作⽤就是通过服务端记录⽤户的 状态 404 | 405 | 406 | 407 | **16、Http 和 Tcp 的区别,什么时候用Http,什么时候用Tcp ** 408 | 409 | **HTTP:** 410 | 411 | 除了HTTP存在于应用层之外,该协议还有5个特点。 412 | 413 | 1. HTTP的标准建立在将两台计算机视为不同的角色:**客户端和服务器**。客户端会向服务器传送不同的请求(request),而服务器会对应每个请求给出回应(response)。 414 | 415 | 2. HTTP属于**无状态协议**(Stateless)。这表示每一个请求之间是没有相关性的。在该协议的规则中服务器是不会记录任何客户端操作,每一次请求都是独立的。(记录用户浏览行为会通过其他技术实现) 416 | 417 | 3. 客户端的请求被定义在几个**动词意义**范围内。最长用到的是GET和POST,其他动词还包括DELETE, HEAD等等。 418 | 419 | 4. 服务器的回应被定义在几个**状态码**之间:5开头表示服务器错误,4开头表示客户端错误,3开头表示需要做进一步处理,2开头表示成功,1开头表示在请求被接受处理的同时提供的额外信息。 420 | 421 | 5. 不管是客户端的请求信息还是服务器的回应,双方都拥有一块头部信息(**Header**)。头部信息是自定义,其用途在于传递额外信息(浏览器信息、请求的内容类型、相应的语言)。 422 | 423 | **TCP:** 424 | 425 | 在HTTP的规范内,两台计算机的交互被视为request和response的传递。而在实际的TCP操作中,信息传递会比单纯的传递request和response要复杂。通过TCP建立的通讯往往需要计算机之间多次的交换信息才能完成一次request或response。 426 | 427 | TCP的传输数据的核心是在于将数据分为若干段并将每段数据按顺序标记。标记后的顺序可以以不同的顺序被另一方接收并集成回完整的数据。计算机对每一段数据的成功接收都会做出相应,确保所有数据的完整性。 428 | 429 | TCP在传递数据时依赖于实现定义好的几个标记(Flags)去向另一方表态传达数据和连接的状态: 430 | 431 | \* F : FIN - 结束; 结束会话 432 | \* S : SYN - 同步; 表示开始会话请求 433 | \* R : RST - 复位;中断一个连接 434 | 435 | 436 | 437 | HTTP的责任是去定义数据,在两台计算机相互传递信息时,HTTP规定了每段数据以什么形式表达才是能够被另外一台计算机理解。而TCP所要规定的是数据应该怎么传输才能稳定且高效的传递与计算机之间 438 | 439 | Application(应用层) 的协议操作成功的前提是Transport(运输层)的存在。没有运输层就没有应用层。 440 | 441 | 442 | 443 | **17、Http头部字段说一下** 444 | 445 | **Request Headers:** 446 | 447 | **Accept**:text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, image/apng, */*; q=0.8 448 | 449 | - 作用:向服务器申明客户端(浏览器)可以接受的媒体类型(MIME)的资源 450 | - 解释:浏览器可以接受 text/html、application/xhtml+xml、application/xml类型,通配符*/* 表示任意类型的数据。并且浏览器按照该顺序进行接收。( text/html —> application/xhtml+xml —> application/xml) 451 | 452 | **Accept-encoding**: gzip, deflate, br 453 | 454 | - 作用:向服务器申明客户端(浏览器)接收的编码方法,通常为压缩方法 455 | - 解释:浏览器支持采用经过 gzip,deflate 或 br 压缩过的资源 456 | 457 | **Accept-Language**: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7 458 | 459 | - 作用:向服务器申明客户端(浏览器)接收的语言 460 | - 解释:浏览器能够接受 en-US, en 和 zh-CN 三种语言,其中 en-US 的权重最高 ( q 最高为1,最低为 0),服务器优先返回 en-US 语言 461 | - 延伸:语言与字符集的区别:zh-CN 为汉语,汉语中有许多的编码:gbk2312 等 462 | 463 | **Cache-control**: max-age=0 464 | 465 | - 作用:控制浏览器的缓存,常见值为 private、no-cache、max-age、alidate,默认为 private,根据浏览器查看页面不同的方式来进行区别 466 | - 解释:浏览器在访问了该页面后,不再会访问服务器 467 | 468 | **Cookie**: 469 | 470 | - 作用:告诉服务器关于 Session 的信息,存储让服务器辨识用户身份的信息。 471 | 472 | **Refer**:https://www.baidu.com/xxxxxxxxxx 473 | 474 | - 作用:告诉服务器该页面从哪个页面链接的 475 | - 解释:该页面从 [https://www.baidu.com](https://www.baidu.com/) 中的搜索结果中点击过来的 476 | 477 | **Upgrade-insecure-requests**:1 478 | 479 | - 作用:申明浏览器支持从 http 请求自动升级为 https 请求,并且在以后发送请求的时候都使用 https 480 | - 解释:当页面中包含大量的 http 资源的时候(图片、iframe),如果服务器发现一旦存在上述的响应头的时候,会在加载 http 资源的时候自动替换为 https 请求 481 | 482 | **User-agent**:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 483 | 484 | - 作用:向服务器发送浏览器的版本、系统、应用程序的信息。 485 | - 解释:Chrome 浏览器的版本信息为 63.0.3239.132,并将自己伪装成 Safari,使用的是 WebKit 引擎,WebKit伪装成 KHTML,KHTML伪装成Gecko(伪装是为了接收那些为Mozilla、safari、gecko编写的界面) 486 | - 延伸:可以随便填(但不应该随便填)不过一般用于统计。 487 | 488 | **X-Chrome-UMA-Enabled**、**X-Client-Data** :与 Chrome 浏览器相关的数据 489 | 490 | ## **二、操作系统篇** 491 | 492 | **1、进程和线程的区别** 493 | 494 | ​ **进程:**是资源分配的最小单位,是程序的执行过程,一个进程可以有多个线程,多个线程共享进程的堆和方法区资源,但每个线程又有属于自己的本地方法栈、虚拟机栈、程序计数器 495 | 496 | ​ **线程:**是任务调度和执行的最小单位,线程间可能存在相互影响,执行开销较小,不利于资源的管理和保护,线程间是共享进程中的资源的 497 | 498 | 499 | 500 | **2、协程?** 501 | 502 | ​ 是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。 503 | 504 | 505 | 506 | **3、进程间通信方式IPC** 507 | 508 | 参考:https://www.jianshu.com/p/c1015f5ffa74 509 | 510 | **匿名管道pipe:** 511 | 512 | ​ 匿名管道是半双工的,数据只能单向通信;需要双方通信时,需要建立起两个管道;只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。 513 | 514 | **命名管道FIFO:** 515 | 516 | ​ 不同于匿名管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。值得注意的是,FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。 517 | 518 | **信号:** 519 | 520 | ​ 信号是一种比较复杂的通信方式,信号产生的条件:按键、硬件异常、进程调用kill函数将信号发送给另一个进程、用户调用kill命令将信号发送给其他进程,信号传递的消息比较少,主要用于通知接收进程某个时间已经发生。 521 | 522 | **消息队列:** 523 | 524 | ​ 消息队列是消息的链表,存放在内核中并由消息队列标识符标识,消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点。消息队列起信箱作用,到了就挂在那里,需要的时候去取。消息队列提供了一种在两个不相关进程间传递数据的简单有效的方法。与命名管道相比:消息队列的优势在于,它独立于发送和接收进程而存在,这消除了在同步命名管道的打开和关闭时可能产生的一些困难。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。而且,每个数据块被认为含有一个类型,接收进程可以独立地接收含有不同类型值的数据块。 525 | 526 | ​ **优点:** 527 | 528 | ​ A. 我们可以通过发送消息来几乎完全避免命名管道的同步和阻塞问题。 529 | 530 | ​ B. 我们可以用一些方法来提前查看紧急消息。 531 | 532 | ​ **缺点:** 533 | 534 | ​ A. 与管道一样,每个数据块有一个最大长度的限制。 535 | 536 | ​ B. 系统中所有队列所包含的全部数据块的总长度也有一个上限。 537 | 538 | **共享内存(share memory):** 539 | 540 | - 使得多个进程可以可以直接读写同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。 541 | - 为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。 542 | - 由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥。 543 | 544 | **信号量(Semaphores) :** 545 | 546 | ​ 信号量是⼀个计数器,⽤于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信⽅式主要⽤于解决与同步相关的问题并避免竞争条件。 547 | 548 | **套接字(Sockets) :** 549 | 550 | ​ 此⽅法主要⽤于在客户端和服务器之间通过⽹络进⾏通信。套接字是⽀持TCP/IP 的⽹络通信的基本操作单元,可以看做是不同主机之间的进程进⾏双向通信的端点,简单的说就是通信的两⽅的⼀种约定,⽤套接字中的相关函数来完成通信过程。 551 | 552 | 553 | 554 | **4、用户态和核心态** 555 | 556 | ​ 在计算机系统中,分两种程序:系统程序和应用程序,为了保证系统程序不被应用程序有意或无意地破坏,为计算机设置了两种状态——用户态、核心态 557 | 558 | **用户态:**只能受限的访问内存,运行所有的应用程序 559 | 560 | **核心态:**运行操作系统程序,cpu可以访问内存的所有数据,包括外围设备 561 | 562 | **为什么要有用户态和内核态:** 563 | 564 | ​ 由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络 565 | 566 | **用户态切换到内核态的3种方式:** 567 | 568 | ​ **a. 系统调用** 569 | 570 | ​ 这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如前例中fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。 571 | 572 | ​ **b. 异常** 573 | 574 | ​ 当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。 575 | 576 | ​ **c. 外围设备的中断** 577 | 578 | ​ 当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。 579 | 580 | ​ 这3种方式是系统在运行时由用户态转到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。 581 | 582 | ​ 583 | 584 | **5、操作系统分配的进程空间是怎样的?线程能共享哪些?** 585 | 586 | ​ 栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。 587 | 588 | ​ 堆区(heap)— 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。 589 | 590 | ​ 静态区(static)—存放全局变量和静态变量的存储 591 | 592 | ​ 代码区(text)—存放函数体的二进制代码。 593 | 594 | ​ **线程共享堆区、静态区** 595 | 596 | 597 | 598 | **6、操作系统内存管理方式,分页分段以及段页式的优缺点** 599 | 600 | 参考地址:https://blog.csdn.net/qq_37189082/article/details/97963763 601 | 602 | **存管理方式:**块式管理、页式管理、段式管理、段页式管理 603 | 604 | **分段管理:** 605 | 606 | ​ 在段式存储管理中,将程序的地址空间划分为若干段(segment),如代码段,数据段,堆栈段;这样每个进程有一个二维地址空间,相互独立,互不干扰。段式管理的优点是:没有内碎片(因为段大小可变,改变段大小来消除内碎片)。但段换入换出时,会产生外碎片(比如4k的段换5k的段,会产生1k的外碎片) 607 | 608 | **分页管理:** 609 | 610 | ​ 在页式存储管理中,将程序的逻辑地址划分为固定大小的页(page),而物理内存划分为同样大小的页框,程序加载时,可以将任意一页放入内存中任意一个页框,这些页框不必连续,从而实现了离散分离。页式存储管理的优点是:没有外碎片(因为页的大小固定),但会产生内碎片(一个页可能填充不满) 611 | 612 | **段页式管理:** 613 | 614 | ​ 段⻚式管理机制结合了段式管理和⻚式管理的优点。简单来说段⻚式管理机制就是把主存先分成若⼲ 段,每个段⼜分成若⼲⻚,也就是说 段⻚式管理机制 中段与段之间以及段的内部的都是离散的。 615 | 616 | 617 | 618 | **7、页面置换算法有哪些,FIFO为什么不好?如何改进?LRU思想,手写LRU** 619 | 620 | **置换算法:**先进先出FIFO、最近最久未使用LRU、最佳置换算法OPT 621 | 622 | **先进先出FIFO:** 623 | 624 | ​ 原理:把内存中驻留时间最久的页面置换算法予以淘汰 625 | 626 | ​ 优点:实现简单、直观 627 | 628 | ​ 缺点:没有考虑到实际的页面使用频率,性能差、与通常页面使用的规则不符合,实际应用较少 629 | 630 | ​ 改进:给每个页面增加一个R位,每次先从链表头开始查找,如果R置位,清除R位并且把该页面节点放 到链表结尾;如果R是0,那么就是又老又没用到,替换掉。 631 | 632 | **最近最久未使用LRU:** 633 | 634 | ​ 原理:选择最近且最久未使用的页面进行淘汰 635 | 636 | ​ 优点:考虑到了程序访问的时间局部性,有较好的性能,实际应用也比较多 637 | 638 | ​ 缺点:实现需要比较多的硬件支持,会增加一些硬件成本 639 | 640 | ​ 手写LRU: 参考 https://www.jianshu.com/p/ec1952b9d84a 641 | 642 | ```java 643 | /** 644 | * @program: Java 645 | * @description: LRU最近最久未使用置换算法,通过LinkedHashMap实现 646 | * @author: Mr.Li 647 | * @create: 2020-07-17 10:29 648 | **/ 649 | public class LRUCache { 650 | private LinkedHashMap cache; 651 | private int capacity; //容量大小 652 | 653 | /** 654 | *初始化构造函数 655 | * @param capacity 656 | */ 657 | public LRUCache(int capacity) { 658 | cache = new LinkedHashMap<>(capacity); 659 | this.capacity = capacity; 660 | } 661 | 662 | public int get(int key) { 663 | //缓存中不存在此key,直接返回 664 | if(!cache.containsKey(key)) { 665 | return -1; 666 | } 667 | 668 | int res = cache.get(key); 669 | cache.remove(key); //先从链表中删除 670 | cache.put(key,res); //再把该节点放到链表末尾处 671 | return res; 672 | } 673 | 674 | public void put(int key,int value) { 675 | if(cache.containsKey(key)) { 676 | cache.remove(key); //已经存在,在当前链表移除 677 | } 678 | if(capacity == cache.size()) { 679 | //cache已满,删除链表头位置 680 | Set keySet = cache.keySet(); 681 | Iterator iterator = keySet.iterator(); 682 | cache.remove(iterator.next()); 683 | } 684 | cache.put(key,value); //插入到链表末尾 685 | } 686 | } 687 | 688 | ``` 689 | 690 | ```java 691 | /** 692 | * @program: Java 693 | * @description: LRU最近最久未使用置换算法,通过LinkedHashMap内部removeEldestEntry方法实现 694 | * @author: Mr.Li 695 | * @create: 2020-07-17 10:59 696 | **/ 697 | class LRUCache { 698 | private Map map; 699 | private int capacity; 700 | 701 | /** 702 | *初始化构造函数 703 | * @param capacity 704 | */ 705 | public LRUCache(int capacity) { 706 | this.capacity = capacity; 707 | map = new LinkedHashMap(capacity, 0.75f, true) { 708 | @Override 709 | protected boolean removeEldestEntry(Map.Entry eldest) { 710 | return size() > capacity; // 容量大于capacity 时就删除 711 | } 712 | }; 713 | } 714 | public int get(int key) { 715 | //返回key对应的value值,若不存在,返回-1 716 | return map.getOrDefault(key, -1); 717 | } 718 | 719 | public void put(int key, int value) { 720 | map.put(key, value); 721 | } 722 | } 723 | ``` 724 | 725 | **最佳置换算法OPT:** 726 | 727 | ​ 原理:每次选择当前物理块中的页面在未来长时间不被访问的或未来不再使用的页面进行淘汰 728 | 729 | ​ 优点:具有较好的性能,可以保证获得最低的缺页率 730 | 731 | ​ 缺点:过于理想化,但是实际上无法实现(没办法预知未来的页面) 732 | 733 | 734 | 735 | **8、死锁条件,解决方式。** 736 | 737 | ​ 死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的相互等待的现象; 738 | 739 | ​ **死锁的条件:** 740 | 741 | ​ 互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源; 742 | 743 | ​ 请求与保持条件:进程获得一定的资源后,又对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但该进程不会释放自己已经占有的资源 744 | 745 | ​ 非剥夺条件:进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放 746 | 747 | ​ 循环等待条件:系统中若干进程组成环路,环路中每个进程都在等待相邻进程占用的资源 748 | 749 | ​ **解决方法:**破坏死锁的任意一条件 750 | 751 | ​ 资源一次性分配,从而剥夺请求和保持条件 752 | 753 | ​ 可剥夺资源:即当进程新的资源未得到满足时,释放已占有的资源,从而破坏不可剥夺的条件 754 | 755 | ​ 资源有序分配法:系统给每类资源赋予一个序号,每个进程按编号递增的请求资源,释放则相反,从而破坏环路等待的条件 756 | 757 | 758 | 759 | **10、有哪些磁盘调度算法?** 760 | 761 | **先来先服务** 762 | 763 | - 按照磁盘请求的顺序进行调度。 764 | - 优点是公平和简单。缺点也很明显,因为未对寻道做任何优化,使平均寻道时间可能较长。 765 | 766 | **最短寻道时间优先** 767 | 768 | - 优先调度与当前磁头所在磁道距离最近的磁道。 769 | - 虽然平均寻道时间比较低,但是不够公平。如果新到达的磁道请求总是比一个在等待的磁道请求近,那么在等待的磁道请求会一直等待下去,也就是出现饥饿现象。一般来说,两端的磁道请求更容易出现饥饿现象。 770 | 771 | **电梯算法** 772 | 773 | - 也叫`SCAN`扫描算法。电梯算法就是说读写磁头总是保持一个方向运行,直到该方向没有请求为止,然后改变运行方向。 774 | - 因为考虑了移动方向,因此所有的磁盘请求都会被满足,解决了最短寻道时间优先的饥饿问题。 775 | 776 | 777 | 778 | **11、什么是虚拟内存?** 779 | 780 | ​ 虚拟内存就是说,让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。虚拟内存使用部分加载的技术,让一个进程或者资源的某些页面加载进内存,从而能够加载更多的进程,甚至能加载比内存大的进程,这样看起来好像内存变大了,这部分内存其实包含了磁盘或者硬盘,并且就叫做虚拟内存。 781 | 782 | 783 | 784 | **12、什么是分页系统?** 785 | 786 | ​ 分页就是说,将磁盘或者硬盘分为大小固定的数据块,叫做页,然后内存也分为同样大小的块,叫做页框。当进程执行的时候,会将磁盘的页载入内存的某些页框中,并且正在执行的进程如果发生缺页中断也会发生这个过程。 787 | 788 | ​ 页和页框都是由两个部分组成的,一个是页号或者页框号,一个是偏移量。分页一般是有硬件来完成的,每个页都对应一个页框,它们的对应关系存放在一个叫做页表的数据结构中,页号作为这个页表的索引,页框号作为页表的值。操作系统负责维护这个页表。 789 | 790 | 791 | 792 | **13、分页和分段有什区别?** 793 | 794 | - 分页对程序员是透明的,但是分段需要程序员显式划分每个段。 795 | - 分页的地址空间是一维地址空间,分段是二维的。 796 | - 页的大小不可变,段的大小可以动态改变。 797 | - 分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。 798 | 799 | 800 | 801 | **14、页面替换算法有哪些?** 802 | 803 | ​ **在程序运行过程**中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。 804 | 805 | **最佳算法** 806 | 807 | ​ 所选择的被换出的页面将是最长时间内不再被访问,通常可以保证获得最低的缺页率。这是一种理论上的算法,因为无法知道一个页面多长时间不再被访问。 808 | 809 | **先进先出** 810 | 811 | ​ 选择换出的页面是最先进入的页面。该算法将那些经常被访问的页面也被换出,从而使缺页率升高。 812 | 813 | **LRU** 814 | 815 | ​ 虽然无法知道将来要使用的页面情况,但是可以知道过去使用页面的情况。`LRU` 将最近最久未使用的页面换出。为了实现 LRU,需要在内存中维护一个所有页面的链表。当一个页面被访问时,将这个页面移到链表表头。这样就能保证链表表尾的页面是最近最久未访问的。因为每次访问都需要更新链表,因此这种方式实现的 `LRU` 代价很高。 816 | 817 | **时钟算法** 818 | 819 | ​ 时钟算法使用环形链表将页面连接起来,再使用一个指针指向最老的页面。它将整个环形链表的每一个页面做一个标记,如果标记是`0`,那么暂时就不会被替换,然后时钟算法遍历整个环,遇到标记为`1`的就替换,否则将标记为`0`的标记为`1`。 820 | 821 | ## **三、Java基础篇** 822 | 823 | **1、Java面向对象特性介绍、与C++区别** 824 | 825 | **特性:**封装、继承、多态 826 | 827 | ​ **封装:**对抽象的事物抽象化成一个对象,并对其对象的属性私有化,同时提供一些能被外界访问属性的方法,这样一个对象便有存在的意义了; 828 | 829 | ​ **继承:**在已存在类的基础上,建立新类并对其增加新的数据域或功能,同时该类可以复用父类的属性与功能,这种思路可以称为继承;通过使用继承能够方便地复用旧代码,减少不必要的代码量; 830 | 831 | ​ **多态:**指程序中的某个引用变量,它所指向的具体类型以及该引用变量发出的方法调用,在编程时不能确定,要在程序运行并使用时由机器自己判别确定;实现多态的方式有两种方式,可以通过继承(多个⼦类对同⼀⽅法的重写)、也可以通过接⼝(实现接⼝并覆盖接⼝中同⼀⽅法) 832 | 833 | **Java与C++区别:** 834 | 835 | ​ 相同点:都是面向对象语言,并且都支持封装、继承、多态 836 | 837 | ​ 不同点:c++支持多继承,并且有指针的概念,由程序员自己管理内存;Java是单继承,可以用接口实现多继承,Java 不提供指针来直接访问内存,程序内存更加安全,并且Java有JVM⾃动内存管理机制,不需要程序员⼿动释放⽆⽤内存 838 | 839 | 840 | 841 | **2、多态实现原理** 842 | 843 | 参考:https://www.baidu.com/link?url=qCqeY8tJInTtjEXuotB712TJtEpKeLJ9ds1ACGEnYwMyJdjf6J0C7JvOEwbV7qNG&wd=&eqid=fe3b2106001ee6ab000000065f111b6c 844 | 845 | 多态的底层实现是动态绑定,即在运行时才把方法调用与方法实现关联起来。 846 | 847 | **静态绑定与动态绑定:** 848 | 849 | ​ JVM 的方法调用指令有五个,分别是: 850 | 851 | ​ invokestatic:调用静态方法; 852 | 853 | ​ invokespecial:调用实例构造器方法、私有方法和父类方法; 854 | 855 | ​ invokevirtual:调用虚方法; 856 | 857 | ​ invokeinterface:调用接口方法,运行时确定具体实现; 858 | 859 | ​ invokedynamic:运行时动态解析所引用的方法,然后再执行,用于支持动态类型语言。 860 | 861 | ​ invokestatic 和 invokespecial 用于静态绑定 862 | 863 | ​ invokevirtual 和 invokeinterface 用于动态绑定 864 | 865 | ​ 可以看出,动态绑定主要应用于虚方法和接口方法。 866 | 867 | ​ 虚方法的方法调用与方法实现的关联(也就是分派)有两种,一种是在编译期确定,被称为静态分派,比如方法的重载;一种是在运行时确定,被称为动态分派,比如方法的覆盖(重写)。对象方法基本上都是虚方法。 868 | 869 | **多态的实现** 870 | 871 | ​ 虚拟机栈中会存放当前方法调用的栈帧(局部变量表、操作栈、动态连接 、返回地址)。多态的实现过程,就是方法调用动态分派的过程,通过栈帧的信息去找到被调用方法的具体实现,然后使用这个具体实现的直接引用完成方法调用。 872 | 873 | 以 invokevirtual 指令为例,在执行时,大致可以分为以下几步: 874 | 875 | 1. 先从操作栈中找到对象的实际类型 class; 876 | 2. 找到 class 中与被调用方法签名相同的方法,如果有访问权限就返回这个方法的直接引用,如果没有访问权限就报错 java.lang.IllegalAccessError ; 877 | 3. 如果第 2 步找不到相符的方法,就去搜索 class 的父类,按照继承关系自下而上依次执行第 2 步的操作; 878 | 4. 如果第 3 步找不到相符的方法,就报错 java.lang.AbstractMethodError ; 879 | 880 | 可以看到,如果子类覆盖了父类的方法,则在多态调用中,动态绑定过程会首先确定实际类型是子类,从而先搜索到子类中的方法。这个过程便是方法覆盖的本质。 881 | 882 | 883 | 884 | **3、抽象类和接口区别,以及各自的使用场景** 885 | 886 | **抽象类:**包含抽象方法的类,即使用abstract修饰的类;不能使用final修饰,final修饰的类不能被继承;抽象类不能被实例化,只能被继承 887 | 888 | **接口:**接口是一个抽象类型,是抽象方法的集合,接口以interface来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法;接口只能继承接口,不能继承类,接口支持多继承;接口中的定义的成员变量,默认是public static final修饰的静态常量;接口中定义的方法,默认是public abstract修饰的抽象方法 889 | 890 | **相同点:** 891 | 892 | ​ ① 抽象类和接口都不能被实例化 893 | 894 | ​ ② 抽象类和接口都可以定义抽象方法,子类/实现类必须覆写这些抽象方法 895 | 896 | **不同点:** 897 | 898 | ​ ① 抽象类有构造方法,接口没有构造方法 899 | 900 | ​ ③抽象类可以包含普通方法,接口中只能是public abstract修饰抽象方法(Java8之后可以) 901 | 902 | ​ ③ 抽象类只能单继承,接口可以多继承 903 | 904 | ​ ④ 抽象类可以定义各种类型的成员变量,接口中只能是public static final修饰的静态常量 905 | 906 | **抽象类的使用场景:** 907 | 908 | ​ 既想约束子类具有共同的行为(但不再乎其如何实现),又想拥有缺省的方法,又能拥有实例变量 909 | 910 | **接口的应用场景:** 911 | 912 | ​ 约束多个实现类具有统一的行为,但是不在乎每个实现类如何具体实现;实现类需要具备很多不同的功能,但各个功能之间可能没有任何联系 913 | 914 | 915 | 916 | **4、泛型以及泛型擦除。List类型的list,可以加入无继承关系的B类型对象吗?如何加入?** 917 | 918 | 参考:https://blog.csdn.net/baoyinwang/article/details/107341997 919 | 920 | **泛型:** 921 | 922 | ​ 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。 923 | 924 | **泛型擦除:** 925 | 926 | ​ Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。 927 | 928 | ​ 如在代码中定义的 List和 List等类型,在编译之后都会变成 List。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的。 929 | 930 | **如何加入:** 931 | 932 | ​ 通过反射添加其它类型元素 933 | 934 | ```java 935 | public class Test { 936 | public static void main(String[] args) throws Exception { 937 | 938 | ArrayList list = new ArrayList(); 939 | 940 | list.add(new A()); //这样调用 add 方法只能存储A,因为泛型类型的实例为 A 941 | 942 | list.getClass().getMethod("add", Object.class).invoke(list, new B()); 943 | 944 | for (int i = 0; i < list.size(); i++) { 945 | System.out.println(list.get(i)); 946 | } 947 | } 948 | 949 | } 950 | ``` 951 | 952 | 953 | 954 | **5、Java异常体系** 955 | 956 | ![](https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3137389296,1222888772&fm=26&gp=0.jpg) 957 | 958 | Throwable 是 Java 语言中所有错误或异常的超类。下一层分为 Error 和 Exception 959 | 960 | **Error :** 961 | 962 | ​ 是指 java 运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。如果出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。 963 | 964 | **Exception 包含:RuntimeException 、CheckedException** 965 | 966 | **RuntimeException:** 运 行 时 异 常 967 | 968 | ​ 如 NullPointerException 、 ClassCastException ; 969 | 970 | ​ RuntimeException 是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类,这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。 971 | 972 | 973 | 974 | **CheckedException:**受检异 常 975 | 976 | ​ 如 I/O 错误导致的 IOException、SQLException; 977 | 978 | ​ CheckedException:一般是外部错误,这种异常都发生在编译阶段,Java 编译器会强制程序去捕获此类 979 | 980 | 异常,即会出现要求你把这段可能出现异常的程序进行 try catch,该类异常一般包括几个方面: 981 | 982 | ​ ①试图在文件尾部读取数据 983 | 984 | ​ ②试图打开一个错误格式的 URL 985 | 986 | ​ ③试图根据给定的字符串查找 class 对象,而这个字符串表示的类并不存在 987 | 988 | 989 | 990 | **6、反射原理以及使用场景** 991 | 992 | **Java反射:** 993 | 994 | ​ 是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为 Java 语言的反射机制。 995 | 996 | **反射原理:** 997 | 998 | ​ 反射首先是能够获取到Java中的反射类的字节码,然后将字节码中的方法,变量,构造函数等映射成 相应的 Method、Filed、Constructor 等类 999 | 1000 | ​ **如何得到Class的实例:** 1001 | 1002 | 1.类名.class(就是一份字节码) 1003 | 2.Class.forName(String className);根据一个类的全限定名来构建Class对象 1004 | 3.每一个对象多有getClass()方法:obj.getClass();返回对象的真实类型 1005 | 1006 | **使用场景:** 1007 | 1008 | ​ 逆向代码 ,例如反编译; 1009 | 1010 | ​ 动态生成类框架,如Spring:xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中; 2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的Class实例; 4)动态配置实例的属性 1011 | 1012 | 1013 | 1014 | **7、ThreadLocal原理,如何使用?** 1015 | 1016 | **ThreadLocal简介:** 1017 | 1018 | ​ 通常情况下,我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有⾃⼰的 1019 | 专属本地变量该如何解决呢? JDK中提供的 ThreadLocal 类正是为了解决这样的问题。 1020 | 1021 | **原理:** 1022 | 1023 | ​ 首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。 1024 | 1025 | ​ 最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 1026 | 1027 | ​ 我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。例如下面的 1028 | 1029 | **set 方法:** 1030 | 1031 | ```java 1032 | public void set(T value) { 1033 | Thread t = Thread.currentThread(); 1034 | ThreadLocalMap map = getMap(t); 1035 | if (map != null) 1036 | map.set(this, value); 1037 | else 1038 | createMap(t, value); 1039 | } 1040 | ``` 1041 | 1042 | **get方法:** 1043 | 1044 | ```java 1045 | public T get() { 1046 | Thread t = Thread.currentThread(); 1047 | ThreadLocalMap map = getMap(t); 1048 | if (map != null) 1049 | return (T)map.get(this); 1050 | 1051 | // 如果不存在,则创建它 1052 | T value = initialValue(); 1053 | createMap(t, value); 1054 | return value; 1055 | } 1056 | ``` 1057 | 1058 | **createMap方法:** 1059 | 1060 | ```java 1061 | void createMap(Thread t, T firstValue) { 1062 | t.threadLocals = new ThreadLocalMap(this, firstValue); 1063 | } 1064 | ``` 1065 | 1066 | **ThreadLocalMap是个静态的内部类:** 1067 | 1068 | ```java 1069 | static class ThreadLocalMap { 1070 | …… 1071 | } 1072 | ``` 1073 | 1074 | **如何使用:** 1075 | 1076 | ​ 1)存储用户Session 1077 | 1078 | ```java 1079 | private static final ThreadLocal threadSession = new ThreadLocal(); 1080 | 1081 | public static Session getSession() throws InfrastructureException { 1082 | Session s = (Session) threadSession.get(); 1083 | try { 1084 | if (s == null) { 1085 | s = getSessionFactory().openSession(); 1086 | threadSession.set(s); 1087 | } 1088 | } catch (HibernateException ex) { 1089 | throw new InfrastructureException(ex); 1090 | } 1091 | return s; 1092 | } 1093 | ``` 1094 | 1095 | ​ 2)解决线程安全的问题 1096 | 1097 | ```java 1098 | public class DateUtil { 1099 | //SimpleDateFormat不是线程安全的,所以每个线程都要有⾃⼰独⽴的副本 1100 | private static ThreadLocal format1 = new ThreadLocal() { 1101 | @Override 1102 | protected SimpleDateFormat initialValue() { 1103 | return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 1104 | } 1105 | }; 1106 | 1107 | public static String formatDate(Date date) { 1108 | return format1.get().format(date); 1109 | } 1110 | } 1111 | ``` 1112 | 1113 | 1114 | 1115 | **8、ThreadLocal内存泄漏的场景** 1116 | 1117 | ​ 实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,⽽ value 是强引⽤。弱引用的特点是,如果这个对象持有弱引用,那么在下一次垃圾回收的时候必然会被清理掉。 1118 | 1119 | ​ 所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。 假如我们不做任何措施的话,value 永远⽆法被GC 回收,这个时候就可能会产⽣内存泄露。 1120 | 1121 | ​ ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。 1122 | 1123 | ​ 因此使⽤完ThreadLocal ⽅法后,最好⼿动调⽤ remove() ⽅法。 1124 | 1125 | 1126 | 1127 | **9、static关键字和final关键字使用情况,一个类不能被继承,除了final关键字之外,还有什么方法(从构造函数考虑)?** 1128 | 1129 | **static:**可以修饰属性、方法 1130 | 1131 | ​ **static修饰属性:** 1132 | 1133 | ​ 所有对象共享一份,一个对象对其修改,其他的调用也会受到影响,类级别;随着类的加载而加载(只加载一次),先于对象的创建;可以使用类名直接调用。 1134 | 1135 | ​ **static修饰方法:** 1136 | 1137 | ​ 随着类的加载而加载;可以使用类名直接调用;静态方法中,只能调用静态的成员;非静态的方法中,可以调用静态和非静态的成员;在静态方法中,不会出现this。 1138 | 1139 | **final:**关键字主要⽤在三个地⽅:变量、⽅法、类。 1140 | 1141 | ​ **final修饰变量:** 1142 | 1143 | ​ 对于⼀个 final 变量,如果是基本数据类型的变量,则其数值⼀旦在初始化之后便不能更改;如果是引⽤类型的变量,则在对其初始化之后便不能再让其指向另⼀个对象。 1144 | 1145 | ​ **final修饰方法:** 1146 | 1147 | ​ 把⽅法锁定,以防任何继承类修改它的含义(重写);类中所有的 private ⽅法都隐式地指定为 final。 1148 | 1149 | ​ **final修饰类:** 1150 | 1151 | ​ final 修饰类时,表明这个类不能被继承。final 类中的所有成员⽅法都会被隐式地指定为 final ⽅法。 1152 | 1153 | 1154 | 1155 | **10、序列化和反序列化。反序列化失败的场景。** 1156 | 1157 | ​ 序列化的意思就是将对象的状态转化成字节流,以后可以通过这些值再生成相同状态的对象。对象序列化是对象持久化的一种实现方法,它是将对象的属性和方法转化为一种序列化的形式用于存储和传输。反序列化就是根据这些保存的信息重建对象的过程。 1158 | 1159 | **序列化:**将java对象转化为字节序列的过程。 1160 | 1161 | **反序列化:**将字节序列转化为java对象的过程。 1162 | 1163 | **优点:** 1164 | 1165 | ​ a、实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里) 1166 | 1167 | ​ b、利用序列化实现远程通信,即在网络上传送对象的字节序列。 1168 | 1169 | **反序列化失败的场景:** 1170 | 1171 | ​ 序列化ID:serialVersionUID不一致的时候,导致反序列化失败 1172 | 1173 | 1174 | 1175 | **11、ArrayList和LinkedList的区别和底层实现?如何实现线程安全?** 1176 | 1177 | **ArrayList:** 1178 | 1179 | ​ 底层基于数组实现,支持对元素进行快速随机访问,支持元素重复;默认初始大小为10,当数组容量不够时,会触发扩容机制(扩大到当前的1.5倍),需要将原来数组的数据复制到新的数组中;当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。 1180 | 1181 | **LinkedList:** 1182 | 1183 | ​ 底层基于双向链表实现,适合数据的动态插入和删除;内部提供了 List 接口中没有定义的方法,用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。 1184 | 1185 | **ArrayList与LinkedList区别:** 1186 | 1187 | ​ 都是线程不安全的,ArrayList 适用于查找的场景,LinkedList 适用于 增加、删除多的场景 1188 | 1189 | **实现线程安全:** 1190 | 1191 | ​ 可以使用原生的**Vector**,或者是**Collections.synchronizedList(List list)**函数返回一个线程安全的ArrayList集合,或者使用concurrent并发包下的**CopyOnWriteArrayList**的。 1192 | 1193 | ​ ①、**Vector:** 底层通过synchronize修饰保证线程安全,效率较差 1194 | 1195 | ​ ② 、**Collections.synchronizedList(List list):** 1196 | 1197 | ```java 1198 | //使用Collections.synchronizedList(List list)方法实现线程安全 1199 | List list=Collections.synchronizedList(new ArrayList<>()); 1200 | ``` 1201 | 1202 | ​ ③、**CopyOnWriteArrayList:**写时加锁,使用了一种叫**写时复制**的方法;读操作是可以不用加锁的 1203 | 1204 | ​ 1205 | 1206 | **12、List遍历时如何删除元素?fail—fast是什么?fail—safe是什么?** 1207 | 1208 | **①、普通for循环遍历List删除指定元素** 1209 | 1210 | ```java 1211 | for(int i=0; i < list.size(); i++){ 1212 | if(list.get(i) == 5) 1213 | list.remove(i); 1214 | } 1215 | ``` 1216 | 1217 | **② 、迭代遍历,用list.remove(i)方法删除元素** 1218 | 1219 | ```JAVA 1220 | Iterator it = list.iterator(); 1221 | while(it.hasNext()){ 1222 | Integer value = it.next(); 1223 | if(value == 5){ 1224 | list.remove(value); 1225 | } 1226 | } 1227 | ``` 1228 | 1229 | **③、foreach遍历List删除元素** 1230 | 1231 | ```JAVA 1232 | for(Integer i:list){ 1233 | if(i==3) list.remove(i); 1234 | } 1235 | ``` 1236 | 1237 | **fail—fast:**快速失败 1238 | 1239 | ​ 当异常产生时,直接抛出异常,程序终止; 1240 | 1241 | ​ fail-fast只要是体现在当我们在遍历集合元素的时候,经常会使用迭代器,但在迭代器遍历元素的过程中,如果集合的结构被改变的话,就会抛出异常ConcurrentModificationException,防止继续遍历。这就是所谓的快速失败机制。这里要注意的这里说的结构被改变,是例如插入和删除这种操作,只是改变集合里的值的话并不会抛出异常。 1242 | 1243 | **fail—safe:**安全失败 1244 | 1245 |     采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。 1246 | 1247 |     原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException。 1248 | 1249 |     缺点:基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。 1250 | 1251 |     场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。 1252 | 1253 | 1254 | 1255 | **13、详细介绍HashMap。** 1256 | 1257 | 角度:数据结构+扩容情况+put查找的详细过程+哈希函数+容量为什么始终都是2^N,JDK1.7与1.8的区别。 1258 | 1259 | 参考:https://www.jianshu.com/p/9fe4cb316c05 1260 | 1261 | **数据结构:** 1262 | 1263 | ​ HashMap在底层数据结构上采用了数组+链表+红黑树,通过散列映射来存储键值对数据 1264 | 1265 | **扩容情况:** 1266 | 1267 | ​ 默认的负载因子是0.75,表示的是,如果数组中已经存储的元素个数大于数组长度的75%,将会引发扩容操作。 1268 | 1269 | ​ 【1】创建一个长度为原来数组长度**两倍的新数组**。 1270 | 1271 | ​ 【2】重新对原数组中的Entry对象进行哈希运算,以确定他们各自在新数组中的新位置。 1272 | 1273 | **put操作步骤:** 1274 | 1275 | ​ 1、判断数组是否为空,为空进行初始化; 1276 | 1277 | ​ 2、不为空,则计算 key 的 hash 值,通过(n - 1) & hash计算应当存放在数组中的下标 index; 1278 | 1279 | ​ 3、查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在 table[index] 中; 1280 | 1281 | ​ 4、存在数据,说明发生了hash冲突(存在二个节点key的hash值一样), 继续判断key是否相等,相等,用新的value替换原数据; 1282 | 1283 | ​ 5、若不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中; 1284 | 1285 | ​ 6、若不是红黑树,创建普通Node加入链表中;判断链表长度是否大于 8,大于则将链表转换为红黑树; 1286 | 1287 | ​ 7、插入完成之后判断当前节点数是否大于阈值,若大于,则扩容为原数组的二倍 1288 | 1289 | **哈希函数:** 1290 | 1291 | ​ hash函数是先拿到 key 的hashcode,是一个32位的值,然后让hashcode的高16位和低16位进行**异或**操作。该函数也称为扰动函数,做到尽可能降低hash碰撞。 1292 | 1293 | **容量为什么始终都是2^N:** 1294 | 1295 | ​ 为了能让 HashMap 存取⾼效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上⾯也讲到了过了,Hash 值的范围值-2147483648到2147483647,前后加起来⼤概40亿的映射空间,只要哈希函数映射得⽐较均匀松散,⼀般应⽤是很难出现碰撞的。但问题是⼀个40亿⻓度的数组,内存是放不下的。所以这个散列值是不能直接拿来⽤的。⽤之前还要先做对数组的⻓度取模运算,得到的余数才能⽤来要存放的位置也就是对应的数组下标。这个数组下标的计算⽅法是“ (n - 1) & hash ”。(n代表数组⻓度)。这也就解释了 HashMap 的⻓度为什么是2的幂次⽅。 1296 | 1297 | **JDK1.7与1.8的区别:** 1298 | 1299 | J**DK1.7 HashMap:** 1300 | 1301 | ​ 底层是 **数组和链表** 结合在⼀起使⽤也就是 链表散列。HashMap 通过 key 的hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这⾥的 n 指的是数组的⻓度),如果当前位置存在元素的话,就判断该元素与要存⼊的元素的 hash值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。 1302 | 1303 | **DK1.8 HashMap:** 1304 | 1305 | ​ HashMap在底层数据结构上采用了数组+链表+红黑树,通过散列映射来存储键值对数据;当链表⻓度⼤于阈值(默认为 8),数组的⻓度大于 64时,将链表转化为红⿊树,以减少搜索时间 1306 | 1307 | 1308 | 1309 | 1310 | 1311 | **14、HashMap如何实现线程安全?ConcurrentHashMap的底层实现?JDK1.7与JDK1.8的区别** 1312 | 1313 | ​ 可以通过**ConcurrentHashMap** 和 **Hashtable**来实现线程安全;Hashtable 是原始API类,通过synchronize同步修饰,效率低下;ConcurrentHashMap 通过分段锁实现,效率较比Hashtable要好; 1314 | 1315 | **ConcurrentHashMap的底层实现:** 1316 | 1317 | ​ **JDK1.7的 ConcurrentHashMap** 底层采⽤ 分段的数组+链表 实现;采用 **分段锁**(Sagment) 对整个桶数组进⾏了分割分段(Segment),每⼀把锁只锁容器其中⼀部分数据,多线程访问容器⾥不同数据段的数据,就不会存在锁竞争,提⾼并发访问率。 1318 | 1319 | ![](https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=1035283471,1167301443&fm=26&gp=0.jpg) 1320 | 1321 | ​ **JDK1.8的 ConcurrentHashMap** 采⽤的数据结构跟HashMap1.8的结构⼀样,数组+链表/红⿊⼆叉树;摒弃了Segment的概念,⽽是直接⽤ Node 数组+链表+红⿊树的数据结构来实现,通过并发控制 **synchronized 和CAS**来操作保证线程的安全。 1322 | 1323 | 1324 | 1325 | **15、正则表达式会写吗?** 1326 | 1327 | 参考:https://blog.csdn.net/qq_39331713/article/details/82871510 1328 | 1329 | ​ 正则通过一些特定的符号与数字来表示一串字符,其中有:元字符、重复限定符、分组、转义、条件或、区间; 1330 | 1331 | 1332 | 1333 | **16、设计模式了解吗?** 1334 | 1335 | 单例模式、工厂模式、代理模式 1336 | 1337 | 1338 | 1339 | **17、linux指令知道哪些?** 1340 | 1341 | 文件管理:ls、cd、touch创建普通文件、rm删除、mkdir新建目录、mv移动、cp拷贝、chmod修改权限 1342 | 1343 | 进程管理:ps显示进程信息、kill杀死进程 1344 | 1345 | 系统管理:top、free显示系统运行信息、vmstat输出各资源使用情况 1346 | 1347 | 网络通讯:ping测试网络连通性、netstat显示网络相关信息 1348 | 1349 | 1350 | 1351 | **18、JVM相关** 1352 | 1353 | **1、JVM运行时内存划分?** 1354 | 1355 | **JVM运行时数据区域:**堆、方法区(元空间)、虚拟机栈、本地方法栈、程序计数器 1356 | 1357 | ![](https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3099290709,1184193923&fm=26&gp=0.jpg) 1358 | 1359 | **Heap(堆):** 1360 | 1361 | ​ 对象的实例以及数组的内存都是要在堆上进行分配的,堆是线程共享的一块区域,用来存放对象实例,也是垃圾回收(GC)的主要区域; 1362 | 1363 | ​ 堆细分:新生代、老年代,对于新生代又分为:Eden区和Surviver1和Surviver2区; 1364 | 1365 | **方法区:** 1366 | 1367 | ​ 对于JVM的方法区也可以称之为永久区,它储存的是已经被java虚拟机加载的类信息、常量、静态变量;Jdk1.8以后取消了方法区这个概念,称之为元空间(MetaSpace); 1368 | 1369 | **虚拟机栈:** 1370 | 1371 | ​ 虚拟机栈**是线程私有的**,他的生命周期和线程的生命周期是一致的。里面装的是一个一个的**栈帧**,每一个方法在执行的时候都会创建一个栈帧,栈帧中用来存放(**局部变量表**、**操作数栈** 、**动态链接** 、**返回地址**);在Java虚拟机规范中,对此区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出**StackOverflowError**异常;如果虚拟机栈动态扩展时无法申请到足够的内存,就会抛出**OutOfMemoryError**异常。 1372 | 1373 | - **局部变量表:**局部变量表是一组变量值存储空间,用来存放**方法参数**、方法内部定义的**局部变量**。局部变量表的容量是以变量槽(variable slot)为最小的单位。Java虚拟机没有明确规定一个slot所占的空间大小。只是导向性的说了每一个slot能存放8种基本数据类型中的一种(long 和double这种64位的需要两个slot); 1374 | 1375 | - **操作数栈:**是用来记录一个方法在执行的过程中,**字节码指令向操作数栈中进行入栈和出栈的过程**。大小在编译的时候已经确定了,当一个方法刚开始执行的时候,操作数栈中是空发的,在方法执行的过程中会有各种**字节码指令**往操作数栈中**入栈和出栈**。 1376 | 1377 | - **动态链接:**因为字节码文件中有很多符号的引用,这些符号引用一部分会在**类加载的解析阶段**或**第一次使用**的时候转化成**直接引用**,这种称为**静态解析**;另一部分会**在运行期间**转化为直接引用,称为**动态链接**。 1378 | 1379 | - **返回地址(returnAddress):**类型(指向了一条字节码指令的地址) 1380 | 1381 | 1382 | 1383 | **本地方法栈:** 1384 | 1385 | ​ 本地方法栈和虚拟机栈类似,不同的是虚拟机栈服务的是Java方法,而**本地方法栈服务的是Native方法**。在HotSpot虚拟机实现中是把本地方法栈和虚拟机栈合二为一的,同理它也会抛出**StackOverflowError**和**OOM**异常。 1386 | 1387 | **PC程序计数器:** 1388 | 1389 | ​ PC,指的是存放下一条指令的位置的这么一个区域。它是一块较小的内存空间,且是**线程私有**的。由于线程的切换,CPU在执行的过程中,一个线程执行完了,接下来CPU切换到另一个线程去执行,另外一个线程执行完再切回到之前的线程,这时需要记住原线程的下一条指令的位置,所以每一个线程都需要有自己的PC。 1390 | 1391 | 1392 | 1393 | **2、堆内存分配策略** 1394 | 1395 | ![](https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3784749479,768026745&fm=26&gp=0.jpg) 1396 | 1397 | - 对象优先分配在Eden区,如果Eden区没有足够的空间进行分配时,虚拟机执行一次MinorGC。而那些无需回收的存活对象,将会进到 Survivor 的 From 区(From 区内存不足时,直接进入 Old 区)。 1398 | 1399 | - 大对象直接进入老年代(需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。 1400 | 1401 | - 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄(Age Count)计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,直到达到阀值(默认15次),对象进入老年区。 1402 | 1403 | - 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。 1404 | 1405 | 1406 | 1407 | 1408 | **3、Full GC触发条件** 1409 | 1410 | ​ 每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小,则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC 1411 | 1412 | 1413 | 1414 | **4、如何判断对象是否存活?回收对象的两次标记过程。** 1415 | 1416 | **引用计数法:** 1417 | 1418 | ​ 给对象添加一个引用计数器,每当由一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。 1419 | 1420 | ​ 优点:实现简单,判定效率也很高 1421 | 1422 | ​ 缺点:他很难解决对象之间相互循环引用的问题。 1423 | 1424 | **对象可达性:** 1425 | 1426 | ​ 通过一系列的成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC ROOTS没有任何引用链相连时,则证明此对象时不可用的; 1427 | 1428 | **两次标记过程:** 1429 | 1430 | ​ 对象被回收之前,该对象的finalize()方法会被调用;两次标记,即第一次标记不在“关系网”中的对象。第二次的话就要先判断该对象有没有实现finalize()方法了,如果没有实现就直接判断该对象可回收;如果实现了就会先放在一个队列中,并由虚拟机建立的一个低优先级的线程去执行它,随后就会进行第二次的小规模标记,在这次被标记的对象就会真正的被回收了。 1431 | 1432 | 1433 | 1434 | **5、垃圾回收算法以及垃圾回收器介绍,尤其是G1和CMS的优缺点** 1435 | 1436 | **垃圾回收算法**:复制算法、标记清除、标记整理、分代收集 1437 | 1438 | **复制算法:** 1439 | 1440 | ​ 将内存分为⼤⼩相同的两块,每次使⽤其中的⼀块。当这⼀块的内存使⽤完后,就将还存活的对象复制到另⼀块去,然后再把使⽤的空间⼀次清理掉。这样就使每次的内存回收都是对内存区间的⼀半进⾏回收; 1441 | 1442 | ​ 优点:实现简单,内存效率高,不易产生碎片 1443 | 1444 | ​ 缺点:内存压缩了一半,倘若存活对象多,Copying 算法的效率会大大降低 1445 | 1446 | **标记清除:** 1447 | 1448 | ​ 标记出所有需要回收的对象,在标记完成后统⼀回收所有被标记的对象 1449 | 1450 | ​ 缺点:效率低,标记清除后会产⽣⼤量不连续的碎⽚,可能发生大对象不能找到可利用空间的问题。 1451 | 1452 | **标记整理:** 1453 | 1454 | ​ 标记过程仍然与“标记-清除”算法⼀样,再让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存;解决了产生大量不连续碎片问题 1455 | 1456 | **分代收集:** 1457 | 1458 | ​ 根据各个年代的特点选择合适的垃圾收集算法。 1459 | 1460 | ​ 新生代采用复制算法,新生代每次垃圾回收都要回收大部分对象,存活对象较少,即要复制的操作比较少,一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。 1461 | 1462 | ​ 老年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进⾏垃圾收集。 1463 | 1464 | 1465 | 1466 | **垃圾收集器:Serial、Parnew、parallel Scavenge、Serial old 、Parnew old、CMS、G1** 1467 | 1468 | **Serial:** 1469 | 1470 | ​ Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。 1471 | 1472 | **Parnew:** 1473 | 1474 | ​ ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。 1475 | 1476 | **parallel Scavenge:** 1477 | 1478 | ​ Parallel Scavenge收集器关注点是吞吐量(⾼效率的利⽤CPU)。CMS等垃圾收集器的关注点更多的是⽤户线程的停顿时间(提⾼⽤户体验);高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。 1479 | 1480 | **Serial old:** 1481 | 1482 | Serial收集器的⽼年代版本,它同样是⼀个单线程收集器,使用标记-整理算法。主要有两个用途: 1483 | 1484 | - 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。 1485 | 1486 | - 作为年老代中使用 CMS 收集器的后备垃圾收集方案。 1487 | 1488 | **parallel old:** 1489 | 1490 | ​ Parallel Scavenge收集器的⽼年代版本。使⽤多线程和“标记-整理”算法。 1491 | 1492 | **CMS:**重要 1493 | 1494 | ​ CMS收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段: 1495 | 1496 | ​ **初始标记:**只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。 1497 | 1498 | ​ **并发标记:**进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。 1499 | 1500 | ​ **重新标记:**为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。 1501 | 1502 | ​ **并发清除:**清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。 1503 | 1504 | ​ **优点:**并发收集、低停顿 1505 | 1506 | ​ **缺点:**对CPU资源敏感;⽆法处理浮动垃圾;使⽤“标记清除”算法,会导致⼤量空间碎⽚产⽣。 1507 | 1508 | 1509 | 1510 | **G1:**重要 1511 | 1512 | ​ 是⼀款⾯向服务器的垃圾收集器,主要针对配备多颗处理器及⼤容量内存的机器.以极⾼概率满⾜GC停顿时间要求的同时,还具备⾼吞吐量性能特征;相比与 CMS 收集器,G1 收集器两个最突出的改进是: 1513 | 1514 | ​ 【1】基于标记-整理算法,不产生内存碎片。 1515 | 1516 | ​ 【2】可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。 1517 | 1518 | ​ G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。**区域划分**和**优先级区域**回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。 1519 | 1520 | 1521 | 1522 | **6、创建一个对象的步骤** 1523 | 1524 | **步骤:类加载检查、分配内存、初始化零值、设置对象头、执行init方法** 1525 | 1526 | **①类加载检查:** 1527 | 1528 | ​ 虚拟机遇到⼀条 new 指令时,⾸先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执⾏相应的类加载过程。 1529 | 1530 | **②分配内存:** 1531 | 1532 | ​ 在类加载检查通过后,接下来虚拟机将为新⽣对象分配内存。对象所需的内存⼤⼩在类加载完成后便可确定,为对象分配空间的任务等同于把⼀块确定⼤⼩的内存从 Java 堆中划分出来。分配⽅式有 **“指针碰撞”** 和 **“空闲列表”** 两种,选择那种分配⽅式由 Java 堆是否规整决定,⽽Java堆是否规整⼜由所采⽤的垃圾收集器是否带有压缩整理功能决定。 1533 | 1534 | **③初始化零值:** 1535 | 1536 | ​ 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,这⼀步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使⽤,程序能访问到这些字段的数据类型所对应的零值。 1537 | 1538 | **④设置对象头:** 1539 | 1540 | ​ 初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运⾏状态的不同,如是否启⽤偏向锁等,对象头会有不同的设置⽅式。 1541 | 1542 | **⑤执⾏ init ⽅法:** 1543 | 1544 | ​ 在上⾯⼯作都完成之后,从虚拟机的视⻆来看,⼀个新的对象已经产⽣了,但从Java 程序的视⻆来看,对象创建才刚开始, ⽅法还没有执⾏,所有的字段都还为零。所以⼀般来说,执⾏ new 指令之后会接着执⾏ ⽅法,把对象按照程序员的意愿进⾏初始化,这样⼀个真正可⽤的对象才算完全产⽣出来。 1545 | 1546 | 1547 | 1548 | **7、详细介绍类加载过程** 1549 | 1550 | **过程:加载、验证、准备、解析、初始化** 1551 | 1552 | ![](https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2723431542,2953407468&fm=26&gp=0.jpg) 1553 | 1554 | **加载阶段:** 1555 | 1556 | ​ 1.通过一个类的全限定名来获取定义此类的二进制字节流。 1557 | 1558 | ​ 2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。 1559 | 1560 | ​ 3.在Java堆中生成一个代表这个类的java.lang.class对象,作为方法区这些数据的访问入口。 1561 | 1562 | **验证阶段:** 1563 | 1564 | ​ 1.文件格式验证(是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理) 1565 | 1566 | ​ 2.元数据验证(对字节码描述的信息进行语意分析,以保证其描述的信息符合Java语言规范要求) 1567 | 1568 | ​ 3.字节码验证(保证被校验类的方法在运行时不会做出危害虚拟机安全的行为) 1569 | 1570 | ​ 4.符号引用验证(虚拟机将符号引用转化为直接引用时,解析阶段中发生) 1571 | 1572 | **准备阶段:** 1573 | 1574 | ​ 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。将对象初始化为“零”值 1575 | 1576 | **解析阶段:** 1577 | 1578 | ​ 解析阶段时虚拟机将常量池内的符号引用替换为直接引用的过程。 1579 | 1580 | **初始化阶段:** 1581 | 1582 | ​ 初始化阶段时加载过程的最后一步,而这一阶段也是真正意义上开始执行类中定义的Java程序代码。 1583 | 1584 | 1585 | 1586 | **8、双亲委派机制,使用这个机制的好处?如何破坏?** 1587 | 1588 | ​ 每⼀个类都有⼀个对应它的类加载器。系统中的 ClassLoder 在协同⼯作的时候会默认使⽤ 双亲委派模型 。即在类加载的时候,系统会⾸先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,⾸先会把该请求委派该⽗类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当⽗类加载器⽆法处理时,才由⾃⼰来处理。当⽗类加载器为null时,会使⽤启动类加载器 BootstrapClassLoader 作为⽗类加载器。 1589 | 1590 | ![](https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1595226330267&di=5597292eb6bf5505abbec489fd54dacd&imgtype=0&src=http%3A%2F%2Fpics4.baidu.com%2Ffeed%2Fca1349540923dd54fb52494e811379d89c824890.jpeg%3Ftoken%3Dc280035219d435ec0f1c3a3a0a071101) 1591 | 1592 | **使用好处:** 1593 | 1594 | ​ 此机制保证JDK核心类的优先加载;使得Java程序的稳定运⾏,可以避免类的重复加载,也保证了 Java 的核⼼ API 不被篡改。如果不⽤没有使⽤双亲委派模型,⽽是每个类加载器加载⾃⼰的话就会出现⼀些问题,⽐如我们编写⼀个称为 java.lang.Object 类的话,那么程序运⾏的时候,系统就会出现多个不同的Object 类。 1595 | 1596 | **破坏双亲委派机制:** 1597 | 1598 | ​ 可以⾃⼰定义⼀个类加载器,重写loadClass方法; 1599 | 1600 | 1601 | 1602 | **9、了解下tomcat的类加载机制** 1603 | 1604 | **步骤:** 1605 | 1606 | 1. 先在本地cache查找该类是否已经加载过,看看 Tomcat 有没有加载过这个类。 1607 | 2. 如果Tomcat 没有加载过这个类,则从系统类加载器的cache中查找是否加载过。 1608 | 3. 如果没有加载过这个类,尝试用ExtClassLoader类加载器类加载,重点来了,这里并没有首先使用 AppClassLoader 来加载类。这个Tomcat 的 WebAPPClassLoader 违背了双亲委派机制,直接使用了 ExtClassLoader来加载类。这里注意 ExtClassLoader 双亲委派依然有效,ExtClassLoader 就会使用 Bootstrap ClassLoader 来对类进行加载,保证了 Jre 里面的核心类不会被重复加载。 比如在 Web 中加载一个 Object 类。WebAppClassLoader → ExtClassLoader → Bootstrap ClassLoader,这个加载链,就保证了 Object 不会被重复加载。 1609 | 4. 如果 BoostrapClassLoader,没有加载成功,就会调用自己的 findClass 方法由自己来对类进行加载,findClass 加载类的地址是自己本 web 应用下的 class。 1610 | 5. 加载依然失败,才使用 AppClassLoader 继续加载。 1611 | 6. 都没有加载成功的话,抛出异常。 1612 | 1613 | 总结一下以上步骤,WebAppClassLoader 加载类的时候,故意打破了JVM 双亲委派机制,绕开了 AppClassLoader,直接先使用 ExtClassLoader 来加载类。 1614 | 1615 | 1616 | 1617 | **10、JVM性能调优,常用命令,以及工具** 1618 | 1619 | 对应进程的JVM状态以定位问题和解决问题并作出相应的优化 1620 | 1621 | **常用命令:**jps、jinfo、jstat、jstack、jmap 1622 | 1623 | **jps:查看java进程及相关信息** 1624 | 1625 | ```java 1626 | jps -l 输出jar包路径,类全名 1627 | jps -m 输出main参数 1628 | jps -v 输出JVM参数 1629 | ``` 1630 | 1631 | **jinfo:查看JVM参数** 1632 | 1633 | ```java 1634 | jinfo 11666 1635 | jinfo -flags 11666 1636 | ``` 1637 | 1638 | **jstat:查看JVM运行时的状态信息,包括内存状态、垃圾回收** 1639 | 1640 | ```java 1641 | 命令格式: 1642 | jstat [option] LVMID [interval] [count] 1643 | 其中LVMID是进程id,interval是打印间隔时间(毫秒),count是打印次数(默认一直打印) 1644 | 1645 | option参数解释: 1646 | -class class loader的行为统计 1647 | -compiler HotSpt JIT编译器行为统计 1648 | -gc 垃圾回收堆的行为统计 1649 | -gccapacity 各个垃圾回收代容量(young,old,perm)和他们相应的空间统计 1650 | -gcutil 垃圾回收统计概述 1651 | -gccause 垃圾收集统计概述(同-gcutil),附加最近两次垃圾回收事件的原因 1652 | -gcnew 新生代行为统计 1653 | -gcnewcapacity 新生代与其相应的内存空间的统计 1654 | -gcold 年老代和永生代行为统计 1655 | -gcoldcapacity 年老代行为统计 1656 | -gcpermcapacity 永生代行为统计 1657 | -printcompilation HotSpot编译方法统计 1658 | ``` 1659 | 1660 | **jstack:查看JVM线程快照,jstack命令可以定位线程出现长时间卡顿的原因,例如死锁,死循环** 1661 | 1662 | ```java 1663 | 命令格式: 1664 | jstack [-l] (连接运行中的进程) 1665 | jstack -F [-m] [-l] (连接挂起的进程) 1666 | jstack [-m] [-l] (连接core文件) 1667 | jstack [-m] [-l] [server_id@] (连接远程debug服务器) 1668 | 1669 | option参数解释: 1670 | -F 当使用jstack 无响应时,强制输出线程堆栈。 1671 | -m 同时输出java和本地堆栈(混合模式) 1672 | -l 额外显示锁信息 1673 | ``` 1674 | 1675 | **jmap:可以用来查看内存信息** 1676 | 1677 | ```java 1678 | 命令格式: 1679 | jmap [option] (连接正在执行的进程) 1680 | jmap [option] (连接一个core文件) 1681 | jmap [option] [server_id@] (链接远程服务器) 1682 | 1683 | option参数解释: 1684 | to print same info as Solaris pmap 1685 | -heap 打印java heap摘要 1686 | -histo[:live] 打印堆中的 java对象统计信息 1687 | -clstats 打印类加载器统计信息 1688 | -finalizerinfo 打印在f-queue中等待执行finalizer方法的对象 1689 | -dump: 生成java堆的dump文件 1690 |       dump-options: 1691 |       live 只转储存活的对象,如果没有指定则转储所有对象 1692 |       format=b 二进制格式 1693 |       file= 转储文件到 1694 | -F 强制选项 1695 | ``` 1696 | 1697 | 1698 | 1699 | **11、JVM性能调优,常用命令,以及工具** 1700 | 1701 | **公平锁和非公平锁** 1702 | 公平锁是指多个线程在等待同一个锁时,必须按照申请锁的先后顺序来一次获得锁。 1703 | 1704 | 公平锁的好处是等待锁的线程不会饿死,但是整体效率相对低一些;非公平锁的好处是整体效率相对高一些,但是有些线程可能会饿死或者说很早就在等待锁,但要等很久才会获得锁。其中的原因是公平锁是严格按照请求所的顺序来排队获得锁的,而非公平锁时可以抢占的,即如果在某个时刻有线程需要获取锁,而这个时候刚好锁可用,那么这个线程会直接抢占,而这时阻塞在等待队列的线程则不会被唤醒。 1705 | 1706 | 公平锁可以使用new ReentrantLock(true)实现。 1707 | 1708 | **自旋锁** 1709 | Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态装换需要耗费很多的处理器时间,对于代码简单的同步块(如被synchronized修饰的getter()和setter()方法),状态转换消耗的时间有可能比用户代码执行的时间还要长。 1710 | 1711 | 虚拟机的开发团队注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间取挂起和恢复现场并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下“,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。 1712 | 1713 | 自旋等待不能代替阻塞。自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会拜拜浪费处理器资源。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当使用传统的方式去挂起线程了。 1714 | 1715 | 自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK6中已经变为默认开启,并且引入了自适应的自旋锁。自适应意味着自旋的时间不在固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。 1716 | 1717 | 自旋是在轻量级锁中使用的,在重量级锁中,线程不使用自旋。 1718 | 1719 | > 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100次循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。 1720 | 1721 | **锁消除** 1722 | 锁消除是虚拟机JIT在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判断依据是来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而能被其他线程访问到,那就可以把他们当做栈上数据对待,认为他们是线程私有的,同步加锁自然就无需进行。 1723 | 1724 | 来看这样一个方法: 1725 | 1726 | ```java 1727 | public String concatString(String s1, String s2, String s3) 1728 | { 1729 | StringBuffer sb = new StringBuffer(); 1730 | sb.append(s1); 1731 | sb.append(s2); 1732 | sb.append(s3); 1733 | return sb.toString(); 1734 | } 1735 | ``` 1736 | 1737 | 可以知道StringBuffer 的append方法定义如下: 1738 | 1739 | ```java 1740 | public synchronized StringBuffer append(StringBuffer sb) { 1741 | super.append(sb); 1742 | return this; 1743 | } 1744 | ``` 1745 | 1746 | 也就是说在concatString()方法中涉及了同步操作。但是可以观察到sb对象它的作用域被限制在方法的内部,也就是sb对象不会“逃逸”出去,其他线程无法访问。因此,虽然这里有锁,但是可以被安全的消除,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。 1747 | 1748 | **锁粗化** 1749 | 原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制的尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁禁止,那等待的线程也能尽快拿到锁。大部分情况下,这些都是正确的。但是,如果一些列的联系操作都是同一个对象反复加上和解锁,甚至加锁操作是出现在循环体中的,那么即使没有线程竞争,频繁地进行互斥同步操作也导致不必要的性能损耗。 1750 | 1751 | 举个案例,类似锁消除的concatString()方法。如果StringBuffer sb = new StringBuffer();定义在方法体之外,那么就会有线程竞争,但是每个append()操作都对同一个对象反复加锁解锁,那么虚拟机探测到有这样的情况的话,会把加锁同步的范围扩展到整个操作序列的外部,即扩展到第一个append()操作之前和最后一个append()操作之后,这样的一个锁范围扩展的操作就称之为锁粗化。 1752 | 1753 | **可重入锁** 1754 | 可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。 1755 | 1756 | 在JAVA环境下 ReentrantLock 和synchronized 都是可重入锁。可重入锁最大的作用是避免死锁。 1757 | 1758 | **类锁和对象锁** 1759 | 类锁:在方法上加上static synchronized的锁,或者synchronized(xxx.class)的锁。如下代码中的method1和method2: 1760 | 1761 | 对象锁:参考method4, method5,method6. 1762 | 1763 | ```java 1764 | public class LockStrategy 1765 | { 1766 | public Object object1 = new Object(); 1767 | 1768 | public static synchronized void method1(){} 1769 | public void method2(){ 1770 | synchronized(LockStrategy.class){} 1771 | } 1772 | 1773 | public synchronized void method4(){} 1774 | public void method5() 1775 | { 1776 | synchronized(this){} 1777 | } 1778 | public void method6() 1779 | { 1780 | synchronized(object1){} 1781 | } 1782 | } 1783 | 1784 | ``` 1785 | 1786 | 下面做一道习题来加深一下对对象锁和类锁的理解. 1787 | 有一个类这样定义: 1788 | 1789 | ```java 1790 | public class SynchronizedTest 1791 | { 1792 | public synchronized void method1(){} 1793 | public synchronized void method2(){} 1794 | public static synchronized void method3(){} 1795 | public static synchronized void method4(){} 1796 | } 1797 | ``` 1798 | 1799 | 那么,有SynchronizedTest的两个实例a和b,对于一下的几个选项有哪些能被一个以上的线程同时访问呢? 1800 | A. a.method1() vs. a.method2() 1801 | B. a.method1() vs. b.method1() 1802 | C. a.method3() vs. b.method4() 1803 | D. a.method3() vs. b.method3() 1804 | E. a.method1() vs. a.method3() 1805 | 答案是什么呢?BE 1806 | 1807 | \##**偏向锁、轻量级锁和重量级锁** 1808 | synchronized的偏向锁、轻量级锁以及重量级锁是通过Java对象头实现的。博主在[Java对象大小内幕浅析](http://blog.csdn.net/u013256816/article/details/51008443)中提到了Java对象的内存布局分为:对象头、实例数据和对齐填充,而对象头又可以分为"Mark Word"和类型指针klass。"Mark Word"是关键,默认情况下,其存储对象的HashCode、分代年龄和锁标记位。 1809 | 1810 | 这里说的都是以HotSpot虚拟机为基准的。首先来看一下"Mark Word"的内容: 1811 | 1812 | | 锁状态 | 存储内容 | 标志位 | 1813 | | ------ | ------------------------------------------------------- | ------ | 1814 | | 无锁 | 对象的hashCode、对象分代年龄、是否是偏向锁(0) | 01 | 1815 | | 轻量级 | 指向栈中锁记录的指针 | 00 | 1816 | | 重量级 | 指向互斥量(重量级锁)的指针 | 10 | 1817 | | GC标记 | (空) | 11 | 1818 | | 偏向锁 | 偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) | 01 | 1819 | 1820 | 注意到这里的无锁和偏向锁在"Mark Word"的倒数第三bit中分别采用0和1标记。 1821 | 1822 | 偏向锁是JDK6中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。 1823 | 1824 | 偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。 1825 | 1826 | 当锁对象第一次被线程获取的时候,线程使用CAS操作把这个锁的线程ID记录再对象Mark Word之中,同时置偏向标志位1。以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。 1827 | 1828 | 如果线程使用CAS操作时失败则表示该锁对象上存在竞争并且这个时候另外一个线程获得偏向锁的所有权。当到达全局安全点(safepoint,这个时间点上没有正在执行的字节码)时获得偏向锁的线程被挂起,膨胀为轻量级锁(涉及Monitor Record,Lock Record相关操作,这里不展开),同时被撤销偏向锁的线程继续往下执行同步代码。 1829 | 1830 | 当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。 1831 | 1832 | 线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录(Lock Record)的空间,并将对象头中的Mard Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。如果自旋失败则锁会膨胀成重量级锁。如果自旋成功则依然处于轻量级锁的状态。 1833 | 1834 | 轻量级锁的解锁过程也是通过CAS操作来进行的,如果对象的Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中赋值的Displaced Mark Word替换回来,如果替换成功,整个同步过程就完成了,如果替换失败,就说明有其他线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。 1835 | 1836 | 轻量级锁提升程序同步性能的依据是:对于绝大部分的锁,在整个同步周期内都是不存在竞争的(区别于偏向锁)。这是一个经验数据。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢。 1837 | 1838 | 整个synchronized锁流程如下: 1839 | 1840 | 1. 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁 1841 | 2. 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1 1842 | 3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。 1843 | 4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁 1844 | 5. 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。 1845 | 6. 如果自旋成功则依然处于轻量级状态。 1846 | 7. 如果自旋失败,则升级为重量级锁。 1847 | 1848 | **悲观锁和乐观锁** 1849 | 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。 1850 | 乐观锁:假定不会发生并发冲突,只在提交操作时检测是否违反数据完整性。(使用版本号或者时间戳来配合实现) 1851 | 1852 | **共享锁和排它锁** 1853 | 共享锁:如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排它锁。获准共享锁的事务只能读数据,不能修改数据。 1854 | 排它锁:如果事务T对数据A加上排它锁后,则其他事务不能再对A加任何类型的锁。获得排它锁的事务即能读数据又能修改数据。 1855 | 1856 | **读写锁** 1857 | 读写锁是一个资源能够被多个读线程访问,或者被一个写线程访问但不能同时存在读线程。Java当中的读写锁通过ReentrantReadWriteLock实现。具体使用方法这里不展开。 1858 | 1859 | **互斥锁** 1860 | 所谓互斥锁就是指一次最多只能有一个线程持有的锁。在JDK中synchronized和JUC的Lock就是互斥锁。 1861 | 1862 | **无锁** 1863 | 要保证现场安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的。 1864 | 1865 | 1. 无状态编程。无状态代码有一些共同的特征:不依赖于存储在对上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非无状态的方法等。可以参考Servlet。 1866 | 2. 线程本地存储。可以参考ThreadLocal 1867 | 3. volatile 1868 | 4. CAS 1869 | 5. 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。 1870 | 1871 | **分段锁** 1872 | ConcurrentHashMap中采用了分段锁 1873 | 1874 | **闭锁** 1875 | 闭锁是一种同步工具类,可以延迟线程的进度直到其到达终止状态。闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能通过,当到达结束状态时,这扇门会打开允许所有的线程通过。当闭锁到达结束状态后,将不会再改变状态,因此这扇门将永远保持打开状态。闭锁可以用来确保某些活动指导其他活动都完成后才继续执行。CountDownLatch就是一种灵活的闭锁实现。 1876 | 1877 | **死锁** 1878 | 死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,他们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足一下4个条件: 1879 | 1880 | - 互斥条件:一个资源每次只能被一个进程使用。 1881 | - 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 1882 | - 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。 1883 | - 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 1884 | 1885 | 避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序做操作来避免死锁。 1886 | 1887 | **活锁** 1888 | LiveLock是一种形式活跃性问题,该问题尽管不会阻塞线程,但也不能继续执行,因为线程将不断重复执行相同的操作,而且总会失败。活锁通常发送在处理事务消息的应用程序中:如果不能成功地处理某个消息,那么消息处理机制将回滚整个事务,并将它重新放到队列的开头:如果不能成功地处理某个消息,那么消息处理机制将回滚整个事务,并将它重新放到队列的开头。如果消息处理器在处理某种特定类型的消息时存在错误并导致它失败,那么每当这个消息从队列中取出并传递到存在错误的处理器时,都会发生事务回滚。由于这条消息又被放回到队列开头,因此处理器将被反复调用,并返回相同的结果。 1889 | 1890 | 1891 | 1892 | 1893 | 1894 | ## **四、多线程并发篇** 1895 | 1896 | **1、进程线程区别,线程安全和非线程安全区别** 1897 | 1898 | ​ 进程是程序的运行过程,是资源分配的基本单位,进程中可以包含多个线程,多个线程共享进程中堆、方法区资源 1899 | 1900 | ​ 线程是cpu任务调度的最小执行单位,每个线程拥有自己独立的程序计数器、虚拟机栈、本地方法栈 1901 | 1902 | **线程安全:**多个线程对同一资源操作,不会互相影响 1903 | 1904 | **非线程安全:**多个线程对同一资源操作,会互相影响 1905 | 1906 | 1907 | 1908 | 1909 | 1910 | **2、线程状态,start,run,wait,notify,yiled,sleep,join等方法的作用以及区别** 1911 | 1912 | **线程状态:创建、就绪、运行、阻塞、死亡** 1913 | 1914 | | 方法 | 作用 | 区别 | 1915 | | --------- | ------------------------------------------------------ | ---------------- | 1916 | | start | 启动线程,由虚拟机自动调度执行run()方法 | 线程处于就绪状态 | 1917 | | run | 线程逻辑代码块处理,JVM调度执行 | 线程处于运行状态 | 1918 | | sleep | 让当前正在执行的线程休眠(暂停执行) | 不释放锁 | 1919 | | wait | 使得当前线程等待 | 释放同步锁 | 1920 | | notify | 唤醒在此对象监视器上等待的单个线程 | 唤醒单个线程 | 1921 | | notifyAll | 唤醒在此对象监视器上等待的所有线程 | 唤醒多个线程 | 1922 | | yiled | 停止当前线程,让同等优先权的线程运行 | 用Thread类调用 | 1923 | | join | 使当前线程停下来等待,直至另一个调用join方法的线程终止 | 用线程对象调用 | 1924 | 1925 | 1926 | 1927 | **3、wait,notify,notifyAll阻塞唤醒确切过程?** 1928 | 1929 | 在哪阻塞,在哪唤醒?为什么要出现在同步代码块中? 1930 | 1931 | **阻塞:** 1932 | 1933 | ​ 这三个方法的调用都会使当前线程阻塞。该线程将会被放置到对该Object的请求等待队列中,然后让出当前对Object所拥有的所有的同步请求。线程会一直暂停所有线程调度,直到下面其中一种情况发生: 1934 | 1935 |     ① 其他线程调用了该Object的notify方法,而该线程刚好是那个被唤醒的线程; 1936 | 1937 |     ② 其他线程调用了该Object的notifyAll方法; 1938 | 1939 | **唤醒:** 1940 | 1941 | ​ 线程将会从等待队列中移除,重新成为可调度线程。它会与其他线程以常规的方式竞争对象同步请求。**一旦它重新获得对象的同步请求,所有之前的请求状态都会恢复,也就是线程调用wait的地方的状态。线程将会在之前调用wait的地方继续运行下去。** 1942 | 1943 | **原因:** 1944 | 1945 | ​ 由于`wait()属于Object方法,调用之后会强制释放当前对象锁,所以在wait()` 调用时必须拿到当前对象的监视器monitor对象。因此,wait()方法在同步方法/代码块中调用。 1946 | 1947 | 1948 | 1949 | **4、守护线程,线程中断** 1950 | 1951 | **守护线程:** 1952 | 1953 | ​ t.setDaemon(true)为守护线程,也叫精灵线程,若主线程启动t线程,则t线程是主线程的守护线程,当主线程执行完了,则守护线程也随之结束。 1954 | 1955 | ```java 1956 | 1957 | public class ThreadDaemon extends Thread{ 1958 | 1959 | public ThreadDaemon(String name){ 1960 | super(name); 1961 | } 1962 | 1963 | @Override 1964 | public void run() { 1965 | while(true){ 1966 | System.out.println(Thread.currentThread().getName() + "线程运行了。。。"); 1967 | try { 1968 | Thread.sleep(1000); 1969 | } catch (InterruptedException e) { 1970 | e.printStackTrace(); 1971 | } 1972 | } 1973 | } 1974 | 1975 | public static void main(String[] args) { 1976 | Thread t1 = new ThreadDaemon("线程一"); 1977 | Thread t2 = new ThreadDaemon("线程二"); 1978 | //设置为守护线程 1979 | t1.setDaemon(true); 1980 | t2.setDaemon(true); 1981 | //启动线程 1982 | t1.start(); 1983 | t2.start(); 1984 | //主线程2s后退出 1985 | try { 1986 | Thread.sleep(2000); 1987 | } catch (InterruptedException e) { 1988 | e.printStackTrace(); 1989 | } 1990 | } 1991 | 1992 | 1993 | } 1994 | ``` 1995 | 1996 | **线程中断:** 1997 | 1998 | ​ t.interrupt();调用interrupt()不会让线程立即中断,只是线程的中断状态发生变化,系统会在后续中断该线程 1999 | 2000 | ```java 2001 | 2002 | public class ThreadInterrupt extends Thread{ 2003 | 2004 | public ThreadInterrupt(String name){ 2005 | super(name); 2006 | } 2007 | 2008 | @Override 2009 | public void run() { 2010 | while(!interrupted()){//中断状态判断 2011 | System.err.println(Thread.currentThread().getName() + "线程运行了。。。"); 2012 | try { 2013 | Thread.sleep(1000); 2014 | } catch (InterruptedException e) { 2015 | e.printStackTrace(); 2016 | } 2017 | } 2018 | } 2019 | 2020 | public static void main(String[] args) { 2021 | Thread t1 = new ThreadInterrupt("线程一"); 2022 | Thread t2 = new ThreadInterrupt("线程二"); 2023 | //启动线程 2024 | t1.start(); 2025 | t2.start(); 2026 | t1.interrupt(); 2027 | } 2028 | 2029 | 2030 | } 2031 | ``` 2032 | 2033 | 2034 | 2035 | **5、Java乐观锁机制,CAS思想?缺点?是否原子性?如何保证?** 2036 | 2037 | **java乐观锁机制:** 2038 | 2039 | ​ 乐观锁体现的是悲观锁的反面。它是一种积极的思想,它总是认为数据是不会被修改的,所以是不会对数据上锁的。但是乐观锁在更新的时候会去判断数据是否被更新过。乐观锁的实现方案一般有两种(版本号机制和CAS)。乐观锁适用于**读多写少的场景,这样可以提高系统的并发量**。在Java中 **java.util.concurrent.atomic**下的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。 2040 | 2041 |   乐观锁,大多是基于数据版本 (Version)记录机制实现。即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来 实现。 读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提 交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据 版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。 2042 | 2043 | **CAS思想:** 2044 | 2045 | ​ CAS就是compare and swap(**比较交换**),是一种很出名的无锁的算法,就是可以不使用锁机制实现线程间的同步。使用CAS线程是不会被阻塞的,所以又称为非阻塞同步。CAS算法涉及到三个操作: 2046 | 2047 | ​ 需要读写内存值V; 2048 | 2049 | ​ 进行比较的值A; 2050 | 2051 | ​ 准备写入的值B 2052 | 2053 | ​ 当且仅当V的值等于A的值等于V的值的时候,才用B的值去更新V的值,否则不会执行任何操作(比较和替换是一个原子操作-A和V比较,V和B替换),一般情况下是一个**自旋操作**,即**不断重试** 2054 | 2055 | **缺点:** 2056 | 2057 | ​ [ABA问题-知乎](https://www.zhihu.com/question/23281499/answer/854522984) 2058 | 2059 | ​ 高并发的情况下,很容易发生并发冲突,如果CAS一直失败,那么就会一直重试,浪费CPU资源 2060 | 2061 | **原子性:** 2062 | 2063 | ​ 功能限制CAS是能保证单个变量的操作是原子性的,在Java中要配合使用volatile关键字来保证线程的安全;当涉及到多个变量的时候CAS无能为力;除此之外CAS实现需要硬件层面的支持,在Java的普通用户中无法直接使用,只能**借助atomic包下的原子类**实现,灵活性受到了限制 2064 | 2065 | 2066 | 2067 | **6、synchronized使用方法?底层实现?** 2068 | 2069 | **使用方法:**主要的三种使⽤⽅式 2070 | 2071 | ​ **修饰实例⽅法:** 作⽤于当前对象实例加锁,进⼊同步代码前要获得当前对象实例的锁 2072 | 2073 | ​ **修饰静态⽅法:** 也就是给当前类加锁,会作⽤于类的所有对象实例,因为静态成员不属于任何⼀个实例对象,是类成员( static 表明这是该类的⼀个静态资源,不管new了多少个对象,只有⼀份)。所以如果⼀个线程A调⽤⼀个实例对象的⾮静态 synchronized ⽅法,⽽线程B需要调⽤这个实例对象所属类的静态 synchronized ⽅法,是允许的,不会发⽣互斥现象,因为访问静态synchronized ⽅法占⽤的锁是当前类的锁,⽽访问⾮静态 synchronized ⽅法占⽤的锁是当前实例对象锁。 2074 | 2075 | ​ **修饰代码块:** 指定加锁对象,对给定对象加锁,进⼊同步代码库前要获得给定对象的锁。 2076 | 2077 | ​ **总结:**synchronized锁住的资源只有两类:一个是**对象**,一个是**类**。 2078 | 2079 | **底层实现:** 2080 | 2081 | ​ 对象头是我们需要关注的重点,它是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由`Mark Word` 和 `Class Metadata Address`组成,**其中`Mark Word`存储对象的hashCode、锁信息或分代年龄或GC标志等信息**,**`Class Metadata Address`是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例**。 2082 | 2083 | ​ 锁也分不同状态,JDK6之前只有两个状态:无锁、有锁(重量级锁),而在JDK6之后对synchronized进行了优化,新增了两种状态,总共就是四个状态:**无锁状态、偏向锁、轻量级锁、重量级锁**,其中无锁就是一种状态了。锁的类型和状态在对象头`Mark Word`中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的`Mark Word`数据。 2084 | 2085 | ​ 每一个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现)。每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态。 2086 | 2087 | 2088 | 2089 | 2090 | 2091 | **7、ReenTrantLock使用方法?底层实现?和synchronized区别?** 2092 | 2093 | ​ 由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下三项: 2094 | 2095 | ```java 2096 | 1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。通过lock.lockInterruptibly()来实现这个机制。 2097 | 2098 | 2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。 2099 | 3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。 2100 | 2101 | ``` 2102 | 2103 | **使用方法:** 2104 | 2105 | ​ 基于API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成 2106 | 2107 | **底层实现:** 2108 | 2109 | ​ ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。 2110 | 2111 | **和synchronized区别:** 2112 | 2113 | ​ 1、**底层实现**上来说,synchronized 是**JVM**层面的锁,是**Java关键字**,通过monitor对象来完成(monitorenter与monitorexit),对象只有在同步块或同步方法中才能调用wait/notify方法;ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的**API层面**的锁。synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁;ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。 2114 | 2115 | ​ 3、**是否可手动释放:**synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。 2116 | 2117 | ​ 4、**是否可中断**synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。 2118 | 2119 | ​ 5、**是否公平锁**synchronized为非公平锁 ;ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。 2120 | 2121 | 2122 | 2123 | **8、公平锁和非公平锁区别?为什么公平锁效率低?** 2124 | 2125 | **公平锁:** 2126 | 2127 | ​ 公平锁自然是遵循**FIFO**(先进先出)原则的,先到的线程会优先获取资源,后到的会进行排队等待 2128 | 2129 | ​ **优点:**所有的线程都能得到资源,不会饿死在队列中。 2130 | 2131 | ​ **缺点:**吞吐量会下降,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销大 2132 | 2133 | 2134 | 2135 | **非公平锁:** 2136 | 2137 | ​ 多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。 2138 | 2139 | ​ **优点:**可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。 2140 | 2141 | ​ **缺点:**你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁 2142 | 2143 | 2144 | 2145 | **公平锁效率低原因:** 2146 | 2147 | ​ 公平锁要维护一个队列,后来的线程要加锁,即使锁空闲,也要先检查有没有其他线程在 wait,如果有自己要挂起,加到队列后面,然后唤醒队列最前面线程。这种情况下相比较非公平锁多了一次**挂起和唤醒**。 2148 | 2149 | ​ **线程切换的开销**,其实就是非公平锁效率高于公平锁的原因,因为**非公平锁减少了线程挂起的几率**,后来的线程有一定几率逃离被挂起的开销。 2150 | 2151 | 2152 | 2153 | 2154 | 2155 | **9、锁优化。自旋锁、自适应自旋锁、锁消除、锁粗化、偏向锁、轻量级锁、重量级锁解释** 2156 | 2157 | **锁优化:** 2158 | 2159 | ​ 【1】**减少锁的时间:** 2160 | ​ 不需要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可以让锁尽快释放; 2161 | 2162 | ​ 【2】**减少锁的粒度:** 2163 | ​ 它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。它的思想也是用空间来换时间;java中很多数据结构都是采用这种方法提高并发操作的效率,比如: 2164 | 2165 | ​ **ConcurrentHashMap:** 2166 | 2167 | ​ java中的ConcurrentHashMap在jdk1.8之前的版本,使用一个Segment 数组:Segment< K,V >[] segments 2168 | 2169 | ​ Segment继承自ReenTrantLock,所以每个Segment是个可重入锁,每个Segment 有一个HashEntry< K,V >数组用来存放数据,put操作时,先确定往哪个Segment放数据,只需要锁定这个Segment,执行put,其它的Segment不会被锁定;所以数组中有多少个Segment就允许同一时刻多少个线程存放数据,这样增加了并发能力。 2170 | 2171 | Segment继承自ReenTrantLock,所以每个Segment就是个可重入锁,每个Segment 有一个HashEntry< K,V >数组用来存放数据,put操作时,先确定往哪个Segment放数据,只需要锁定这个Segment,执行put,其它的Segment不会被锁定;所以数组中有多少个Segment就允许同一时刻多少个线程存放数据,这样增加了并发能力。 2172 | 2173 | ​ 【3】**锁粗化:** 2174 | ​ 大部分情况下我们是要让锁的粒度最小化,锁的粗化则是要增大锁的粒度; 2175 | 2176 | ​ 在以下场景下需要粗化锁的粒度: 2177 | 2178 | ​ 假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的; 2179 | 2180 | ​ 【4】**使用读写锁:** 2181 | 2182 | ​ ReentrantReadWriteLock 是一个读写锁,读操作加读锁,可并发读,写操作使用写锁,只能单线程写; 2183 | 2184 | ​ 【5】**使用cas:** 2185 | 2186 | ​ 如果需要同步的操作执行速度非常快,并且线程竞争并不激烈,这时候使用cas效率会更高,因为加锁会导致线程的上下文切换,如果上下文切换的耗时比同步操作本身更耗时,且线程对资源的竞争不激烈,使用volatiled+cas操作会是非常高效的选择; 2187 | 2188 | 2189 | 2190 | **自旋锁:** 2191 | 2192 | ​ 自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做**内核态和用户态之间的切换**进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就**避免用户线程和内核的切换的消耗**。 2193 | 2194 | ​ **缺点:**如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销;自旋次数默认值是10 2195 | 2196 | **自适应自旋锁:** 2197 | 2198 | ​ 对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点 2199 | 2200 | **锁消除:** 2201 | 2202 | ​ 锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。 2203 | 2204 | **锁粗化:** 2205 | 2206 | ​ 假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的; 2207 | 2208 | **偏向锁:** 2209 | 2210 | ​ 所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程;也就是说,这个线程已经占有这个锁,当他在次试图去获取这个锁的时候,他会已最快的方式去拿到这个锁,而不需要在进行一些monitor操作,因此这方面他是会对性能有所提升的,因为在大部分情况下是没有竞争的,所以锁此时是没用的,所以使用偏向锁是可以提高性能的; 2211 | 2212 | **重量级锁:** 2213 | 2214 | ​ 重量级锁的加锁、解锁过程和轻量级锁差不多,区别是:竞争失败后,线程阻塞,释放锁后,唤醒阻塞的线程,不使用自旋锁,不会那么消耗CPU,所以重量级锁适合用在同步块执行时间长的情况下。 2215 | 2216 | 2217 | 2218 | **10、Java内存模型** 2219 | 2220 | ​ Java 内存模型(Java Memory Model,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了 Java 程序在各种平台下对内存的访问都能保证效果一致的机制及规范。 2221 | 2222 | ​ JMM 是一种规范,是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。目的是保证并发编程场景中的原子性、可见性和有序性。 2223 | 2224 | ​ 所以,Java 内存模型,除了定义了一套规范,还提供了一系列原语,封装了底层实现后,供开发者直接使用。我们前面提到,并发编程要解决**原子性**、**有序性**和**一致性**的问题。 2225 | 2226 | **原子性:** 2227 | 2228 | ​ 在 Java 中,为了保证原子性,提供了两个高级的字节码指令 Monitorenter 和 Monitorexit。这两个字节码,在 Java 中对应的关键字就是 Synchronized。因此,在 Java 中可以使用 Synchronized 来保证方法和代码块内的操作是原子性的。 2229 | 2230 | **可见性:** 2231 | 2232 | ​ Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。Java 中的 Volatile 关键字修饰的变量在被修改后可以立即同步到主内存。被其修饰的变量在每次使用之前都从主内存刷新。因此,可以使用 Volatile 来保证多线程操作时变量的可见性。除了 Volatile,Java 中的 Synchronized 和 Final 两个关键字也可以实现可见性。只不过实现方式不同 2233 | 2234 | **有序性** 2235 | 2236 | ​ 在 Java 中,可以使用 Synchronized 和 Volatile 来保证多线程之间操作的有序性。区别:Volatile 禁止指令重排。Synchronized 保证同一时刻只允许一条线程操作。 2237 | 2238 | 2239 | 2240 | **11、volatile作用?底层实现?单例模式中volatile的作用?** 2241 | 2242 | **作用:** 2243 | 2244 | ​ 保证数据的“可见性”:被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。 2245 | 2246 | ​ 禁止指令重排:在多线程操作情况下,指令重排会导致计算结果不一致 2247 | 2248 | **底层实现:** 2249 | 2250 | ​ “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令” 2251 | 2252 |   lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能: 2253 | 2254 |   1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成; 2255 | 2256 |   2)它会强制将对缓存的修改操作立即写入主存; 2257 | 2258 |   3)如果是写操作,它会导致其他CPU中对应的缓存行无效。 2259 | 2260 | **单例模式中volatile的作用:** 2261 | 2262 | 防止代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化。 2263 | 2264 | ```java 2265 | class Singleton{ 2266 | private volatile static Singleton instance = null; //禁止指令重排 2267 | private Singleton() { 2268 | 2269 | } 2270 | public static Singleton getInstance() { 2271 | if(instance==null) { 2272 | synchronized (Singleton.class) { 2273 | if(instance==null) 2274 | instance = new Singleton(); 2275 | } 2276 | } 2277 | return instance; 2278 | } 2279 | } 2280 | ``` 2281 | 2282 | 2283 | 2284 | **12、AQS思想,以及基于AQS实现的lock, CountDownLatch、CyclicBarrier、Semaphore介绍** 2285 | 2286 | ​ AQS的全称为(AbstractQueuedSynchronizer)抽象的队列式的同步器,是⼀个⽤来构建锁和同步器的框架,使⽤AQS能简单且⾼效地构造出应⽤⼴泛的⼤量的同步器,如:基于AQS实现的lock, CountDownLatch、CyclicBarrier、Semaphore 2287 | 2288 | ​ AQS核⼼思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的⼯作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占⽤,那么就需要⼀套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是⽤CLH(虚拟的双向队列)队列锁实现的,即将暂时获取不到锁的线程加⼊到队列中。 2289 | 2290 | **lock:** 2291 | 2292 | ​ 是一种可重入锁,除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。默认为非公平锁,但可以初始化为公平锁; 通过方法 lock()与 unlock()来进行加锁与解锁操作; 2293 | 2294 | **CountDownLatch:** 2295 | 2296 | ​ 通过计数法(倒计时器),让一些线程堵塞直到另一个线程完成一系列操作后才被唤醒;该⼯具通常⽤来控制线程等待,它可以让某⼀个线程等待直到倒计时结束,再开始执⾏。 2297 | 2298 | ​ 假设我们有这么一个场景,教室里有班长和其他6个人在教室上自习,怎么保证班长等其他6个人都走出教室在把教室门给关掉。 2299 | 2300 | ```java 2301 | public class CountDownLanchDemo { 2302 | public static void main(String[] args) { 2303 | for (int i = 0; i < 6; i++) { 2304 | new Thread(() -> { 2305 | System.out.println(Thread.currentThread().getName() + " 离开了教室..."); 2306 | }, String.valueOf(i)).start(); 2307 | } 2308 | System.out.println("班长把门给关了,离开了教室..."); 2309 | } 2310 | } 2311 | ``` 2312 | 2313 | **此时输出:** 2314 | 2315 | ```java 2316 | 0 离开了教室... 2317 | 1 离开了教室... 2318 | 2 离开了教室... 2319 | 3 离开了教室... 2320 | 班长把门给关了,离开了教室... 2321 | 5 离开了教室... 2322 | 4 离开了教室... 2323 | ``` 2324 | 2325 | 发现班长都没有等其他人理他教室就把门给关了,此时我们就可以使用 CountDownLatch 来控制 2326 | 2327 | ```java 2328 | public class CountDownLanchDemo { 2329 | public static void main(String[] args) throws InterruptedException { 2330 | CountDownLatch countDownLatch = new CountDownLatch(6); 2331 | for (int i = 0; i < 6; i++) { 2332 | new Thread(() -> { 2333 | countDownLatch.countDown(); 2334 | System.out.println(Thread.currentThread().getName() + " 离开了教室..."); 2335 | }, String.valueOf(i)).start(); 2336 | } 2337 | countDownLatch.await(); 2338 | System.out.println("班长把门给关了,离开了教室..."); 2339 | } 2340 | } 2341 | ``` 2342 | 2343 | **此时输出:** 2344 | 2345 | ```java 2346 | 0 离开了教室... 2347 | 1 离开了教室... 2348 | 2 离开了教室... 2349 | 3 离开了教室... 2350 | 4 离开了教室... 2351 | 5 离开了教室... 2352 | 班长把门给关了,离开了教室... 2353 | ``` 2354 | 2355 | 2356 | 2357 | **CyclicBarrier:** 2358 | 2359 | ​ 字面意思是可循环(Cyclic)使用的屏障(Barrier)。他要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法。 2360 | 2361 | 我们假设有这么一个场景,每辆车只能坐4个人,当车满了,就发车。 2362 | 2363 | ```java 2364 | public class CyclicBarrierDemo { 2365 | public static void main(String[] args) { 2366 | CyclicBarrier cyclicBarrier = new CyclicBarrier(4, () -> { 2367 | System.out.println("车满了,开始出发..."); 2368 | }); 2369 | for (int i = 0; i < 8; i++) { 2370 | new Thread(() -> { 2371 | System.out.println(Thread.currentThread().getName() + " 开始上车..."); 2372 | try { 2373 | cyclicBarrier.await(); 2374 | } catch (InterruptedException e) { 2375 | e.printStackTrace(); 2376 | } catch (BrokenBarrierException e) { 2377 | e.printStackTrace(); 2378 | } 2379 | }).start(); 2380 | } 2381 | } 2382 | } 2383 | ``` 2384 | 2385 | **输出结果:** 2386 | 2387 | ```java 2388 | Thread-0 开始上车... 2389 | Thread-1 开始上车... 2390 | Thread-3 开始上车... 2391 | Thread-4 开始上车... 2392 | 车满了,开始出发... 2393 | Thread-5 开始上车... 2394 | Thread-7 开始上车... 2395 | Thread-2 开始上车... 2396 | Thread-6 开始上车... 2397 | 车满了,开始出发... 2398 | ``` 2399 | 2400 | 2401 | 2402 | **Semaphore:** 2403 | 2404 | ​ 信号量主要用于两个目的,一个是用于多个共享资源的互斥作用,另一个用于并发线程数的控制。 2405 | 2406 | 假设我们有 3 个停车位,6 辆车去抢;指定多个线程同时访问某个资源。 2407 | 2408 | ```java 2409 | public class SemaphoreDemo { 2410 | public static void main(String[] args) { 2411 | Semaphore semaphore = new Semaphore(3); 2412 | for (int i = 0; i < 6; i++) { 2413 | new Thread(() -> { 2414 | try { 2415 | semaphore.acquire(); // 获取一个许可 2416 | System.out.println(Thread.currentThread().getName() + " 抢到车位..."); 2417 | Thread.sleep(3000); 2418 | System.out.println(Thread.currentThread().getName() + " 离开车位"); 2419 | } catch (InterruptedException e) { 2420 | e.printStackTrace(); 2421 | } finally { 2422 | semaphore.release(); // 释放一个许可 2423 | } 2424 | }).start(); 2425 | } 2426 | } 2427 | } 2428 | 2429 | /**输出 2430 | Thread-1 抢到车位... 2431 | Thread-2 抢到车位... 2432 | Thread-0 抢到车位... 2433 | Thread-2 离开车位 2434 | Thread-0 离开车位 2435 | Thread-3 抢到车位... 2436 | Thread-1 离开车位 2437 | Thread-4 抢到车位... 2438 | Thread-5 抢到车位... 2439 | Thread-3 离开车位 2440 | Thread-5 离开车位 2441 | Thread-4 离开车位 2442 | */ 2443 | ``` 2444 | 2445 | 2446 | 2447 | 2448 | 2449 | **13、线程池构造函数7大参数,线程处理任务过程,线程拒绝策略** 2450 | 2451 | ```java 2452 | /** 2453 | * 线程池构造函数7大参数 2454 | */ 2455 | public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime, 2456 | TimeUnit unit,BlockingQueue workQueue,ThreadFactory threadFactory, 2457 | RejectedExecutionHandler handler) { 2458 | if (corePoolSize < 0 ||maximumPoolSize <= 0 ||maximumPoolSize < corePoolSize || 2459 | keepAliveTime < 0) 2460 | throw new IllegalArgumentException(); 2461 | if (workQueue == null || threadFactory == null || handler == null) 2462 | throw new NullPointerException(); 2463 | this.corePoolSize = corePoolSize; 2464 | this.maximumPoolSize = maximumPoolSize; 2465 | this.workQueue = workQueue; 2466 | this.keepAliveTime = unit.toNanos(keepAliveTime); 2467 | this.threadFactory = threadFactory; 2468 | this.handler = handler; 2469 | } 2470 | ``` 2471 | 2472 | **参数介绍:** 2473 | 2474 | | 参数 | 作用 | 2475 | | ------------------------ | ------------------------------------------------------------ | 2476 | | corePoolSize | 核心线程池大小 | 2477 | | maximumPoolSize | 最大线程池大小 | 2478 | | keepAliveTime | 线程池中超过 corePoolSize 数目的空闲线程最大存活时间;可以allowCoreThreadTimeOut(true) 使得核心线程有效时间 | 2479 | | TimeUnit | keepAliveTime 时间单位 | 2480 | | workQueue | 阻塞任务队列 | 2481 | | threadFactory | 新建线程工厂 | 2482 | | RejectedExecutionHandler | 拒绝策略。当提交任务数超过 maxmumPoolSize+workQueue 之和时,任务会交给RejectedExecutionHandler 来处理 | 2483 | 2484 | 2485 | 2486 | **线程拒绝策略:** 2487 | 2488 | ​ 线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。 2489 | 2490 | JDK 内置的拒绝策略如下: 2491 | 2492 | ​ **AbortPolicy:**直接抛出异常,阻止系统正常运行。 2493 | 2494 | ​ **CallerRunsPolicy :**只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。 2495 | 2496 | ​ **DiscardOldestPolicy :**丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。 2497 | 2498 | ​ **DiscardPolicy :**该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。 2499 | 2500 | 2501 | 2502 | **线程处理任务过程:** 2503 | 2504 | 1. 当线程池小于corePoolSize,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。 2505 | 2. 当线程池达到corePoolSize时,新提交任务将被放入 workQueue 中,等待线程池中任务调度执行。 2506 | 3. 当workQueue已满,且 maximumPoolSize 大于 corePoolSize 时,新提交任务会创建新线程执行任务。 2507 | 4. 当提交任务数超过 maximumPoolSize 时,新提交任务由 RejectedExecutionHandler 处理。 2508 | 5. 当线程池中超过corePoolSize 线程,空闲时间达到 keepAliveTime 时,关闭空闲线程 。 2509 | 2510 | 2511 | 2512 | **14、Execuors类实现的几种线程池类型,阿里为啥不让用?** 2513 | 2514 | - **Executors.newSingleThreadExecutor():**只有一个线程的线程池,因此所有提交的任务是顺序执行,适用于一个一个任务执行的场景 2515 | - **Executors.newCachedThreadPool():**线程池里有很多线程需要同时执行,老的可用线程将被新的任务触发重新执行,如果线程超过60秒内没执行,那么将被终止并从池中删除,适用执行很多短期异步的小程序或者负载较轻的服务 2516 | - **Executors.newFixedThreadPool():**拥有固定线程数的线程池,如果没有任务执行,那么线程会一直等待,适用执行长期的任务,性能好很多。 2517 | - **Executors.newScheduledThreadPool():**用来调度即将执行的任务的线程池 2518 | 2519 | **因为以上方式都存在弊端:** 2520 | 2521 | ​ FixedThreadPool 和 SingleThreadExecutor : 允许请求的**队列⻓度**为 Integer.MAX_VALUE,可能堆积⼤量的请求,从⽽导致OOM。 2522 | ​ CachedThreadPool 和 ScheduledThreadPool : 允许创建的**线程数量**为 Integer.MAX_VALUE,可能会创建⼤量线程,从⽽导致OOM。 2523 | 2524 | 2525 | 2526 | **15、线程池大小如何设置?** 2527 | 2528 | - CPU 密集型 2529 | 2530 | - - CPU 密集的意思是该任务需要大量的运算,而没有阻塞,CPU 一直全速运行。 2531 | - CPU 密集型任务尽可能的少的线程数量,一般为 CPU 核数 + 1 个线程的线程池。 2532 | 2533 | - IO 密集型 2534 | 2535 | - - 由于 IO 密集型任务线程并不是一直在执行任务,可以多分配一点线程数,如 CPU * 2 2536 | - 也可以使用公式:CPU 核数 / (1 - 阻塞系数);其中阻塞系数在 0.8 ~ 0.9 之间。 2537 | 2538 | 2539 | 2540 | **16、手写简单的线程池,体现线程复用** 2541 | 2542 | https://blog.csdn.net/hongtaolong/article/details/87808009 2543 | 2544 | 2545 | 2546 | **17、手写消费者生产者模式** 2547 | 2548 | https://www.cnblogs.com/liuqing576598117/p/11233250.html 2549 | 2550 | 2551 | 2552 | **18、手写阻塞队列** 2553 | 2554 | https://www.cnblogs.com/keeya/p/9713686.html 2555 | 2556 | 2557 | 2558 | **19、手写多线程交替打印ABC** 2559 | 2560 | https://blog.csdn.net/xiaokang123456kao/article/details/77331878 2561 | 2562 | 2563 | 2564 | ## **五、MySQL篇** 2565 | 2566 | **1、事务4大特性?这4个特性mysql如何保证实现的?** 2567 | 2568 | **事务4大特性:**原子性、一致性、隔离性、持久性 2569 | 2570 | ​ **原⼦性:** 事务是最⼩的执⾏单位,不允许分割。事务的原⼦性确保动作要么全部完成,要么全不执行 2571 | 2572 | ​ **一致性:** 执⾏事务前后,数据保持⼀致,多个事务对同⼀个数据读取的结果是相同的; 2573 | 2574 | ​ **隔离性:** 并发访问数据库时,⼀个⽤户的事务不被其他事务所⼲扰,各并发事务之间数据库是独⽴的; 2575 | 2576 | ​ **持久性:** ⼀个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发⽣故障也不应该对其有任何影响。 2577 | 2578 | **实现保证:** 2579 | 2580 | ​ MySQL的存储引擎InnoDB使用重做日志保证一致性与持久性,回滚日志保证原子性,使用各种锁来保证隔离性。 2581 | 2582 | 2583 | 2584 | **2、事务隔离级别,4个隔离级别分别有什么并发问题?** 2585 | 2586 | **读未提交:**最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。 2587 | 2588 | **读已提交:**允许读取并发事务已经提交的数据,可以阻⽌脏读,但是幻读或不可重复读仍有可能发⽣。 2589 | 2590 | **可重复读:**同⼀字段的多次读取结果都是⼀致的,除⾮数据是被本身事务⾃⼰所修改,可以阻⽌脏读和不可重复读,但幻读仍有可能发⽣。 2591 | 2592 | **可串行化:**最⾼的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执⾏,这样事务之间就完全不可能产⽣⼲扰,也就是说,该级别可以防⽌脏读、不可重复读以及幻读。 2593 | 2594 | | 隔离级别 | 并发问题 | 2595 | | -------- | -------------------------------- | 2596 | | 读未提交 | 可能会导致脏读、幻读或不可重复读 | 2597 | | 读已提交 | 可能会导致幻读或不可重复读 | 2598 | | 可重复度 | 可能会导致幻读 | 2599 | | 可串行化 | 不会产⽣⼲扰 | 2600 | 2601 | 2602 | 2603 | **3、Mysql默认隔离级别?如何保证并发安全?** 2604 | 2605 | **默认隔离级别:**可重复读; 2606 | 2607 | ​ 同⼀字段的多次读取结果都是⼀致的,除⾮数据是被本身事务⾃⼰所修改; 2608 | 2609 | ​ 可重复读是有可能出现幻读的,如果要保证绝对的安全只能把隔离级别设置成SERIALIZABLE;这样所有事务都只能顺序执行,自然不会因为并发有什么影响了,但是性能会下降许多。 2610 | 2611 | ​ 第二种方式,使用更新的版本控制。维护一个字段作为updateversion,修改时updateversion也作为一个参数传入,在条件语句中添加例如`where id=? and update_version = ?` 当然set里面要update_version+1。这样可以控制到每次只能有一个人更新一个版本。 2612 | 2613 | 2614 | 2615 | **4、RR和RC如何实现的?RR使用场景?** 2616 | 2617 | ​ 事务隔离级别RC(read commit)和RR(repeatable read)两种事务隔离级别基于多版本并发控制MVCC(multi-version concurrency control)来实现。 2618 | 2619 | ​ 由于RC隔离级别需要保持语句级别的一致性,事务中每一次读取都是访问当前时间点的已提交数据,因此事务中多条查询语句会创建多个不同的ReadView,开销较大,复杂度更高;而对于RR隔离级别,仅需要一个版本的ReadView,消耗更少,因此Mysql默认使用RR隔离级别。 2620 | 2621 | ​ RC隔离级别获得的是语句级读一致性;RR隔离级别获得的是事务级读一致性 2622 | 2623 | ​ 对于RC隔离级别,访问的数据是每次语句执行时间点的数据,而对于RR隔离级别,访问的数据是事务中第一条语句执行时间点的数据。 2624 | 2625 | 2626 | 2627 | **5、隔离级别的单位是数据表还是数据行?如串行化级别,两个事务访问不同的数据行,能并发?** 2628 | 2629 | ​ 读未提交:不加锁 2630 | 2631 | ​ 读已提交:加行锁,只锁要修改的行 2632 | 2633 | ​ 可重复读:加行锁,锁定的是查询的行 2634 | 2635 | ​ 可串行化:加表锁,在读取的每张表上加锁 2636 | 2637 | ​ **串行化级别:读不同的行,可以并发** 2638 | 2639 | 2640 | 2641 | **6、存储引擎Innodb和Myisam的区别以及使用场景** 2642 | 2643 | **Myisam:**支持表锁,适合读密集的场景,不支持外键,不支持事务,索引与数据在不同的文件 2644 | 2645 | **Innodb:**支持行、表锁,默认为行锁,适合并发场景,支持外键,支持事务,索引与数据同一文件 2646 | 2647 | 2648 | 2649 | **7、 介绍Inodb锁机制,行锁,表锁,意向锁** 2650 | 2651 | **InnoDB⽀持⾏级锁(row-level locking)和表级锁,默认为⾏级锁** 2652 | 2653 | InnoDB按照不同的分类的锁: 2654 | 2655 | 共享/排它锁(Shared and Exclusive Locks):行级别锁, 2656 | 2657 | 意向锁(Intention Locks),表级别锁 2658 | 2659 | 间隙锁(Gap Locks),锁定一个区间 2660 | 2661 | 记录锁(Record Locks),锁定一个行记录 2662 | 2663 | **表级锁:** 2664 | 2665 | ​ Mysql中锁定 粒度最大 的一种锁,对当前操作的整张表加锁,实现简单 ,资源消耗也比较少,加锁快,不会出现死锁 。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM和 InnoDB引擎都支持表级锁。 2666 | 2667 | **行级锁:** 2668 | 2669 | ​ Mysql中锁定 粒度最小 的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。 InnoDB支持的行级锁,包括如下几种: 2670 | 2671 | ​ **记录锁(Record Lock):** 对索引项加锁,锁定符合条件的行。其他事务不能修改和删除加锁项; 2672 | 2673 | ​ **间隙锁(Gap Lock):** 对索引项之间的“间隙”加锁,锁定记录的范围(对第一条记录前的间隙或最后一条将记录后的间隙加锁),不包含索引项本身。其他事务不能在锁范围内插入数据,这样就防止了别的事务新增幻影行。 2674 | 2675 | ​ **Next-key Lock:** 锁定索引项本身和索引范围。即Record Lock和Gap Lock的结合。可解决幻读问题。 2676 | 2677 | **意向锁:** 2678 | 2679 | ​ 当一个事务在需要获取资源的锁定时,如果该资源已经被排他锁占用,则数据库会自动给该事务申请一个该表的意向锁。如果自己需要一个共享锁定,就申请一个**意向共享锁**。如果需要的是某行(或者某些行)的排他锁定,则申请一个**意向排他锁**。 2680 | 2681 | 2682 | 2683 | **8、介绍MVCC.** 2684 | 2685 | ​ MVCC是一种多版本并发控制机制,在大多数情况下代替行级锁,使用MVCC,能降低其系统开销. 2686 | 2687 | ​ MVCC是通过保存数据在某个时间点的快照来实现的. 不同存储引擎的MVCC实现是不同的,典型的有乐观并发控制和悲观并发控制. 2688 | 2689 | ​ InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的,这两个列,分别保存了这个行的创建时间,一个保存的是行的删除时间。这里存储的并不是实际的时间值,而是系统版本号(可以理解为事务的ID),每开始一个新的事务,系统版本号就会自动递增,事务开始时刻的系统版本号会作为事务的ID. 2690 | 2691 | ​ InnoDB只会查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的. 2692 | 2693 | ​ 1.MVCC手段只适用于Msyql隔离级别中的读已提交(Read committed)和可重复读(Repeatable Read). 2694 | 2695 | ​ 2.Read uncimmitted由于存在脏读,即能读到未提交事务的数据行,所以不适用MVCC. 2696 | 2697 | ​ 原因是MVCC的创建版本和删除版本只要在事务提交后才会产生。客观上,我们认为他就是乐观锁的一整实现方式,就是每行都有版本号,保存时根据版本号决定是否成功。 2698 | 2699 | 2700 | 2701 | 2702 | **9、哈希索引是如何实现的?** 2703 | 2704 | ​ 哈希索引用索引列的值计算该值的hashCode,然后在hashCode相应的位置存执该值所在行数据的物理位置,因为使用散列算法,因此访问速度非常快,但是一个值只能对应一个hashCode,而且是散列的分布方式,因此哈希索引不支持范围查找和排序的功能 2705 | 2706 | 2707 | 2708 | **10、数据库索引为什么使用B+树,相对于B树有什么优点?为什么不能红黑树?** 2709 | 2710 | **因为:** 2711 | 2712 | ​ B+树的磁盘读写代价低,更少的查询次数,查询效率更加稳定,有利于对数据库的扫描 2713 | 2714 | ​ 相对B树,B+树是B树的升级版,只是把非叶子节点冗余一下,这么做的好处是**为了提高范围查找的效率,解决数据库遍历效率低下问题**;**B+树只有叶节点存放数据,其余节点用来索引,而B树是每个索引节点都会有Data域。** 2715 | 2716 | ​ 在大规模数据存储的时候,红黑树往往出现由于**树的深度过大**而造成磁盘IO读写过于频繁,进而导致效率低下的情况。所以,只要我们通过某种较好的树结构减少树的结构尽量减少树的高度,B树与B+树可以有多个子女,从几十到上千,可以降低树的高度。 2717 | 2718 | ​ **磁盘预读原理**:将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。为了达到这个目的,在实际实现B-Tree还需要使用如下技巧:每次新建节点时,直接申请一个页的空间,这样就保证**一个节点物理上也存储在一个页里**,加之计算机存储分配都是按页对齐的,就实现了一个node只需一次I/O。 2719 | 2720 | 2721 | 2722 | **11、聚簇索引和非聚簇索引区别** 2723 | 2724 | ​ **聚簇索引:**将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据 2725 | 2726 | ​ **非聚簇索引:**将数据与索引分开存储,索引结构的叶子节点指向了数据对应的位置 2727 | 2728 | ​ 聚簇索引的叶子节点就是数据节点,而非聚簇索引的叶子节点仍然是索引节点,只不过有指向对应数据块的指针。 2729 | 2730 | 2731 | 2732 | **12、回表查询和覆盖索引** 2733 | 2734 | 普通索引 需要扫描两遍索引树 2735 | 2736 | (1)先通过普通索引定位到主键值id=5; 2737 | 2738 | (2)在通过聚集索引定位到行记录; 2739 | 2740 | 这就是所谓的**回表查询**,先定位主键值,再定位行记录,它的性能较扫一遍索引树更低。 2741 | 2742 | **覆盖索引**:如果where条件的列和返回的数据在一个索引中,那么不需要回查表,那么就叫覆盖索引。 2743 | 2744 | **实现覆盖索引**:常见的方法是,将被查询的字段,建立到联合索引里去。 2745 | 2746 | 2747 | 2748 | **13、如何创建索引?** 2749 | 2750 | ```sql 2751 | CREATE TABLE 表名( 2752 | 字段名 数据类型 [完整性约束条件], 2753 | ……, 2754 | [UNIQUE | FULLTEXT | SPATIAL] INDEX | KEY 2755 | [索引名](字段名1 [(长度)] [ASC | DESC]) [USING 索引方法] 2756 | ); 2757 | 2758 | 说明: 2759 | UNIQUE:可选。表示索引为唯一性索引。 2760 | FULLTEXT:可选。表示索引为全文索引。 2761 | SPATIAL:可选。表示索引为空间索引。 2762 | INDEX和KEY:用于指定字段为索引,两者选择其中之一就可以了,作用是一样的。 2763 | 索引名:可选。给创建的索引取一个新名称。 2764 | 字段名1:指定索引对应的字段的名称,该字段必须是前面定义好的字段。 2765 | 长度:可选。指索引的长度,必须是字符串类型才可以使用。 2766 | ASC:可选。表示升序排列。 2767 | DESC:可选。表示降序排列。 2768 | 注:索引方法默认使用B+TREE。 2769 | 2770 | ALTER TABLE 表名 ADD [UNIQUE | FULLTEXT | SPATIAL] INDEX | KEY [索引名] (字段名1 [(长度)] [ASC | DESC]) [USING 索引方法]; 2771 | 2772 | 或 2773 | 2774 | CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX 索引名 ON 表名(字段名) [USING 索引方法]; 2775 | ``` 2776 | 2777 | 2778 | 2779 | 2780 | 2781 | **14、如何避免全表扫描?** 2782 | 2783 | 1.对查询进行优化,应考虑在 where 及 order by 涉及的列上建立索引。 2784 | 2785 | 2.应尽量避免在 where 子句中对字段进行 null 值判断 2786 | 2787 | 3.应尽量避免在 where 子句中使用!=或<>操作符 2788 | 2789 | 4.in 和 not in 要慎用 2790 | 2791 | 否则将导致引擎放弃使用索引而进行全表扫描 2792 | 2793 | 2794 | 2795 | 2796 | 2797 | **15、Explain语句各字段的意义** 2798 | 2799 | 参考:https://www.jianshu.com/p/8fab76bbf448 2800 | 2801 | ```mysql 2802 | mysql> explain select * from staff; 2803 | +----+-------------+-------+------+---------------+------+---------+------+------+-------+ 2804 | | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | 2805 | +----+-------------+-------+------+---------------+------+---------+------+------+-------+ 2806 | | 1 | SIMPLE | staff | ALL | NULL | NULL | NULL | NULL | 2 | NULL | 2807 | +----+-------------+-------+------+---------------+------+---------+------+------+-------+ 2808 | 1 row in set 2809 | ``` 2810 | 2811 | | 列 | 含义 | 2812 | | ------------- | -------------------------------------------- | 2813 | | id | 查询序号,序号越大越先执行,一样则按顺序执行 | 2814 | | select_type | 查询类型,SIMPLE、PRIMARY、UNION、SUBQUERY等 | 2815 | | table | 表名 | 2816 | | type | join类型,const,eq_ref,ref等 | 2817 | | possible_keys | join类型 | 2818 | | key | 实际选择的索引 | 2819 | | ken_len | 索引的长度 | 2820 | | ref | 与索引作比较的列 | 2821 | | rows | 要检索的行数 | 2822 | | Extra | 额外信息 | 2823 | 2824 | 2825 | 2826 | **16、最左前缀!!联合索引B+树是如何建立的?是如何查询的?当where子句中出现>时,联合索引命中是如何的? ** 2827 | 2828 | ​ 最左前缀原则主要使用在联合索引中,联合索引的B+Tree是按照第一个关键字进行索引排列的。 2829 | 2830 | ​ 联合索引的底层是一颗B+树,那么联合索引的底层也就是一颗B+树,只不过联合索引的B+树节点中存储的是键值。由于构建一棵B+树只能根据一个值来确定索引关系,所以数据库依赖联合索引最左的字段来构建。 2831 | 2832 | ​ 采用>、<等进行匹配都会导致后面的列无法走索引,因为通过以上方式匹配到的数据是不可知的。 2833 | 2834 | 2835 | 2836 | **17、MySQL中一条SQL语句的执行过程** 2837 | 2838 | 参考:https://zhuanlan.zhihu.com/p/126722329 2839 | 2840 | **查询语句:** 2841 | 2842 | ```mysql 2843 | select * from student A where A.age='18' and A.name='张三'; 2844 | ``` 2845 | 2846 | 结合上面的说明,我们分析下这个语句的执行流程: 2847 | 2848 | - 先检查该语句是否有权限,如果没有权限,直接返回错误信息,如果有权限,在mysql8.0版本以前,会先查询缓存,以这条sql语句为key在内存中查询是否有结果,如果有直接缓存,如果没有,执行下一步。 2849 | - 通过分析器进行词法分析,提取sql语句的关键元素,比如提取上面这个语句是查询select,提取需要查询的表名为tb_student,需要查询所有的列,查询条件是这个表的id='1'。然后判断这个sql语句是否有语法错误,比如关键词是否正确等等,如果检查没问题就执行下一步。 2850 | - 接下来就是优化器进行确定执行方案,上面的sql语句,可以有两种执行方案: a.先查询学生表中姓名为“张三”的学生,然后判断是否年龄是18。 b.先找出学生中年龄18岁的学生,然后再查询姓名为“张三”的学生。 那么优化器根据自己的优化算法进行选择执行效率最好的一个方案(优化器认为,有时候不一定最好)。那么确认了执行计划后就准备开始执行了。 2851 | - 进行权限校验,如果没有权限就会返回错误信息,如果有权限就会调用数据库引擎接口,返回引擎的执行结果。 2852 | 2853 | **更新语句:** 2854 | 2855 | ```mysql 2856 | update student A set A.age='19' where A.name='张三'; 2857 | ``` 2858 | 2859 | 我们来给张三修改下年龄,在实际数据库肯定不会设置年龄这个字段的,不然要被技术负责人打的。其实这条语句也基本上会沿着上一个查询的流程走,只不过执行更新的时候肯定要记录日志啦,这就会引入日志模块了,mysql 自带的日志模块式binlog(归档日志),所有的存储引擎都可以使用,我们常用的InnoDB引擎还自带了一个日志模块redo log,我们就以InnoDB模式下来探讨这个语句的执行流程。流程如下: 2860 | 2861 | - 先查询到张三这一条数据,如果有缓存,也是会用到缓存。 2862 | - 然后拿到查询的语句,把 age 改为19,然后调用引擎API接口,写入这一行数据,InnoDB引擎把数据保存在内存中,同时记录redo log,此时redo log进入prepare状态,然后告诉执行器,执行完成了,随时可以提交。 2863 | - 执行器收到通知后记录binlog,然后调用引擎接口,提交redo log 为提交状态。 2864 | - 更新完成。 2865 | 2866 | 2867 | 2868 | **18、数据库几大范式** 2869 | 2870 | 参考:[数据库范式](https://blog.csdn.net/w__yi/article/details/19934319?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param) 2871 | 2872 | **第一范式(1NF)列不可分割** 2873 | 2874 | **第二范式(2NF)属性完全依赖于主键 [ 消除部分子函数依赖 ]** 2875 | 2876 | **第三范式(3NF)属性不依赖于其它非主属性 [ 消除传递依赖 ]** 2877 | 2878 | 2879 | 2880 | **19、left join,right join,inner join,outer join的含义及区别** 2881 | 2882 | ![](https://image-static.segmentfault.com/276/780/2767807589-5c122586a23c4_articlex) 2883 | 2884 | left join(左联接) 返回包括左表中的所有记录和右表中关联字段相等的记录 2885 | 2886 | right join(右联接) 返回包括右表中的所有记录和左表中关联字段相等的记录 2887 | 2888 | inner join(等值连接) 只返回两个表中关联字段相等的行 2889 | 2890 | 2891 | 2892 | **20、mysql主从复制过程,binlog记录格式,异步复制、同步复制、半同步复制模式区别** 2893 | 2894 | **MySQl主从复制:** 2895 | 2896 | - **原理**:将主服务器的binlog日志复制到从服务器上执行一遍,达到主从数据的一致状态。 2897 | - **过程**:从库开启一个I/O线程,向主库请求Binlog日志。主节点开启一个binlog dump线程,检查自己的二进制日志,并发送给从节点;从库将接收到的数据保存到中继日志(Relay log)中,另外开启一个SQL线程,把Relay中的操作在自身机器上执行一遍 2898 | - **优点**: 2899 | - 作为备用数据库,并且不影响业务 2900 | - 可做读写分离,一般是一个写库,一个或多个读库,分布在不同的服务器上,充分发挥服务器和数据库的性能,但要保证数据的一致性 2901 | 2902 | **binlog记录格式:**statement、row、mixed 2903 | 2904 | ​ 基于语句statement的复制、基于行row的复制、基于语句和行(mix)的复制。其中基于row的复制方式更能保证主从库数据的一致性,但日志量较大,在设置时考虑磁盘的空间问题 2905 | 2906 | **异步复制:** 2907 | 2908 | ​ 在异步复制中,主库执行完操作后,写入binlog日志后,就返回客户端,这一动作就结束了,并不会验证从库有没有收到,完不完整,所以这样可能会造成数据的不一致。 2909 | 2910 | **半同步复制:** 2911 | 2912 | ​ 当主库每提交一个事务后,不会立即返回,而是等待其中一个从库接收到Binlog并成功写入Relay-log中才返回客户端,所以这样就保证了一个事务至少有两份日志,一份保存在主库的Binlog,另一份保存在其中一个从库的Relay-log中,从而保证了数据的安全性和一致性。 2913 | 2914 | **全同步复制:** 2915 | 2916 | ​ 指当主库执行完一个事务,所有的从库都执行了该事务才返回给客户端。因为需要等待所有从库执行完该事务才能返回,所以全同步复制的性能必然会收到严重的影响。 2917 | 2918 | 2919 | 2920 | **21、主从复制或读写分离等数据不一致性问题以及如何解决** 2921 | 2922 | "主从复制有延时",这个延时期间读取从库,可能读到不一致的数据。 2923 | 2924 | **半同步复制法:** 2925 | 2926 | ​ 当主库每提交一个事务后,不会立即返回,而是等待其中一个从库接收到Binlog并成功写入Relay-log中才返回客户端,所以这样就保证了一个事务至少有两份日志,一份保存在主库的Binlog,另一份保存在其中一个从库的Relay-log中,从而保证了数据的安全性和一致性。 2927 | 2928 | **全同步复制法:** 2929 | 2930 | ​ 指当主库执行完一个事务,所有的从库都执行了该事务才返回给客户端。因为需要等待所有从库执行完该事务才能返回,所以全同步复制的性能必然会收到严重的影响。 2931 | 2932 | **缓存记录写key法:** 2933 | 2934 | ​ 在cache里记录哪些记录发生过的写请求,来路由读主库还是读从库 2935 | 2936 | 2937 | 2938 | **22、银行的话,可能会考mysql数据类型,如余额要用decimal** 2939 | 2940 | ​ 2941 | 2942 | ## **六、Redis篇** 2943 | 2944 | **1、为什么使用Redis** 2945 | 2946 | ​ 速度快,完全基于内存,使用C语言实现,网络层使用epoll解决高并发问题,单线程模型避免了不必要的上下文切换及竞争条件; 2947 | 2948 | ​ 与传统数据库不同的是 Redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。另外,Redis 也经常用来做分布式锁。除此之外,Redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。 2949 | 2950 | ​ 2951 | 2952 | **2、分布式缓存和本地缓存有啥区别?让你自己设计本地缓存怎么设计?如何解决缓存过期问题?如何解决内存溢出问题?** 2953 | 2954 | ​ 分布式缓存一致性更好一点,用于集群环境下多节点使用同一份缓存的情况;有网络IO,吞吐率与缓存的数据大小有较大关系; 2955 | 2956 | ​ 本地缓存非常高效,本地缓存会占用堆内存,影响垃圾回收、影响系统性能。 2957 | 2958 | **本地缓存设计:** 2959 | 2960 | ​ 以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况,每个实例都需要各自保存一份缓存,缓存不具有一致性。 2961 | 2962 | **解决缓存过期:** 2963 | 2964 | ​ 1、将缓存过期时间调为永久 2965 | 2966 | ​ 2、将缓存失效时间分散开,不要将缓存时间长度都设置成一样;比如我们可以在原有的失效时间基础上增加一个随机值,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。 2967 | 2968 | **解决内存溢出:** 2969 | 2970 | ​ **第一步**,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。) 2971 | 2972 |  **第二步**,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。 2973 | 2974 |  **第三步**,对代码进行走查和分析,找出可能发生内存溢出的位置。 2975 | 2976 | 2977 | 2978 | **3、redis和*Mem*cached的区别** 2979 | 2980 | | redis | Memcached | 2981 | | ------------------------------------- | -------------------------------------------- | 2982 | | 内存高速数据库 | 高性能分布式内存缓存数据库,可缓存图片、视频 | 2983 | | 支持hash、list、set、zset、string结构 | 只支持key-value结构 | 2984 | | 将大部分数据放到内存 | 全部数据放到内存中 | 2985 | | 支持持久化、主从复制备份 | 不支持数据持久化及数据备份 | 2986 | | 数据丢失可通过AOF恢复 | 挂掉后,数据不可恢复 | 2987 | 2988 | **使用场景:** 2989 | 2990 | ​ 1、如果有持久方面的需求或对数据类型和处理有要求的应该选择redis。 2991 | ​ 2、如果简单的key/value 存储应该选择memcached。 2992 | 2993 | 2994 | 2995 | **4、redis常用数据结构和使用场景** 2996 | 2997 | Redis主要有5种数据类型,包括String,List,Set,Zset,Hash 2998 | 2999 | | 类型 | 存储值 | 应用场景 | 3000 | | ------ | -------------------- | -------------------------------------------- | 3001 | | String | 字符串、整数、浮点数 | 简单的键值对缓存 | 3002 | | List | 列表 | 存储列表型数据结构,例如:评论列表、商品列表 | 3003 | | Set | 无序集合 | 适合交集、并集、查集操作,例如朋友关系 | 3004 | | Zset | 有序集合 | 去重后排序,适合排名场景 | 3005 | | Hash | 哈希 | 结构化数据,比如存储对象 | 3006 | 3007 | **Redis的应用场景:** 3008 | 3009 | **计数器** 3010 | 3011 | ​ 可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。 3012 | 3013 | **缓存** 3014 | 3015 | ​ 将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。 3016 | 3017 | **会话缓存** 3018 | 3019 | ​ 可以使用 Redis 来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。 3020 | 3021 | **其它** 3022 | 3023 | ​ Set 可以实现交集、并集等操作,从而实现共同好友等功能。ZSet 可以实现有序性操作,从而实现排行榜等功能。 3024 | 3025 | 3026 | 3027 | **5、Zset底层实现?跳表搜索插入删除过程?** 3028 | 3029 | ​ 跳表(skip List)是一种随机化的数据结构,基于并联的链表,实现简单,插入、删除、查找的复杂度均为O(logN)。简单说来跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能,正是这个跳跃的功能,使得在查找元素时,跳表能够提供O(logN)的时间复杂度 3030 | 3031 | ​ Zset数据量少的时候使用压缩链表ziplist实现,有序集合使用紧挨在一起的压缩列表节点来保存,第一个节点保存member,第二个保存score。ziplist内的集合元素按score从小到大排序,score较小的排在表头位置。 3032 | 3033 | ​ 数据量大的时候使用跳跃列表skiplist和哈希表hash_map结合实现,查找删除插入的时间复杂度都是O(logN) 3034 | 3035 | **搜索** 3036 | 3037 | ​ 跳跃表按 score 从小到大保存所有集合元素,查找时间复杂度为平均 *O(logN),最坏 O(N) 。* 3038 | 3039 | **插入** 3040 | 3041 |   之前就说了,之所以选用链表作为底层结构支持,也是为了高效地动态增删。单链表在知道删除的节点是谁时,时间复杂度为O(1),因为跳表底层的单链表是有序的,为了维护这种有序性,在插入前需要遍历链表,找到该插入的位置,单链表遍历查找的时间复杂度是O(n),同理可得,跳表的遍历也是需要遍历索引数,所以是O(logn)。 3042 | 3043 | **删除** 3044 | 3045 |   删除的节点要分两种情况,如果该节点还在索引中,那删除时不仅要删除单链表中的节点,还要删除索引中的节点;另一种情况是删除的节点只在链表中,不在索引中,那只需要删除链表中的节点即可。但针对单链表来说,删除时都需要拿到前驱节点才可改变引用关系从而删除目标节点。 3046 | 3047 | 3048 | 3049 | 3050 | 3051 | **6、redis过期淘汰策略** 3052 | 3053 | **1)全局的键空间选择性移除** 3054 | 3055 | ​ **noeviction**:当内存不足以容纳新写入数据时,新写入操作会报错。 3056 | 3057 | ​ **allkeys-lru**:在键空间中,移除最近最少使用的key。(这个是最常用的) 3058 | 3059 | ​ **allkeys-random**:在键空间中,随机移除某个key。 3060 | 3061 | **2)设置过期时间的键空间选择性移除** 3062 | 3063 | ​ **volatile-lru**:在设置了过期时间的键空间中,移除最近最少使用的key。 3064 | 3065 | ​ **volatile-random**:在设置了过期时间的键空间中,随机移除某个key。 3066 | 3067 | ​ **volatile-ttl**:在设置了过期时间的键空间中,有更早过期时间的key优先移除。 3068 | 3069 | **总结** 3070 | 3071 | ​ Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。 3072 | 3073 | 3074 | 3075 | **7、redis持久化机制?都有什么优缺点?持久化的时候还能接受请求吗?** 3076 | 3077 | 持久化就是把内存中的数据持久化到本地磁盘,防止服务器宕机了内存数据丢失 3078 | 3079 | Redis 提供两种持久化机制 **RDB(默认)** 和 **AOF 机制** 3080 | 3081 | **RDB:**是Redis DataBase缩写快照 3082 | 3083 | ​ RDB是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。 3084 | 3085 | ​ **优点:** 3086 | 3087 | ​ 1)只有一个文件 dump.rdb,方便持久化; 3088 | 3089 | ​ 2)容灾性好,一个文件可以保存到安全的磁盘。 3090 | 3091 | ​ 3)性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能。 3092 | 3093 | ​ 4)相对于数据集大时,比 AOF 的启动效率更高。 3094 | 3095 | ​ **缺点:** 3096 | 3097 | ​ 1)数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候 3098 | 3099 | **AOF:持久化** 3100 | 3101 | ​ AOF持久化(即Append Only File持久化),则是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。 3102 | 3103 | ​ **优点:** 3104 | 3105 | ​ 1)数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次 命令操作就记录到 aof 文件中一次。 3106 | 3107 | ​ 2)通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。 3108 | 3109 | **缺点:** 3110 | 3111 | ​ 1)AOF 文件比 RDB 文件大,且恢复速度慢。 3112 | 3113 | ​ 2)数据集大的时候,比 rdb 启动效率低。 3114 | 3115 | 3116 | 3117 | **8、redis事务** 3118 | 3119 | ​ 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。 3120 | 3121 | **Redis事务的概念** 3122 | 3123 | ​ Redis 事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。 3124 | 3125 | Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的 3126 | 3127 | Redis会将一个事务中的所有命令序列化,然后按顺序执行。 3128 | 3129 | Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。 3130 | 3131 | **事务命令:** 3132 | 3133 | **MULTI:**用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。 3134 | 3135 | **EXEC:**执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。 3136 | 3137 | **WATCH :**是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。 3138 | 3139 | **DISCARD:**调用该命令,客户端可以清空事务队列,并放弃执行事务,且客户端会从事务状态中退出。 3140 | 3141 | **UNWATCH**:命令可以取消watch对所有key的监控。 3142 | 3143 | 3144 | 3145 | **9、缓存雪崩和缓存穿透,以及解决方法** 3146 | 3147 | **【1】缓存雪崩:** 3148 | 3149 | ​ 指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。 3150 | 3151 | ​ **解决方案:** 3152 | 3153 | ​ 1)缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。 3154 | 3155 | ​ 2)一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。 3156 | 3157 | ​ 3)给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存。 3158 | 3159 | **【2】缓存穿透:** 3160 | 3161 | ​ 缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。 3162 | 3163 | ​ **解决方案:** 3164 | 3165 | ​ 1)接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截; 3166 | 3167 | ​ 2)从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击; 3168 | 3169 | ​ 3)采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。 3170 | 3171 | **【3】缓存击穿:** 3172 | 3173 | ​ 这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库 3174 | 3175 | ​ **解决方案:** 3176 | 3177 | ​ 1)设置热点数据永远不过期 3178 | 3179 | ​ 2)加互斥锁,互斥锁 3180 | 3181 | 3182 | 3183 | 3184 | 3185 | **10、如何保证缓存和数据库的数据一致性?** 3186 | 3187 | **方式一:** 3188 | 3189 | ​ 读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。 3190 | 3191 | **方式二:** 3192 | 3193 | ​ 先更新数据库,假如读缓存失败,先读数据库,再回写缓存的方式实现 3194 | 3195 | 3196 | 3197 | **11、redis是单线程还是多线程?为什么那么快?** 3198 | 3199 | redis是单线程,快的原因: 3200 | 3201 | ​ 1)完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1); 3202 | 3203 | ​ 2)数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的; 3204 | 3205 | ​ 3)采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗; 3206 | 3207 | ​ 4)使用多路 I/O 复用模型,非阻塞 IO; 3208 | 3209 | ​ 5)使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求; 3210 | 3211 | 3212 | 3213 | **12、五种IO模型的区别** 3214 | 3215 | **阻塞I/O:** 3216 | 3217 | ​ 当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。 3218 | 3219 | **非阻塞I/O:** 3220 | 3221 | ​ 在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。 3222 | 3223 | **多路复用I/O(select和poll):** 3224 | 3225 | ​ IO多路转接是多了一个select函数,select函数有一个参数是文件描述符集合,对这些文件描述符进行循环监听,当某个文件描述符就绪时,就对这个文件描述符进行处理。其中,select只负责等,recvfrom只负责拷贝。 IO多路转接是属于阻塞IO,但可以对多个文件描述符进行阻塞监听,所以效率较阻塞IO的高。 3226 | 3227 | **信号驱动I/O(SIGIO):** 3228 | 3229 | ​ 信号驱动IO模型,应用进程告诉内核:当数据报准备好的时候,给我发送一个信号,对SIGIO信号进行捕捉,并且调用我的信号处理函数来获取数据报。 3230 | 3231 | **异步I/O(Posix.1的aio_系列函数):** 3232 | 3233 | ​ 当应用程序调用aio_read时,内核一方面去取数据报内容返回,另一方面将程序控制权还给应用进程,应用进程继续处理其他事情,是一种非阻塞的状态。当内核中有数据报就绪时,由内核将数据报拷贝到应用程序中,返回aio_read中定义好的函数处理程序。 3234 | 3235 | 3236 | 3237 | **可以看出,阻塞程度:阻塞IO>非阻塞IO>多路转接IO>信号驱动IO>异步IO,效率是由低到高的**。 3238 | 3239 | 3240 | 3241 | 3242 | 3243 | **13、select、poll、epoll的区别?** 3244 | 3245 | 参考:https://www.jianshu.com/p/dfd940e7fca2 3246 | 3247 | **select** 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。 3248 | 3249 | **poll **本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。 3250 | 3251 | **epoll **支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。 3252 | 3253 | 3254 | 3255 | **14、redis热key问题?如何发现以及如何解决?** 3256 | 3257 | ​ 缓存中的一个Key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。 3258 | 3259 | **解决方案:** 3260 | 3261 | ​ 对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询 3262 | 3263 | 3264 | 3265 | **15、redis数据分布方式?有什么优点?一致性hash呢?** 3266 | 3267 | **Hash:** 3268 | 3269 | ​ 客户端分片:哈希+取余 3270 | 3271 | ​ 节点伸缩:数据节点关系变化,导致数据迁移 3272 | 3273 | ​ 迁移数量和添加节点数量有关:建议翻倍扩容 3274 | 3275 | ​ 一个简单直观的想法是直接用Hash来计算,以Key做哈希后对节点数取模。可以看出,在key足够分散的情况下,均匀性可以获得,但一旦有节点加入或退出,所有的原有节点都会受到影响,稳定性无从谈起。 3276 | 3277 | **一致性Hash:** 3278 | 3279 | ​ 客户端分片:哈希+顺时针(优化取余) 3280 | 3281 | ​ 节点伸缩:只影响邻近节点,但是还是有数据迁移 3282 | 3283 | ​ 翻倍伸缩:保证最小迁移数据和负载均衡 3284 | 3285 | ​ 一致性Hash可以很好的解决稳定问题,可以将所有的存储节点排列在收尾相接的Hash环上,每个key在计算Hash后会顺时针找到先遇到的一组存储节点存放。而当有节点加入或退出时,仅影响该节点在Hash环上顺时针相邻的后续节点,将数据从该节点接收或者给予。但这有带来均匀性的问题,即使可以将存储节点等距排列,也会在**存储节点个数变化时带来数据的不均匀**。而这种可能成倍数的不均匀在实际工程中是不可接受的。 3286 | 3287 | 3288 | 3289 | **16、redis主从复制** 3290 | 3291 | **主从复制原理:** 3292 | 3293 | 当启动一个 slave node 的时候,它会发送一个 PSYNC 命令给 master node。 3294 | 3295 | 如果这是 slave node 初次连接到 master node,那么会触发一次 full resynchronization 全量复制。此时 master 会启动一个后台线程,开始生成一份 RDB 快照文件, 3296 | 3297 | 同时还会将从客户端 client 新收到的所有写命令缓存在内存中。RDB 文件生成完毕后, master 会将这个 RDB 发送给 slave,slave 会先写入本地磁盘,然后再从本地磁盘加载到内存中, 3298 | 3299 | 接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。 3300 | 3301 | slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。 3302 | 3303 | **过程原理** 3304 | 3305 | ​ 1、当从库和主库建立MS关系后,会向主数据库发送SYNC命令 3306 | 3307 | ​ 2、主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令缓存起来 3308 | 3309 | ​ 3、当快照完成后,主Redis会将快照文件和所有缓存的写命令发送给从Redis 3310 | 3311 | ​ 4、从Redis接收到后,会载入快照文件并且执行收到的缓存的命令 3312 | 3313 | ​ 5、之后,主Redis每当接收到写命令时就会将命令发送从Redis,从而保证数据的一致 3314 | 3315 | **缺点** 3316 | 3317 | ​ 所有的slave节点数据的复制和同步都由master节点来处理,会照成master节点压力太大,使用主从从结构来解决 3318 | 3319 | ## **七、Spring 篇** 3320 | 3321 | **1、Spring IOC** 3322 | 3323 | ​ IoC(Inverse of Control:控制反转)是⼀种设计思想,就是 将原本在程序中⼿动创建对象的控制 3324 | 权,交由Spring框架来管理。 IoC 在其他语⾔中也有应⽤,并⾮ Spring 特有。 3325 | 3326 | ​ IoC 容器是 Spring⽤来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注⼊。这样可以很⼤程度上简化应⽤的开发,把应⽤从复杂的依赖关系中解放出来。 IoC 容器就像是⼀个⼯⼚⼀样,当我们需要创建⼀个对象的时候,只需要配置好配置⽂件/注解即可,完全不⽤考虑对象是如何被创建出来的。 3327 | 3328 | 3329 | 3330 | **2、Spring AOP,动态代理** 3331 | 3332 | ​ AOP(Aspect-Oriented Programming:⾯向切⾯编程)能够将那些与业务⽆关,却为业务模块所共同调⽤ 3333 | 的逻辑或责任(例如事务处理、⽇志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模 3334 | 块间的耦合度,并有利于未来的可拓展性和可维护性。 3335 | 3336 | ​ Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接⼝,那么Spring AOP会使⽤JDK 3337 | Proxy,去创建代理对象,⽽对于没有实现接⼝的对象,就⽆法使⽤ JDK Proxy 去进⾏代理了,这时候 3338 | Spring AOP会使⽤Cglib ,这时候Spring AOP会使⽤ Cglib ⽣成⼀个被代理对象的⼦类来作为代理, 3339 | 3340 | 3341 | 3342 | **3、Bean生命周期** 3343 | 3344 | **单例对象:** singleton 3345 | 3346 | 出生:当容器创建时对象出生 3347 | 3348 | 活着:只要容器还在,对象一直活着 3349 | 3350 | 死亡:容器销毁,对象消亡 3351 | 3352 | 总结:单例对象的生命周期和容器相同 3353 | 3354 | **多例对象:** prototype 3355 | 3356 | 出生: 使用对象时spring框架为我们创建 3357 | 3358 | 活着:对象只要是在使用过程中就一直活着 3359 | 3360 | 死亡:当对象长时间不用且没有其它对象引用时,由java的垃圾回收机制回收 3361 | 3362 | 3363 | 3364 | **4、Bean作用域?默认什么级别?是否线程安全?Spring如何保障线程安全的?** 3365 | 3366 | | 名称 | | 3367 | | -------------- | ------------------------------------------------------------ | 3368 | | singleton | 单例对象,默认值的作用域 | 3369 | | prototype | 每次获取都会创建⼀个新的 bean 实例 | 3370 | | request | 每⼀次HTTP请求都会产⽣⼀个新的bean,该bean仅在当前HTTP request内有效。 | 3371 | | session | 在一次 HTTP session 中,容器将返回同一个实例 | 3372 | | global-session | 将对象存入到web项目集群的session域中,若不存在集群,则global session相当于session | 3373 | 3374 | 默认作用域是singleton,多个线程访问同一个bean时会存在线程不安全问题 3375 | 3376 | **保障线程安全方法:** 3377 | 3378 | 1. 在Bean对象中尽量避免定义可变的成员变量(不太现实)。 3379 | 3380 | 2. 在类中定义⼀个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中 3381 | 3382 | **ThreadLocal**: 3383 | 3384 | ​ 每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。 3385 | 3386 | ​ 将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后**在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象**,避免了将这个对象作为参数传递的麻烦。 3387 | 3388 | 3389 | 3390 | **5、Spring事务隔离级别和事务传播属性** 3391 | 3392 | **隔离级别:** 3393 | 3394 | **1) DEFAULT (默认)** 3395 | 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别。另外四个与JDBC的隔离级别相对应。 3396 | 3397 | **2) READ_UNCOMMITTED (读未提交)** 3398 | 这是事务最低的隔离级别,它允许另外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。 3399 | 3400 | **3) READ_COMMITTED (读已提交)** 3401 | 保证一个事务修改的数据提交后才能被另外一个事务读取,另外一个事务不能读取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻像读。 3402 | 3403 | **4) REPEATABLE_READ (可重复读)** 3404 | 这种事务隔离级别可以防止脏读、不可重复读,但是可能出现幻像读。它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了不可重复读。 3405 | 3406 | **5) SERIALIZABLE(串行化)** 3407 | 这是花费最高代价但是最可靠的事务隔离级别,事务被处理为顺序执行。除了防止脏读、不可重复读外,还避免了幻像读。 3408 | 3409 | 3410 | 3411 | **Spring事务传播属性(Propagation):** 3412 | 3413 | **1) REQUIRED(默认属性)** 3414 | 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。 被设置成这个级别时,会为每一个被调用的方法创建一个逻辑事务域。如果前面的方法已经创建了事务,那么后面的方法支持当前的事务,如果当前没有事务会重新建立事务。 3415 | 3416 | **2) MANDATORY** 3417 | 支持当前事务,如果当前没有事务,就抛出异常。 3418 | 3419 | **3) NEVER** 3420 | 以非事务方式执行,如果当前存在事务,则抛出异常。 3421 | 3422 | **4) NOT_SUPPORTED** 3423 | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 3424 | 3425 | **5) REQUIRES_NEW** 3426 | 新建事务,如果当前存在事务,把当前事务挂起。 3427 | 3428 | **6) SUPPORTS** 3429 | 支持当前事务,如果当前没有事务,就以非事务方式执行。 3430 | 3431 | **7) NESTED** 3432 | 支持当前事务,新增Savepoint点,与当前事务同步提交或回滚。 嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。 3433 | 3434 | 3435 | 3436 | **6、Spring以及Spring MVC常见注解** 3437 | 3438 | ***Spring部分:*** 3439 | 3440 | ​ **声明bean的注解** 3441 | 3442 | ​ **@Component** 通⽤的注解,可标注任意类为 Spring 组件 3443 | 3444 | ​ **@Service** 在业务逻辑层使用(service层) 3445 | 3446 | ​ **@Repository** 在数据访问层使用(dao层) 3447 | 3448 | ​ **@Controller** 在展现层使用,控制器的声明(controller层) 3449 | 3450 | ​ **注入bean的注解** 3451 | 3452 | ​ **@Autowired**:可以对类成员变量、方法、构造方法进行标注 3453 | 3454 | ​ 默认按照类型注入,若要按照名称注入,需要搭配**@Qualifier**注解一起使用 3455 | 3456 | ​ **@Resource**:默认按照名称来装配注入 3457 | 3458 | **Spring MVC部分:** 3459 | 3460 | ​ **@Controller** 声明该类为SpringMVC中的Controller 3461 | 3462 | ​ **@RequestMapping** 用于映射Web请求 3463 | 3464 | ​ **@ResponseBody** 支持将返回值放在response内,而不是一个页面,通常用户返回json数据 3465 | 3466 | ​ **@RequestBody** 允许request的参数在request体中,而不是在直接连接在地址后面。 3467 | 3468 | ​ **@PathVariable** 用于接收路径参数,比如@RequestMapping("/hello/{name}")申明的路径,将注解放在参数中前,即可获取该值,通常作为Restful的接口实现方法。 3469 | 3470 | 3471 | 3472 | **7、@autowired和@resource的区别?** 3473 | 3474 | **@Autowired**:可以对类成员变量、方法、构造方法进行标注 3475 | 3476 | ​ 默认按照类型注入,若要按照名称注入,需要搭配**@Qualifier**注解一起使用 3477 | 3478 | **@Resource**:默认按照名称来装配注入 3479 | 3480 | 3481 | 3482 | **8、mybatis如何防止sql注入?$和#的区别是什么?传入表名用哪个?** 3483 | 3484 | **防止sql注入:** 3485 | 3486 | ​ 在编写mybatis的映射语句时,尽量采用“#{xxx}”这样的格式 3487 | 3488 | **#和$区别:** 3489 | 3490 | | # | $ | 3491 | | ------------------------------------------------------- | ----------------------------------- | 3492 | | 相当于对数据加上双引号 | 相当于直接显示数据 | 3493 | | 很大程度上防止SQL注入 | 无法防止SQL注入 | 3494 | | #{xxx},使用的是PreparedStatement,会有类型转换,比较安全 | ${xxx},使用字符串拼接,容易SQL注入 | 3495 | 3496 | ​ 简单的说就是#{}是经过预编译的,是安全的,**$**{}是未经过预编译的,仅仅是取变量的值,是非安全的,存在SQL注入。 3497 | 3498 | 要实现动态传入表名、列名,需要做如下修改: 3499 | 3500 | **添加属性statementType="STATEMENT"**,**同时sql里的属有变量取值都改成${xxxx}** 3501 | 3502 | 3503 | 3504 | **9、Spring MVC工作原理** 3505 | 3506 | ![](https://img-blog.csdn.net/20181022224058617?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2F3YWtlX2xxaA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) 3507 | 3508 | 1. 客户端(浏览器)发送请求,直接请求到 DispatcherServlet 。 3509 | 2. DispatcherServlet 根据请求信息调⽤ HandlerMapping ,解析请求对应的 Handler 。 3510 | 3. 解析到对应的 Handler (也就是 Controller 控制器)后,开始由HandlerAdapter 适配器处理。 3511 | 4. HandlerAdapter 会根据 Handler 来调⽤真正的处理器开处理请求,并处理相应的业务逻辑。 3512 | 5. 处理器处理完业务后,会返回⼀个 ModelAndView 对象, Model 是返回的数据对象 3513 | 6. ViewResolver 会根据逻辑 View 查找实际的 View 。 3514 | 7. DispaterServlet 把返回的 Model 传给 View (视图渲染)。 3515 | 8. 把 View 返回给请求者(浏览器) 3516 | 3517 | 3518 | 3519 | **10、SpringBoot自动配置的原理是什么?介绍SpringBootApplication注解.** 3520 | 3521 | 启动类: 3522 | 3523 | ```java 3524 | @SpringBootApplication 3525 | public class JpaApplication { 3526 | public static void main(String[] args) { 3527 | SpringApplication.run(JpaApplication.class, args); 3528 | } 3529 | } 3530 | ``` 3531 | 3532 | 它主要加载了@SpringBootApplication注解主配置类,这个@SpringBootApplication注解主配置类里边最主要的功能就是SpringBoot开启了一个@EnableAutoConfiguration注解的自动配置功能。 3533 | 3534 | **@EnableAutoConfiguration作用:** 3535 | 3536 | 它主要利用了一个 3537 | 3538 | EnableAutoConfigurationImportSelector选择器给Spring容器中来导入一些组件。 3539 | 3540 | ```java 3541 | @Import(EnableAutoConfigurationImportSelector.class) 3542 | public @interface EnableAutoConfiguration 3543 | ``` 3544 | 3545 | 3546 | 3547 | **@SpringBootApplication**注解等同于下面三个注解: 3548 | 3549 | - **@SpringBootConfiguration:** 底层是**Configuration**注解,说白了就是支持**JavaConfig**的方式来进行配置 3550 | - **@EnableAutoConfiguration:**开启**自动配置**功能 3551 | - **@ComponentScan:**就是**扫描**注解,默认是扫描**当前类下**的package 3552 | 3553 | 其中`@EnableAutoConfiguration`是关键(启用自动配置),内部实际上就去加载`META-INF/spring.factories`文件的信息,然后筛选出以`EnableAutoConfiguration`为key的数据,加载到IOC容器中,实现自动配置功能! 3554 | 3555 | 3556 | 3557 | **11、Mybatis和Hibernate的区别** 3558 | 3559 | **Hibernate 框架:** 3560 | 3561 | ​ **Hibernate**是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,建立对象与数据库表的映射。是一个全自动的、完全面向对象的持久层框架。 3562 | 3563 | **Mybatis框架:** 3564 | 3565 | ​ **Mybatis**是一个开源对象关系映射框架,原名:ibatis,2010年由谷歌接管以后更名。是一个半自动化的持久层框架。 3566 | 3567 | **区别:** 3568 | 3569 | **开发方面** 3570 | 3571 | ​ 在项目开发过程当中,就速度而言: 3572 | 3573 | ​ hibernate开发中,sql语句已经被封装,直接可以使用,加快系统开发; 3574 | 3575 | ​ Mybatis 属于半自动化,sql需要手工完成,稍微繁琐; 3576 | 3577 | ​ 但是,凡事都不是绝对的,如果对于庞大复杂的系统项目来说,复杂语句较多,hibernate 就不是好方案。 3578 | 3579 | **sql优化方面** 3580 | 3581 | ​ Hibernate 自动生成sql,有些语句较为繁琐,会多消耗一些性能; 3582 | 3583 | ​ Mybatis 手动编写sql,可以避免不需要的查询,提高系统性能; 3584 | 3585 | **对象管理比对** 3586 | 3587 | ​ Hibernate 是完整的对象-关系映射的框架,开发工程中,无需过多关注底层实现,只要去管理对象即可; 3588 | 3589 | ​ Mybatis 需要自行管理 映射关系; 3590 | 3591 | 3592 | 3593 | **12、spring中的@Autowired注解原理?** 3594 | 3595 | ​ @Autowired的使用简化了我们的开发,其原理是使用 AutowiredAnnotationBeanPostProcessor 类来实现,该类实现了 Spring 框架的一些扩展接口,通过实现 BeanFactoryAware 接口使其内部持有了 BeanFactory(可轻松的获取需要依赖的的 Bean);通过实现 MergedBeanDefinitionPostProcessor 扩展接口,在 BeanFactory 里面的每个 Bean 实例化前获取到每个 Bean 里面的 @Autowired 信息并缓存下来;通过实现 Spring 框架的 postProcessPropertyValues 扩展接口在 BeanFactory 里面的每个 Bean 实例后从缓存取出对应的注解信息,获取依赖对象,并通过反射设置到 Bean 属性里面。 3596 | 3597 | 3598 | 3599 | **13、Spring中用到了哪些设计模式?单例、工厂、代理、适配、观察者之类的说一说就行** 3600 | 3601 | 参考:[spring中的设计模式](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485303&idx=1&sn=9e4626a1e3f001f9b0d84a6fa0cff04a&chksm=cea248bcf9d5c1aaf48b67cc52bac74eb29d6037848d6cf213b0e5466f2d1fda970db700ba41&token=255050878&lang=zh_CN%23rd) 3602 | 3603 | **单例设计模式 :** Spring 中的 Bean 默认都是单例的。 3604 | 3605 | **⼯⼚设计模式 :** Spring使⽤⼯⼚模式通过 BeanFactory 、 ApplicationContext 创建bean 对象。 3606 | 3607 | **代理设计模式 :** Spring AOP 功能的实现。 3608 | 3609 | **观察者模式:** Spring 事件驱动模型就是观察者模式很经典的⼀个应⽤。 3610 | 3611 | **适配器模式:**Spring AOP 的增强或通知(Advice)使⽤到了适配器模式、spring MVC 中也是⽤到了适配器模式适配 Controller 。 3612 | 3613 | 3614 | 3615 | ## 八、Linux篇 3616 | 3617 | **1、vim怎么查询一个单词,怎么做匹配。** 3618 | 3619 | - 从头搜索:/hello 3620 | 3621 | - 从尾搜索:?hello 3622 | 3623 | - 精确搜搜:"<“表示匹配单词开头,”>“表示匹配单词末尾,匹配完整单词”?“或者”/" 3624 | 如输入"/hel"、"/",可能搜索到hello 3625 | 3626 | 3627 | 3628 | **2.说几个你常用的Linux命令。** 3629 | 3630 | **top命令:** 3631 | 3632 | top程序提供运行系统的动态实时视图,它可以显示系统概要信息以及当前由Linux内核当前管理的任务列表。 3633 | 3634 | **vmstat命令:** 3635 | 3636 | **vmstat**(Virtual Memory Statistics 虚拟内存统计) 命令用来显示Linux系统虚拟内存状态,也可以报告关于进程、内存、I/O等系统整体运行状态。 3637 | 3638 | **ps命令:** 3639 | 3640 | 要对系统中进程进行监测控制,用 ps 命令满足你。 3641 | 3642 | ps 是显示瞬间行程的状态,并不动态连续;如果想对进程运行时间监控,应该用 top 工具。 3643 | 3644 | 3645 | 3646 | **3.怎么判断一个进程的状态,用什么命令。** 3647 | 3648 | ps -l 列出与本次登录有关的进程信息; 3649 | ps -aux 查询内存中进程信息; 3650 | ps -aux | grep *** 查询***进程的详细信息; 3651 | top 查看内存中进程的动态信息; 3652 | kill -9 pid 杀死进程。 3653 | 3654 | 3655 | 3656 | **4.vim怎么跳到最前面,怎么跳到最后面。** 3657 | 3658 | 3659 | 3660 | **5.一个文件太大应该怎么打开。** 3661 | 3662 | 3663 | 3664 | **6.怎么查看cpu,磁盘io,网络io。** 3665 | 3666 | 3667 | 3668 | **7.怎么查看一个文件的大小。** 3669 | 3670 | 3671 | 3672 | **8.怎么看端口占用情况。** 3673 | 3674 | 3675 | 3676 | **9.grep怎么用。** 3677 | 3678 | 3679 | 3680 | **10.怎么看一个进程主要是哪个函数在消耗性能?** 3681 | 3682 | 3683 | 3684 | **11.怎么定位一个300万次中出现一次的错误。** 3685 | 3686 | 3687 | 3688 | **12.怎么杀死一个进程。** 3689 | 3690 | 3691 | 3692 | ## 九、**客观题** 3693 | 3694 | 3695 | 3696 | **1、100亿黑名单URL,每个64B,问这个黑名单要怎么存?判断一个URL是否在黑名单中** 3697 | 3698 | ​ **散列表:** 3699 | 3700 | ​ 如果把黑名单看成一个集合,将其存在 hashmap 中,貌似太大了,需要 640G,明显不科学。 3701 | 3702 | ​ **布隆过滤器:** 3703 | 3704 | ​ 它实际上是一个很长的二进制矢量和一系列随机映射函数。 3705 | 3706 | ​ 它**可以用来判断一个元素是否在一个集合中**。它的优势是只需要占用很小的内存空间以及有着高效的查询效率。对于布隆过滤器而言,它的本质是一个**位数组**:位数组就是数组的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1。 3707 | 3708 | ​ 在数组中的每一位都是二进制位。布隆过滤器除了一个位数组,还有 K 个哈希函数。当一个元素加入布隆过滤器中的时候,会进行如下操作: 3709 | 3710 | - 使用 K 个哈希函数对元素值进行 K 次计算,得到 K 个哈希值。 3711 | - 根据得到的哈希值,在位数组中把对应下标的值置为 1。 3712 | 3713 | 3714 | 3715 | **2、2GB内存在20亿整数中找到出现次数最多的数** 3716 | 3717 | ​ 通常做法是使用哈希表对 出现的每一个数做词频统计,哈希表的key是某个整数,value记录整数出现的次数。本题的数据量是20亿,有可能一个数出现20亿次,则为了避免溢出,哈希表的key是32位(4B),value也是 32位(4B),那么一条哈希表的记录就需要占用8B。 3718 | 3719 | ​ 当哈希表记录数为2亿个时,需要16亿个字节数(8\*2亿),需要至少1.6GB内存(16亿/2^30,1GB== 2 ^30个字节 == 10亿)。则20亿个记录,至少需要16GB的内存,不符合题目要求。 3720 | 3721 | ​ 解决办法是将20亿个数的大文件利用哈希函数分成16个小文件,根据哈希函数可以把20亿条数据均匀分布到16个文件上,同一种数不可能被哈希函数分到不同的小文件上,假设哈希函数够好。然后对每一个小文件用哈希函数来统计其中每种数出现的次数,这样我们就得到16个文件中出现次数最多的数,接着从16个数中选出次数最大的那个key即可。 3722 | 3723 | 3724 | 3725 | **3、40亿个非负整数中找到没有出现的数** 3726 | 3727 | ​ 对于原问题,如果使用哈希表来保存出现过的数,那么最坏情况下是40亿个数都不相同,那么哈希表则需要保存40亿条数据,一个32位整数需要4B,那么40亿*4B = 160亿个字节,一般大概10亿个字节的数据需要1G的空间,那么大概需要16G的空间,这不符合要求。 3728 | 3729 |   我们换一种方式,申请一个bit数组,数组大小为4294967295,大概为40亿bit,40亿/8 = 5亿字节,那么需要0.5G空间, bit数组的每个位置有两种状态0和1,那么怎么使用这个bit数组呢?呵呵,数组的长度刚好满足我们整数的个数范围,那么数组的每个下标值对应4294967295中的一个数,逐个遍历40亿个无符号数,例如,遇到20,则bitArray[20] = 1;遇到666,则bitArray[666] = 1,遍历完所有的数,将数组相应位置变为1。 3730 | 3731 | 3732 | 3733 | **4、40亿个非负整数中找到一个没有出现的数,内存限制10MB** 3734 | 3735 | ​ 本题将内存空间缩小为10MB,对于40亿个数据来说那是明显不够用的,那么我们只有将数据分块处理,分块应该怎么分,分多少块合理呢?根据我做过的题经验来看,10亿个字节的数据大概需要1GB空间处理(如果这个结论不正确欢迎读者指出),那么10MB内存换算过来就是可以处理1千万字节的数据,也就是8千万bit,对于40亿非负整数如果申请bit数组的话,40亿bit / 0.8亿bit = 50,那么这样最少也得分50块来处理,处理每块数据的时候几乎用完了内存空间,这样也不太好。看书上解说是分成了64块,至于为什么是64我目前也不是很了解,我只知道最少50块。所以下面就以64块来进行分析解答吧。 3736 | 3737 |   首先,将0 - 4294967259这个范围平均分成64个区间,每个区间是67108864个数,为了定位更加准确一些,我们先开辟一个大小为64的整型数组intArray,将40亿个数进行区间划分,第0区间(0-67108863)、第一区间(67108864-134217728)、第i区间(67108864*i-67108864*(i+1)-1),......,第63区间(4227858432 - 4294967259)。intArray分别记录每个区间出现的数的个数,肯定至少有一个区间上的计数少于67108864.利用这一点可以快速找出一个没有出现过的数。 3738 | 3739 | ​ 第一次遍历时,先申请长度为64的整型数组countArr[0..63],countArr[i]用来统计区间i 上的数有多少。遍历40亿个数,根据当前数是多少来决定哪一个区间上的计数增加。例如,如果当前数是3422552090,3422552090/67108864=51,所以第51区间上的计数增加countArr[51]++。遍历完40亿个数之后,遍历countArr,必然会有某一个位置上的值(countArr[i])小于67108864,表示第i 区间上至少有一个数没出现过。我们肯定会至少找到一个这样的区间。此时使用的内存就是countArr的大小(64×4B),是非常小的。 3740 | 3741 | 假设我们找到第37区间上的计数小于67108864,以下为第二次遍历的过程: 3742 | 3743 | 1.申请长度为67108864的bit map,这占用大约8MB的空间,记为bitArr[0..67108863]; 3744 | 3745 | 2.再遍历一次40亿个数,此时的遍历只关注落在第37区间上的数,记为num(num/67108864==37),其他区间的数全部忽略。 3746 | 3747 | 3.如果步骤2的num在第37区间上,将bitArr[num - 67108864*37]的值设置为1,也就是只做第37区间上的数的bitArr映射。 3748 | 3749 | 4.遍历完40亿个数之后,在bitArr上必然存在没被设置成1的位置,假设第i 个位置上的值没设置成1,那么67108864×37+i 这个数就是一个没出现过的数。 3750 | 3751 | **总结一下进阶的解法:** 3752 | 3753 | 1.根据10MB的内存限制,确定统计区间的大小,就是第二次遍历时的bitArr大小。 3754 | 3755 | 2.利用区间计数的方式,找到那个计数不足的区间,这个区间上肯定有没出现的数。 3756 | 3757 | 3.对这个区间上的数做bit map映射,再遍历bit map,找到一个没出现的数即可。 3758 | 3759 | 3760 | 3761 | **5、找到100亿个URL中重复的URL** 3762 | 3763 | ​ 原问题的解法使用解决大数据问题的一种常规方法:把大文件通过哈希函数分配到机器,或者通过哈希函数把大文件拆成小文件。一直进行这种划分,直到划分的结果满足资源限制的要求。首先,你要向面试官询问在资源上的限制有哪些,包括内存、计算时间等要求。在明确了限制要求之后,可以将每条URL通过哈希函数分配到若干机器或者拆分成若干小文件,这里的“若干”由具体的资源限制来计算出精确的数量。 3764 | 3765 | ​ 例如,将100亿字节的大文件通过哈希函数分配到100台机器上,然后每一台机器分别统计分给自己的URL中是否有重复的URL,**同时哈希函数的性质决定了同一条URL不可能分给不同的机器;**或者在单机上将大文件通过哈希函数拆成1000个小文件,对每一个小文件再利用哈希表遍历,找出重复的URL;或者在分给机器或拆完文件之后,进行排序,排序过后再看是否有重复的URL出现。总之,牢记一点,很多大数据问题都离不开分流,要么是哈希函数把大文件的内容分配给不同的机器,要么是哈希函数把大文件拆成小文件,然后处理每一个小数量的集合。 3766 | 3767 | 3768 | 3769 | **6、海量搜索词汇,找到最热TOP100词汇的方法** 3770 | 3771 | ​ 最开始还是用哈希分流的思路来处理,把包含百亿数据量的词汇文件分流到不同的机器上,具体多少台机器由面试官规定或者由更多的限制来决定。对每一台机器来说,如果分到的数据量依然很大,比如,内存不够或其他问题,可以再用哈希函数把每台机器的分流文件拆成更小的文件处理。 3772 | 3773 | ​ 处理每一个小文件的时候,哈希表统计每种词及其词频,哈希表记录建立完成后,再遍历哈希表,遍历哈希表的过程中使用大小为100的小根堆来选出每一个小文件的top 100(整体未排序的top 100)。每一个小文件都有自己词频的小根堆(整体未排序的top 100),将小根堆里的词按照词频排序,就得到了每个小文件的排序后top 100。然后把各个小文件排序后的top 100进行外排序或者继续利用小根堆,就可以选出每台机器上的top 100。不同机器之间的top100再进行外排序或者继续利用小根堆,最终求出整个百亿数据量中的top 100。对于top K 的问题,除哈希函数分流和用哈希表做词频统计之外,还经常用堆结构和外排序的手段进行处理。 3774 | 3775 | 3776 | 3777 | **7、40亿个无符号整数,1GB内存,找到所有出现两次的数** 3778 | 3779 | ​ 对于原问题,可以用bit map的方式来表示数出现的情况。具体地说,是申请一个长度为4294967295×2的bit类型的数组bitArr,用2个位置表示一个数出现的词频,1B占用8个bit,所以长度为4294967295×2的bit类型的数组占用1GB空间。怎么使用这个bitArr数组呢?遍历这40亿个无符号数,如果初次遇到num,就把bitArr[num*2 + 1]和bitArr[num*2]设置为01,如果第二次遇到num,就把bitArr[num*2+1]和bitArr[num*2]设置为10,如果第三次遇到num,就把bitArr[num*2+1]和bitArr[num*2]设置为11。以后再遇到num,发现此时bitArr[num*2+1]和bitArr[num*2]已经被设置为11,就不再做任何设置。遍历完成后,再依次遍历bitArr,如果发现bitArr[i*2+1]和bitArr[i*2]设置为10,那么i 就是出现了两次的数。 3780 | 3781 | 3782 | 3783 | **8、10MB内存,找到40亿整数的中位数** 3784 | 3785 | ①内存够:内存够还慌什么啊,直接把100亿个全部排序了,你用冒泡都可以...然后找到中间那个就可以了。但是你以为面试官会给你内存?? 3786 | 3787 | ②内存不够:题目说是整数,我们认为是带符号的int,所以4字节,占32位。 3788 | 3789 | 假设100亿个数字保存在一个大文件中,依次读一部分文件到内存(不超过内存的限制),将每个数字用二进制表示,比较二进制的最高位(第32位,符号位,0是正,1是负),如果数字的最高位为0,则将这个数字写入 file_0文件中;如果最高位为 1,则将该数字写入file_1文件中。 3790 | 3791 | 从而将100亿个数字分成了两个文件,假设 file_0文件中有 60亿 个数字,file_1文件中有 40亿 个数字。那么中位数就在 file_0 文件中,并且是 file_0 文件中所有数字排序之后的第 10亿 个数字。(file_1中的数都是负数,file_0中的数都是正数,也即这里一共只有40亿个负数,那么排序之后的第50亿个数一定位于file_0中) 3792 | 3793 | 现在,我们只需要处理 file_0 文件了(不需要再考虑file_1文件)。对于 file_0 文件,同样采取上面的措施处理:将file_0文件依次读一部分到内存(不超内存限制),将每个数字用二进制表示,比较二进制的 次高位(第31位),如果数字的次高位为0,写入file_0_0文件中;如果次高位为1,写入file_0_1文件 中。 3794 | 3795 | 现假设 file_0_0文件中有30亿个数字,file_0_1中也有30亿个数字,则中位数就是:file_0_0文件中的数字从小到大排序之后的第10亿个数字。 3796 | 3797 | 抛弃file_0_1文件,继续对 file_0_0文件 根据 次次高位(第30位) 划分,假设此次划分的两个文件为:file_0_0_0中有5亿个数字,file_0_0_1中有25亿个数字,那么中位数就是 file_0_0_1文件中的所有数字排序之后的 第 5亿 个数。 3798 | 3799 | 按照上述思路,直到划分的文件可直接加载进内存时,就可以直接对数字进行快速排序,找出中位数了。 3800 | 3801 | 3802 | 3803 | **9、设计短域名系统,将长URL转化成短的URL.** 3804 | 3805 | (1)利用放号器,初始值为0,对于每一个短链接生成请求,都递增放号器的值,再将此值转换为62进制(a-zA-Z0-9),比如第一次请求时放号器的值为0,对应62进制为a,第二次请求时放号器的值为1,对应62进制为b,第10001次请求时放号器的值为10000,对应62进制为sBc。 3806 | 3807 | (2)将短链接服务器域名与放号器的62进制值进行字符串连接,即为短链接的URL,比如:[t.cn/sBc。](http://t.cn/sBc。) 3808 | 3809 | (3)重定向过程:生成短链接之后,需要存储短链接到长链接的映射关系,即sBc -> URL,浏览器访问短链接服务器时,根据URL Path取到原始的链接,然后进行302重定向。映射关系可使用K-V存储,比如Redis或Memcache。 3810 | 3811 | 3812 | 3813 | **10、让你系统的设计一个高并发的架构,你会从哪几个方面考虑?** 3814 | 3815 | **系统拆分** 3816 | 3817 | ​ 将一个系统拆分为多个子系统,用 dubbo 来搞。然后每个系统连一个数据库, 这样本来就一个库,现在多个数据库,不也可以扛高并发么。 3818 | 3819 | **缓存** 3820 | 3821 | ​ 缓存,必须得用缓存。大部分的高并发场景,都是读多写少,那你完全可以在数 据库和缓存里都写一份,然后读的时候大量走缓存不就得了。毕竟人家 redis 轻 轻松松单机几万的并发。所以你可以考虑考虑你的项目里,那些承载主要请求的 读场景,怎么用缓存来抗高并**发。** 3822 | 3823 | **MQ** 3824 | 3825 | ​ MQ,必须得用 MQ。可能你还是会出现高并发写的场景,比如说一个业务操作 里要频繁搞数据库几十次,增删改增删改,疯了。那高并发绝对搞挂你的系统, 你要是用 redis 来承载写那肯定不行,人家是缓存,数据随时就被 LRU 了,数 据格式还无比简单,没有事务支持。所以该用 mysql 还得用 mysql 啊。那你 咋办?用 MQ 吧,大量的写请求灌入 MQ 里,排队慢慢玩儿,后边系统消费 后慢慢写,控制在 mysql 承载范围之内。所以你得考虑考虑你的项目里,那些 承载复杂写业务逻辑的场景里,如何用 MQ 来异步写,提升并发性。MQ 单机 抗几万并发也是 ok 的,这个之前还特意说过。 3826 | 3827 | **分库分表** 3828 | 3829 | ​ 分库分表,可能到了最后数据库层面还是免不了抗高并发的要求,好吧,那么就 将一个数据库拆分为多个库,多个库来扛更高的并发;然后将一个表拆分为多个 表,每个表的数据量保持少一点,提高 sql 跑的性能。 3830 | 3831 | **读写分离** 3832 | 3833 | ​ 读写分离,这个就是说大部分时候数据库可能也是读多写少,没必要所有请求都 集中在一个库上吧,可以搞个主从架构,主库写入,从库读取,搞一个读写分离。 读流量太多的时候,还可以加更多的从库。 3834 | 3835 | **ElasticSearch** 3836 | 3837 | ​ Elasticsearch,简称 es。es 是分布式的,可以随便扩容,分布式天然就可以支 撑高并发,因为动不动就可以扩容加机器来扛更高的并发。那么一些比较简单的 查询、统计类的操作,可以考虑用 es 来承载,还有一些全文搜索类的操作,也 可以考虑用 es 来承载。 3838 | 3839 | 3840 | 3841 | **11、假设有这么一个场景,有一条新闻,新闻的评论量可能很大,如何设计评论的读和写** 3842 | 3843 | 可以进行读写分离、加载到缓存 3844 | 3845 | 3846 | 3847 | **12、显示网站的用户在线数的解决思路** 3848 | 3849 | 1. 维护在线用户表 3850 | 3851 | 2. 使用Redis统计 3852 | 3853 | ## **十、个人项目** 3854 | 3855 | #### 一、秒杀项目: 3856 | 3857 | **项目架构介绍:** 3858 | 3859 | ​ 系统主要通过缓存,异步,限流来保证系统的高并发和高可用。 3860 | 3861 | ​ https://blog.csdn.net/awake_lqh/article/details/83306983 3862 | 3863 | ​ [https://www.baidu.com/link](https://www.baidu.com/link?url=a1nIR9sc4lEPBYZIjnm6IUxkZ1WgVKqtWMKKbHLa7smmZt0PB_gJ4Ad0IcXLz999&wd=&eqid=815709f600005763000000065f39f595) 3864 | 3865 | 3866 | 3867 | **1、秒杀的流程** 3868 | 3869 | 用户发起请求——秒杀接口——访问内存标记库存——访问redis预减库存——判断是否重复下单——压入消息队列——客户端通过ajax请求轮询访问下单结果接口,直到响应状态成功或者失败 3870 | 3871 | 3872 | 3873 | **2、库存预减用的是哪个redis方法** 3874 | 3875 | redisService.**decr**(GoodsKey.getSeckillGoodsStock, "" + goodsId); 3876 | 3877 | 3878 | 3879 | **3、如果项目中的redis服务挂掉,如何减轻数据库的压力** 3880 | 3881 | redis当做二级缓存使用,ehcache作为一级缓存,当redis挂掉,还有一级缓存减轻数据库的压力。 3882 | 3883 | 3884 | 3885 | **4、如何避免消息队列的消费方重复消费消息** 3886 | 3887 | 消费者每次执行查询前,首先在DB上查询任务的执行状态,若处于「取消/失败/成功」则表示已经由其它消费者消费过,那么直接返回ACK状态码给MQ,将消息从MQ中移除; 3888 | 3889 | 3890 | 3891 | **5、说一下如何解决超卖的** 3892 | 3893 | 更改数据库时,判断库存是否大于0 3894 | 3895 | redis预减库存:redis库存不足时直接返回失败 3896 | 3897 | Mysql乐观锁:给每个商品库存一个版本号version字段,在实际减库存的SQL操作中,首先判断version是否是我们查询库存时候的version,如果是,扣减库存,成功抢购。如果发现version变了,则不更新数据库,返回抢购失败。 3898 | 3899 | 3900 | 3901 | **6、你说你用到了redis,redis有哪些数据结构,你为什么要用redis,哪里用到了,为什么说redis快,多路io复用详细原理可以说说嘛?** 3902 | 3903 | String、List、Set、ZSet、Map 3904 | 3905 | redis是内存型数据库,访问、存储都非常快,通常用来缓存一些热点数据,比如秒杀商品、用户状态信息等,减轻数据库访问的压力 3906 | 3907 | 快是由于redis基于单线程,减少了线程的上下文切换;内存型数据库,访问快;基于多路io复用; 3908 | 3909 | 多路-指的是多个socket连接,复用-指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll是最新的也是目前最好的多路复用技术。 3910 | 3911 | 这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。 3912 | 3913 | 3914 | 3915 | **7、说一下redis的应用场景** 3916 | 3917 | **计数器** 3918 | 3919 | ​ 可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。 3920 | 3921 | **缓存** 3922 | 3923 | ​ 将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。 3924 | 3925 | **会话缓存** 3926 | 3927 | ​ 可以使用 Redis 来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。 3928 | 3929 | **其它** 3930 | 3931 | ​ Set 可以实现交集、并集等操作,从而实现共同好友等功能。ZSet 可以实现有序性操作,从而实现排行榜等功能。 3932 | 3933 | 3934 | 3935 | **8、秒杀系统怎么做的,架构图画了一下** 3936 | 3937 | 3938 | 3939 | **10.秒杀模块怎么设计的,如何压测,抗压手段,如何保证数据库与redis缓存一致的,消息队列怎么用的** 3940 | **11.秒杀系统服务器抗压思路,从哪些方面去优化** 3941 | **12.如何解决超卖** 3942 | **13.讲讲你做的秒杀项目** 3943 | **14.你的秒杀项目,别说你里面的优化,你还有什么优化策略吗?多服务器负载均衡,把秒杀商品平均分给服务器。** 3944 | **15.秒杀项目部分实现怎么做的** 3945 | **16.秒杀系统的前端设计怎么做?** 3946 | **17.说说秒杀如何实现的?(用redis预库存的减少,然后方式异步消息队列rabbitMQ中)** 3947 | **18.如何解决商城中超卖问题?秒杀场景呢?** 3948 | **19.秒杀过程中怎么保证redis缓存和数据库的一致性** 3949 | **20.具体的秒杀细节怎么做的?秒杀的核心技术在哪儿?你怎么保证的?** 3950 | **21.秒杀商品的库存放在哪里,如何保证redis和DB的一致性** 3951 | **22.设计秒杀方案(从高并发、快速响应、高可用三方面回答,高并发(增加网络带宽、DNS域名解析分发多台服务器、使用前置代理服务器ngnix、CDN内容分发、数据库查询优化(读写分离、分库分表)),快速响应(缓存服务器(memcached、redis)、能使用静态页面就用静态页面,减少容器解析、把常访问的图片等内容缓存)、高可用(热备,如数据库服务器的热备、集群监控(如使用zabbix,重点关注IO、内存、带宽和机器load)))** 3952 | **23.秒杀时如果机器资源有限怎么办** 3953 | **24.秒杀接口防刷怎么做** 3954 | **25.如何防止超卖和少卖** 3955 | **26.秒杀系统场景下怎么防止超卖,redis和数据库数据不一致怎么办,以什么为准** 3956 | **27.秒杀流程图 如何保证不超卖 以及对应SQL** 3957 | 3958 | 3959 | 3960 | **1、如何解决超卖?** 3961 | 3962 | mysql乐观锁+redis预减库存+redis缓存卖完标记 3963 | 3964 | 3965 | 3966 | **2、如何解决重复下单?** 3967 | 3968 | mysql唯一索引+分布式锁 3969 | 3970 | 3971 | 3972 | **3、如何防刷?** 3973 | 3974 | IP限流+验证码 3975 | 3976 | 3977 | 3978 | **4、热key问题如何解决?** 3979 | 3980 | redis集群+本地缓存+限流+key加随机值分布在多个实例中 3981 | 3982 | 3983 | 3984 | **5、消息队列的作用?如何保证消息的不丢失?** 3985 | 3986 | 异步削峰;发送方开启confirm+消息队列持久化+消费方关闭自动ACK,确保消费成功之后自动调用API进行确认。 3987 | 3988 | 3989 | 3990 | **6、缓存和数据库数据一致性如何保证?** 3991 | 3992 | 秒杀项目不用保证,其他项目就用延时双删或者先更新数据再是缓存失效,为防缓存失效这一信息丢失,可用消息队列确保。 3993 | 3994 | 3995 | 3996 | -------------------------------------------------------------------------------- /Java/v3.0-JavaGuide面试突击版.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pure-xiaojie/JavaInterview/fe204ee9d9b38be59c1d57847f8c3f388351c0da/Java/v3.0-JavaGuide面试突击版.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Java开发一轮复习 2 | 3 | ## **一、网络篇** 4 | 5 | **1、OSI七层模型与TCP/IP 五层模型** 6 | 7 | ​ OSI七层:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层 8 | 9 | ​ TCP/IP五层:物理层、数据链路层、网络层、传输层、应用层 10 | 11 | 12 | 13 | **2、常见应用层协议和运输层、网络层协议,以及硬件如路由器之类在哪一层** 14 | 15 | ​ 应用层:HTTP、SMTP、DNS、FTP 16 | 17 | ​ 传输层:TCP 、UDP 18 | 19 | ​ 网络层:ICMP 、IP、路由器、防火墙 20 | 21 | ​ 数据链路层:网卡、网桥、交换机 22 | 23 | ​ 物理层:中继器、集线器 24 | 25 | 26 | 27 | **3、TCP与UDP区别和应用场景,基于TCP的协议有哪些,基于UDP的有哪些** 28 | 29 | | 类型 | 特点 | 性能 | 应用过场景 | 首部字节 | | 30 | | ---- | :------------------------: | :--------------------: | :--------------: | :------: | ---- | 31 | | TCP | 面向连接、可靠、字节流 | 传输效率慢、所需资源多 | 文件、邮件传输 | 20-60 | | 32 | | UDP | 无连接、不可靠、数据报文段 | 传输效率快、所需资源少 | 语音、视频、直播 | 8个字节 | | 33 | 34 | ​ **基于TCP的协议:**HTTP、FTP、SMTP 35 | 36 | ​ **基于UDP的协议:**RIP、DNS、SNMP 37 | 38 | 39 | 40 | **4、TCP可靠传输的保证,拥塞控制目的和过程** 41 | 42 | ​ **TCP通过:**应用数据分割、对数据包进行编号、校验和、流量控制、拥塞控制、ARP协议、超时重传等措施保证数据的可靠传输; 43 | 44 | ​ **拥塞控制目的:**为了防止过多的数据注入到网络中,避免网络中的路由器、链路过载 45 | 46 | ​ **拥塞控制过程:**TCP发送将维护一个拥塞窗口的状态变量,该变量随着网络拥塞程度动态变化,通过慢开始、拥塞避免等算法减少网络拥塞的发生。 47 | 48 | 49 | 50 | **5、TCP粘包现象原因和解决方法** 51 | 52 | ​ **TCP粘包是指**:发送方发送的若干包数据到接收方接收时粘成一包 53 | 54 | ​ **发送方原因:** 55 | 56 | ​ TCP默认使用Nagle算法(主要作用:减少网络中报文段的数量),而Nagle算法主要做两件事: 57 | 58 | ​ 只有上一个分组得到确认,才会发送下一个分组 59 | ​ 收集多个小分组,在一个确认到来时一起发送 60 | ​ Nagle算法造成了发送方可能会出现粘包问题 61 | 62 | ​ **接收方原因:** 63 | 64 | ​ TCP接收到数据包时,并不会马上交到应用层进行处理,或者说应用层并不会立即处理。实际上, TCP将接收到的数据包保存在接收缓存里,然后应用程序主动从缓存读取收到的分组。这样一来,如果 TCP 接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程 序就有可能读取到多个首尾相接粘到一起的包。 65 | 66 | ​ **解决粘包问题:** 67 | 68 | ​ 最本质原因在与接收对等方无法分辨消息与消息之间的边界在哪,通过使用某种方案给出边界,例如: 69 | 70 | - 发送定长包。如果每个消息的大小都是一样的,那么在接收对等方只要累计接收数据,直到数据等于一个定长的数值就将它作为一个消息。 71 | - 包尾加上\r\n标记。FTP协议正是这么做的。但问题在于如果数据正文中也含有\r\n,则会误判为消息的边界。 72 | - 包头加上包体长度。包头是定长的4个字节,说明了包体的长度。接收对等方先接收包体长度,依据包体长度来接收包体。 73 | 74 | 75 | 76 | **6、TCP三次握手过程以及每次握手后的状态改变,为什么三次? 为什么两次不行?** 77 | 78 | ​ **三次握手过程:** 79 | 80 | ​ 客户端——发送带有SYN标志的数据包——服务端 **一次握手** Client进入syn_sent状态 81 | 82 | ​ 服务端——发送带有SYN/ACK标志的数据包——客户端 **二次握手** 服务端进入syn_rcvd 83 | 84 | ​ 客户端——发送带有ACK标志的数据包——服务端 **三次握手** 连接就进入Established状态 85 | 86 | ​ **为什么三次:** 87 | 88 | ​ 主要是为了建立可靠的通信信道,保证客户端与服务端同时具备发送、接收数据的能力 89 | 90 | ​ **为什么两次不行?** 91 | 92 | ​ 1、防止已失效的请求报文又传送到了服务端,建立了多余的链接,浪费资源 93 | 94 | ​ 2、 两次握手只能保证单向连接是畅通的。(为了实现可靠数据传输, TCP 协议的通信双方, 都必须维 护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方 相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤;如果只是两次握手, 至多只 有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认) 95 | 96 | 97 | 98 | **7、TCP四次挥手过程以及状态改变,为什么四次?CLOSE-WAIT和TIME-WAIT存在的意义?如何查看TIME-WAIT状态的链接数量?为什么会TIME-WAIT过多?解决方法是怎样的?** 99 | 100 | **四次挥手过程:** 101 | 102 | ​ 客户端——发送带有FIN标志的数据包——服务端,关闭与服务端的连接 ,客户端进入FIN-WAIT-1状态 103 | 104 | ​ 服务端收到这个 FIN,它发回⼀ 个 ACK,确认序号为收到的序号加1,服务端就进入了CLOSE-WAIT状态 105 | 106 | ​ 服务端——发送⼀个FIN数据包——客户端,关闭与客户端的连接,客户端就进入FIN-WAIT-2状态 107 | 108 | ​ 客户端收到这个 FIN,发回 ACK 报⽂确认,并将确认序号设置为收到序号加1,TIME-WAIT状态 109 | 110 | **为什么四次:** 111 | 112 | ​ 因为需要确保客户端与服务端的数据能够完成传输。 113 | 114 | **CLOSE-WAIT:** 115 | 116 | ​ 这种状态的含义其实是表示在等待关闭 117 | 118 | **TIME-WAIT:** 119 | 120 | ​ 为了解决网络的丢包和网络不稳定所带来的其他问题,确保连接方能在时间范围内,关闭自己的连接 121 | 122 | **如何查看TIME-WAIT状态的链接数量?** 123 | 124 | ​ netstat -an |grep TIME_WAIT|wc -l 查看连接数等待time_wait状态连接数 125 | 126 | **为什么会TIME-WAIT过多?解决方法是怎样的?** 127 | 128 | ​ **可能原因:** 高并发短连接的TCP服务器上,当服务器处理完请求后立刻按照主动正常关闭连接 129 | 130 | ​ **解决:**负载均衡服务器;Web服务器首先关闭来自负载均衡服务器的连接 131 | 132 | 133 | 134 | **8、TCP、UDP、IP、以太网报文格式以及重要字段,报文从一端到另一端传递的过程。** 135 | 136 | ​ **TCP报文格式:** 137 | 138 | ​ ![](https://farm1.staticflickr.com/792/27194088468_4cb0141fc8_b.jpg) 139 | 140 | ​ **源端口号和目的端口号**: 141 | 142 | ​ 用于寻找发端和收端应用进程。这两个值加上ip首部源端ip地址和目的端ip地址唯一确定一个tcp连接。 143 | 144 | ​ **序号字段:** 145 | 146 | ​ 序号用来标识从T C P发端向T C P收端发送的数据字节流,它表示在这个报文段中的的第一个数据字节。如果将字节流看作在两个应用程序间的单向流动,则 T C P用序号对每个字节进行计数。序号是32 bit的无符号数,序号到达 2^32-1后又从0开始。 147 | 148 |   当建立一个新的连接时,SYN标志变1。序号字段包含由这个主机选择的该连接的初始序号ISN(Initial Sequence Number)。该主机要发送数据的第一个字节序号为这个ISN加1,因为SYN标志消耗了一个序号 149 | 150 | ​ **确认序号**: 151 | 152 | ​ 既然每个传输的字节都被计数,确认序号包含发送确认的一端所期望收到的下一个序号。因此,确认序号应当是上次已成功收到数据字节序号加 1。只有ACK标志为 1时确认序号字段才有效。发送ACK无需任何代价,因为 32 bit的确认序号字段和A C K标志一样,总是T C P首部的一部分。因此,我们看到一旦一个连接建立起来,这个字段总是被设置, ACK标志也总是被设置为1。TCP为应用层提供全双工服务。这意味数据能在两个方向上独立地进行传输。因此,连接的每一端必须保持每个方向上的传输数据序号。 153 | 154 | ​ **首都长度**: 155 | 156 | ​ 首部长度给出首部中 32 bit字的数目。需要这个值是因为任选字段的长度是可变的。这个字段占4 bit,因此T C P最多有6 0字节的首部。然而,没有任选字段,正常的长度是 2 0字节。 157 | 158 | ​ **标志字段**:在T C P首部中有 6个标志比特。它们中的多个可同时被设置为1. 159 |   URG紧急指针(u rgent pointer)有效 160 |   ACK确认序号有效。 161 |   PSH接收方应该尽快将这个报文段交给应用层。 162 |   RST重建连接。 163 |   SYN同步序号用来发起一个连接。这个标志和下一个标志将在第 1 8章介绍。 164 |   FIN发端完成发送任务。 165 | 166 | ​ **窗口大小**: 167 | 168 | ​ T C P的流量控制由连接的每一端通过声明的窗口大小来提供。窗口大小为字节数,起始于确认序号字段指明的值,这个值是接收端期望接收的字节。窗口大小是一个 16 bit字段,因而窗口大小最大为 65535字节。 169 | 170 | ​ **检验和:** 171 | 172 | ​ 检验和覆盖了整个的 T C P报文段:T C P首部和T C P数据。这是一个强制性的字段,一定是由发端计算和存储,并由收端进行验证。 173 | 174 | ​ **紧急指针**: 175 | 176 | ​ 只有当URG标志置1时紧急指针才有效。紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。 T C P的紧急方式是发送端向另一端发送紧急数据的一种方式。 177 | 178 | ​ **选项**: 179 | 180 | ​ 最常见的可选字段是最长报文大小,又称为 MSS (Maximum Segment Size)。每个连接方通常都在通信的第一个报文段(为建立连接而设置 S Y N标志的那个段)中指明这个选项。它指明本端所能接收的最大长度的报文段。 181 | 182 | 183 | 184 | ​ **UDP报文格式:** 185 | 186 | ​ ![](https://appwk.baidu.com/naapi/doc/view?ih=482&o=png_6_0_0_176_917_504_247_892.979_1262.879&iw=986&ix=0&iy=0&aimw=986&rn=1&doc_id=182d935c3186bceb18e8bb77&pn=1&sign=b88bda03b9954e506622f97b8b2ae438&type=1&app_ver=2.9.8.2&ua=bd_800_800_IncredibleS_2.9.8.2_2.3.7&bid=1&app_ua=IncredibleS&uid=&cuid=&fr=3&Bdi_bear=WIFI&from=3_10000&bduss=&pid=1&screen=800_800&sys_ver=2.3.7) 187 | 188 | ​ **端口号**: 189 | 190 | ​ 用来表示发送和接受进程。由于 I P层已经把I P数据报分配给T C P或U D P(根据I P首部中协议字段值),因此T C P端口号由T C P来查看,而 U D P端口号由UDP来查看。T C P端口号与UDP端口号是相互独立的。 191 | 192 | ​ **长度**: 193 | 194 | ​ UDP长度字段指的是UDP首部和UDP数据的字节长度。该字段的最小值为 8字节(发送一份0字节的UDP数据报是 O K)。 195 | 196 | ​ **检验和**: 197 | 198 | ​ UDP检验和是一个端到端的检验和。它由发送端计算,然后由接收端验证。其目的是为了发现UDP首部和数据在发送端到接收端之间发生的任何改动。 199 | 200 | 201 | 202 | ​ **IP报文格式:**普通的IP首部长为20个字节,除非含有可选项字段。 203 | 204 | ​ ![](https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=1614312792,1954581760&fm=26&gp=0.jpg) 205 | 206 | ​ **4位版本**: 207 | 208 | ​ 目前协议版本号是4,因此IP有时也称作IPV4. 209 | 210 | ​ **4位首部长度**: 211 | 212 | ​ 首部长度指的是首部占32bit字的数目,包括任何选项。由于它是一个4比特字段,因此首部长度最长为60个字节。 213 | 214 | ​ **服务类型(TOS)**: 215 | 216 | ​ 服务类型字段包括一个3bit的优先权字段(现在已经被忽略),4bit的TOS子字段和1bit未用位必须置0。4bit的TOS分别代表:最小时延,最大吞吐量,最高可靠性和最小费用。4bit中只能置其中1比特。如果所有4bit均为0,那么就意味着是一般服务。 217 | 218 | ​ **总长度**: 219 | 220 | ​ 总长度字段是指整个IP数据报的长度,以字节为单位。利用首部长度和总长度字段,就可以知道IP数据报中数据内容的起始位置和长度。由于该字段长16bit,所以IP数据报最长可达65535字节。当数据报被分片时,该字段的值也随着变化。 221 | 222 | ​ **标识字段**: 223 | 224 | ​ 标识字段唯一地标识主机发送的每一份数据报。通常每发送一份报文它的值就会加1。 225 | 226 | ​ **生存时间**: 227 | 228 | ​ TTL(time-to-live)生存时间字段设置了数据报可以经过的最多路由器数。它指定了数据报的生存时间。TTL的初始值由源主机设置(通常为 3 2或6 4),一旦经过一个处理它的路由器,它的值就减去 1。当该字段的值为 0时,数据报就被丢弃,并发送 ICMP 报文通知源主机。 229 | 230 | ​ **首部检验和**: 231 | 232 | ​ 首部检验和字段是根据 I P首部计算的检验和码。它不对首部后面的数据进行计算。 ICMP、IGMP、UDP和TCP在它们各自的首部中均含有同时覆盖首部和数据检验和码。 233 | 234 | 235 | 236 | ​ **以太网报文格式:** 237 | 238 | ![](https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=2733576797,55677727&fm=26&gp=0.jpg) 239 | 240 | ​ **目的地址和源地址:** 241 | 242 | ​ 是指网卡的硬件地址(也叫MAC 地址),长度是48 位,是在网卡出厂时固化的。 243 | 244 | ​ **数据:** 245 | 246 | ​ 以太网帧中的数据长度规定最小46 字节,最大1500 字节,ARP 和RARP 数据包的长度不够46 字节,要在后面补填充位。最大值1500 称为以太网的最大传输单元(MTU),不同的网络类型有不同的MTU,如果一个数据包从以太网路由到拨号链路上,数据包度大于拨号链路的MTU了,则需要对数据包进行分片fragmentation)。ifconfig 命令的输出中也有“MTU:1500”。注意,MTU 个概念指数据帧中有效载荷的最大长度,不包括帧首部的长度。 247 | 248 | 249 | 250 | **9、浏览器输入URL并回车的过程以及相关协议,DNS查询过程。** 251 | 252 | ​ **过程:**DNS解析、TCP连接、发送HTTP请求、服务器处理请求并返回HTTP报文、浏览器渲染、结束 253 | 254 | | 过程 | 使用的协议 | 255 | | ------------------------------------------------------------ | --------------------- | 256 | | 1、浏览器查找域名DNS的IP地址
DNS查找过程(浏览器缓存、路由器缓存、DNS缓存) | DNS:获取域名对应的ip | 257 | | 2、根据ip建立TCP连接 | TCP:与服务器建立连接 | 258 | | 3、浏览器向服务器发送HTTP请求 | HTTP:发送请求 | 259 | | 4、服务器响应HTTP响应 | HTTP | 260 | | 5、浏览器进行渲染 | | 261 | 262 | 263 | 264 | **10、HTTP1.0、1.1、2.0之间的区别** 265 | 266 | ​ **HTTP1.0:**默认使用Connection:cloose,浏览器每次请求都需要与服务器建立一个TCP连接,服务器处理完成后立即断开TCP连接(无连接),服务器不跟踪每个客户端也不记录过去的请求(无状态)。 267 | 268 | ​ **HTTP1.1:**默认使用Connection:keep-alive(长连接),避免了连接建立和释放的开销;通过Content-Length字段来判断当前请求的数据是否已经全部接受。不允许同时存在两个并行的响应。 269 | 270 | ​ **HTTP2.0:**引入二进制数据帧和流的概念,其中帧对数据进行顺序标识;因为有了序列,服务器可以并行的传输数据。 271 | 272 | ​ **http1.0和http1.1的主要区别如下:** 273 | ​ 1、缓存处理:1.1添加更多的缓存控制策略(如:Entity tag,If-Match) 274 | ​ 2、网络连接的优化:1.1支持断点续传 275 | ​ 3、错误状态码的增多:1.1新增了24个错误状态响应码,丰富的错误码更加明确各个状态 276 | ​ 4、Host头处理:支持Host头域,不在以IP为请求方标志 277 | ​ 5、长连接:减少了建立和关闭连接的消耗和延迟。 278 | 279 | ​ **http1.1和http2.0的主要区别:** 280 | ​ 1、新的传输格式:2.0使用二进制格式,1.0依然使用基于文本格式 281 | ​ 2、多路复用:连接共享,不同的request可以使用同一个连接传输(最后根据每个request上的id号组合成 正常的请求) 282 | ​ 3、header压缩:由于1.X中header带有大量的信息,并且得重复传输,2.0使用encoder来减少需要传输的 hearder大小 283 | ​ 4、服务端推送:同google的SPDUY(1.0的一种升级)一样 284 | 285 | 286 | 287 | **11、HTTP与HTTPS之间的区别,HTTPS链接建立的过程,了解对称加密算法和非对称加密算法不?** 288 | 289 | ​ **HTTP与HTTPS之间的区别:** 290 | 291 | | HTTP | HTTPS | 292 | | :----------------------------: | :--------------------------------------: | 293 | | 默认端口80 | HTTPS默认使用端口443 | 294 | | 明文传输、数据未加密、安全性差 | 传输过程ssl加密、安全性较好 | 295 | | 响应速度快、消耗资源少 | 响应速度较慢、消耗资源多、需要用到CA证书 | 296 | 297 | ​ **HTTPS链接建立的过程:** 298 | 299 | ​ 1.首先客户端先给服务器发送一个请求 300 | 301 | ​ 2.服务器发送一个SSL证书给客户端,内容包括:证书的发布机构、有效期、所有者、签名以及公钥 302 | 303 | ​ 3.客户端对发来的公钥进行真伪校验,校验为真则使用公钥对对称加密算法以及对称密钥进行加密 304 | 305 | ​ 4.服务器端使用私钥进行解密并使用对称密钥加密确认信息发送给客户端 306 | 307 | ​ 5.随后客户端和服务端就使用对称密钥进行信息传输 308 | 309 | ​ **对称加密算法:** 310 | 311 | ​ 双方持有相同的密钥,且加密速度快,典型对称加密算法:DES、AES 312 | 313 | ​ **非对称加密算法:** 314 | 315 | ​ 密钥成对出现(私钥、公钥),私钥只有自己知道,不在网络中传输;而公钥可以公开。相比对称加密速度较慢,典型的非对称加密算法有:RSA、DSA 316 | 317 | 318 | 319 | **12、HTTP请求有哪些。get和Post区别。** 320 | 321 | **HTTP请求:** 322 | 323 | | 方法 | 描述 | 324 | | :------ | ------------------------------------------------------------ | 325 | | GET | 向特定资源发送请求,查询数据,并返回实体 | 326 | | POST | 向指定资源提交数据进行处理请求,可能会导致新的资源建立、已有资源修改 | 327 | | PUT | 向服务器上传新的内容 | 328 | | HEAD | 类似GET请求,返回的响应中没有具体的内容,用于获取报头 | 329 | | DELETE | 请求服务器删除指定标识的资源 | 330 | | OPTIONS | 可以用来向服务器发送请求来测试服务器的功能性 | 331 | | TRACE | 回显服务器收到的请求,用于测试或诊断 | 332 | | CONNECT | HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器 | 333 | 334 | **get和Post区别:** 335 | 336 | | | GET | POST | 337 | | -------- | ------------------------------------------------------------ | ---------------------------------------------------------- | 338 | | 可见性 | 数据在URL中对所有人可见 | 数据不会显示在URL中 | 339 | | 安全性 | 与post相比,get的安全性较差,因为所
发送的数据是URL的一部分 | 安全,因为参数不会被保存在浏览器
历史或web服务器日志中 | 340 | | 数据长度 | 受限制,最长2kb | 无限制 | 341 | | 编码类型 | application/x-www-form-urlencoded | multipart/form-data | 342 | | 缓存 | 能被缓存 | 不能被缓存 | 343 | | | | | 344 | 345 | 346 | 347 | **13、HTTP常见响应状态码,从1xx到5xx** 348 | 349 | ​ 100:Continue --- 继续。客户端应继续其请求。 350 | 351 | ​ 200:OK --- 请求成功。一般用于GET与POST请求。 352 | 353 | ​ 301:Moved Permanently --- 永久重定向。 354 | 355 | ​ 302:Found --- 暂时重定向。 356 | 357 | ​ 400:Bad Request --- 客户端请求的语法错误,服务器无法理解。 358 | 359 | ​ 403:Forbideen --- 服务器理解请求客户端的请求,但是拒绝执行此请求。 360 | 361 | ​ 404:Not Found --- 服务器无法根据客户端的请求找到资源(网页)。 362 | 363 | ​ 500:Internal Server Error --- 服务器内部错误,无法完成请求。 364 | 365 | ​ 502:Bad Gateway --- 作为网关或者代理服务器尝试执行请求时,从远程服务器接收到了无效的响应。 366 | 367 | 368 | 369 | **14、重定向和转发区别** 370 | 371 | ​ **重定向:redirect:** 372 | 373 | ​ 地址栏发生变化 374 | 375 | ​ 重定向可以访问其他站点(服务器)的资源 376 | 377 | ​ 重定向是两次请求。不能使用request对象来共享数据 378 | 379 | ​ **转发:forward:** 380 | 381 | ​ 转发地址栏路径不变 382 | 383 | ​ 转发只能访问当前服务器下的资源 384 | 385 | ​ 转发是一次请求,可以使用request对象共享数据 386 | 387 | 388 | 389 | **15、cookie和session区别。** 390 | 391 | ​ Cookie 和 Session都是用来跟踪浏览器用户身份的会话方式,但两者有所区别: 392 | 393 | ​ Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。 394 | 395 | ​ cookie不是很安全,别人可以分析存放在本地的COOKIE并进行欺骗,考虑到安全应当使用session。 396 | 397 | ​ Cookie ⼀般⽤来保存⽤户信息,Session 的主要作⽤就是通过服务端记录⽤户的状态 398 | 399 | ## **二、操作系统篇** 400 | 401 | **1、进程和线程的区别** 402 | 403 | ​ **进程:**是资源分配的最小单位,是程序的执行过程,一个进程可以有多个线程,多个线程共享进程的堆和方法区资源,但每个线程又有属于自己的本地方法栈、虚拟机栈、程序计数器 404 | 405 | ​ **线程:**是任务调度和执行的最小单位,线程间可能存在相互影响,执行开销较小,不利于资源的管理和保护,线程间是共享进程中的资源的 406 | 407 | 408 | 409 | **2、协程?** 410 | 411 | ​ 是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。 412 | 413 | 414 | 415 | **3、进程间通信方式IPC** 416 | 417 | 参考:https://www.jianshu.com/p/c1015f5ffa74 418 | 419 | **匿名管道pipe:** 420 | 421 | ​ 匿名管道是半双工的,数据只能单向通信;需要双方通信时,需要建立起两个管道;只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。 422 | 423 | **命名管道FIFO:** 424 | 425 | ​ 不同于匿名管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。值得注意的是,FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。 426 | 427 | **信号:** 428 | 429 | ​ 信号是一种比较复杂的通信方式,信号产生的条件:按键、硬件异常、进程调用kill函数将信号发送给另一个进程、用户调用kill命令将信号发送给其他进程,信号传递的消息比较少,主要用于通知接收进程某个时间已经发生。 430 | 431 | **消息队列:** 432 | 433 | ​ 消息队列是消息的链表,存放在内核中并由消息队列标识符标识,消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点。消息队列起信箱作用,到了就挂在那里,需要的时候去取。消息队列提供了一种在两个不相关进程间传递数据的简单有效的方法。与命名管道相比:消息队列的优势在于,它独立于发送和接收进程而存在,这消除了在同步命名管道的打开和关闭时可能产生的一些困难。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。而且,每个数据块被认为含有一个类型,接收进程可以独立地接收含有不同类型值的数据块。 434 | 435 | ​ **优点:** 436 | 437 | ​ A. 我们可以通过发送消息来几乎完全避免命名管道的同步和阻塞问题。 438 | 439 | ​ B. 我们可以用一些方法来提前查看紧急消息。 440 | 441 | ​ **缺点:** 442 | 443 | ​ A. 与管道一样,每个数据块有一个最大长度的限制。 444 | 445 | ​ B. 系统中所有队列所包含的全部数据块的总长度也有一个上限。 446 | 447 | **共享内存(share memory):** 448 | 449 | - 使得多个进程可以可以直接读写同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。 450 | - 为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。 451 | - 由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥。 452 | 453 | **信号量(Semaphores) :** 454 | 455 | ​ 信号量是⼀个计数器,⽤于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信⽅式主要⽤于解决与同步相关的问题并避免竞争条件。 456 | 457 | **套接字(Sockets) :** 458 | 459 | ​ 此⽅法主要⽤于在客户端和服务器之间通过⽹络进⾏通信。套接字是⽀持TCP/IP 的⽹络通信的基本操作单元,可以看做是不同主机之间的进程进⾏双向通信的端点,简单的说就是通信的两⽅的⼀种约定,⽤套接字中的相关函数来完成通信过程。 460 | 461 | 462 | 463 | **4、用户态和核心态** 464 | 465 | ​ 在计算机系统中,分两种程序:系统程序和应用程序,为了保证系统程序不被应用程序有意或无意地破坏,为计算机设置了两种状态——用户态、核心态 466 | 467 | **用户态:**只能受限的访问内存,运行所有的应用程序 468 | 469 | **核心态:**运行操作系统程序,cpu可以访问内存的所有数据,包括外围设备 470 | 471 | **为什么要有用户态和内核态:** 472 | 473 | ​ 由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络 474 | 475 | **用户态切换到内核态的3种方式:** 476 | 477 | ​ **a. 系统调用** 478 | 479 | ​ 这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如前例中fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。 480 | 481 | ​ **b. 异常** 482 | 483 | ​ 当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。 484 | 485 | ​ **c. 外围设备的中断** 486 | 487 | ​ 当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。 488 | 489 | ​ 这3种方式是系统在运行时由用户态转到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。 490 | 491 | ​ 492 | 493 | **5、操作系统分配的进程空间是怎样的?线程能共享哪些?** 494 | 495 | ​ 栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。 496 | 497 | ​ 堆区(heap)— 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。 498 | 499 | ​ 静态区(static)—存放全局变量和静态变量的存储 500 | 501 | ​ 代码区(text)—存放函数体的二进制代码。 502 | 503 | ​ **线程共享堆区、静态区** 504 | 505 | 506 | 507 | **6、操作系统内存管理方式,分页分段以及段页式的优缺点** 508 | 509 | 参考地址:https://blog.csdn.net/qq_37189082/article/details/97963763 510 | 511 | **存管理方式:**块式管理、页式管理、段式管理、段页式管理 512 | 513 | **分段管理:** 514 | 515 | ​ 在段式存储管理中,将程序的地址空间划分为若干段(segment),如代码段,数据段,堆栈段;这样每个进程有一个二维地址空间,相互独立,互不干扰。段式管理的优点是:没有内碎片(因为段大小可变,改变段大小来消除内碎片)。但段换入换出时,会产生外碎片(比如4k的段换5k的段,会产生1k的外碎片) 516 | 517 | **分页管理:** 518 | 519 | ​ 在页式存储管理中,将程序的逻辑地址划分为固定大小的页(page),而物理内存划分为同样大小的页框,程序加载时,可以将任意一页放入内存中任意一个页框,这些页框不必连续,从而实现了离散分离。页式存储管理的优点是:没有外碎片(因为页的大小固定),但会产生内碎片(一个页可能填充不满) 520 | 521 | **段页式管理:** 522 | 523 | ​ 段⻚式管理机制结合了段式管理和⻚式管理的优点。简单来说段⻚式管理机制就是把主存先分成若⼲ 段,每个段⼜分成若⼲⻚,也就是说 段⻚式管理机制 中段与段之间以及段的内部的都是离散的。 524 | 525 | 526 | 527 | **7、页面置换算法有哪些,FIFO为什么不好?如何改进?LRU思想,手写LRU** 528 | 529 | **置换算法:**先进先出FIFO、最近最久未使用LRU、最佳置换算法OPT 530 | 531 | **先进先出FIFO:** 532 | 533 | ​ 原理:把内存中驻留时间最久的页面置换算法予以淘汰 534 | 535 | ​ 优点:实现简单、直观 536 | 537 | ​ 缺点:没有考虑到实际的页面使用频率,性能差、与通常页面使用的规则不符合,实际应用较少 538 | 539 | ​ 改进:给每个页面增加一个R位,每次先从链表头开始查找,如果R置位,清除R位并且把该页面节点放 到链表结尾;如果R是0,那么就是又老又没用到,替换掉。 540 | 541 | **最近最久未使用LRU:** 542 | 543 | ​ 原理:选择最近且最久未使用的页面进行淘汰 544 | 545 | ​ 优点:考虑到了程序访问的时间局部性,有较好的性能,实际应用也比较多 546 | 547 | ​ 缺点:实现需要比较多的硬件支持,会增加一些硬件成本 548 | 549 | ​ 手写LRU: 参考 https://www.jianshu.com/p/ec1952b9d84a 550 | 551 | ```java 552 | /** 553 | * @program: Java 554 | * @description: LRU最近最久未使用置换算法,通过LinkedHashMap实现 555 | * @author: Mr.Li 556 | * @create: 2020-07-17 10:29 557 | **/ 558 | public class LRUCache { 559 | private LinkedHashMap cache; 560 | private int capacity; //容量大小 561 | 562 | /** 563 | *初始化构造函数 564 | * @param capacity 565 | */ 566 | public LRUCache(int capacity) { 567 | cache = new LinkedHashMap<>(capacity); 568 | this.capacity = capacity; 569 | } 570 | 571 | public int get(int key) { 572 | //缓存中不存在此key,直接返回 573 | if(!cache.containsKey(key)) { 574 | return -1; 575 | } 576 | 577 | int res = cache.get(key); 578 | cache.remove(key); //先从链表中删除 579 | cache.put(key,res); //再把该节点放到链表末尾处 580 | return res; 581 | } 582 | 583 | public void put(int key,int value) { 584 | if(cache.containsKey(key)) { 585 | cache.remove(key); //已经存在,在当前链表移除 586 | } 587 | if(capacity == cache.size()) { 588 | //cache已满,删除链表头位置 589 | Set keySet = cache.keySet(); 590 | Iterator iterator = keySet.iterator(); 591 | cache.remove(iterator.next()); 592 | } 593 | cache.put(key,value); //插入到链表末尾 594 | } 595 | } 596 | 597 | ``` 598 | 599 | ```java 600 | /** 601 | * @program: Java 602 | * @description: LRU最近最久未使用置换算法,通过LinkedHashMap内部removeEldestEntry方法实现 603 | * @author: Mr.Li 604 | * @create: 2020-07-17 10:59 605 | **/ 606 | class LRUCache { 607 | private Map map; 608 | private int capacity; 609 | 610 | /** 611 | *初始化构造函数 612 | * @param capacity 613 | */ 614 | public LRUCache(int capacity) { 615 | this.capacity = capacity; 616 | map = new LinkedHashMap(capacity, 0.75f, true) { 617 | @Override 618 | protected boolean removeEldestEntry(Map.Entry eldest) { 619 | return size() > capacity; // 容量大于capacity 时就删除 620 | } 621 | }; 622 | } 623 | public int get(int key) { 624 | //返回key对应的value值,若不存在,返回-1 625 | return map.getOrDefault(key, -1); 626 | } 627 | 628 | public void put(int key, int value) { 629 | map.put(key, value); 630 | } 631 | } 632 | ``` 633 | 634 | **最佳置换算法OPT:** 635 | 636 | ​ 原理:每次选择当前物理块中的页面在未来长时间不被访问的或未来不再使用的页面进行淘汰 637 | 638 | ​ 优点:具有较好的性能,可以保证获得最低的缺页率 639 | 640 | ​ 缺点:过于理想化,但是实际上无法实现(没办法预知未来的页面) 641 | 642 | 643 | 644 | **8、死锁条件,解决方式。** 645 | 646 | ​ 死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的下相互等待的现象; 647 | 648 | ​ **死锁的条件:** 649 | 650 | ​ 互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源; 651 | 652 | ​ 请求与保持条件:进程获得一定的资源后,又对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但该进程不会释放自己已经占有的资源 653 | 654 | ​ 非剥夺条件:进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放 655 | 656 | ​ 循环等待条件:系统中若干进程组成环路,环路中每个进程都在等待相邻进程占用的资源 657 | 658 | ​ **解决方法:**破坏死锁的任意一条件 659 | 660 | ​ 资源一次性分配,从而剥夺请求和保持条件 661 | 662 | ​ 可剥夺资源:即当进程新的资源未得到满足时,释放已占有的资源,从而破坏不可剥夺的条件 663 | 664 | ​ 资源有序分配法:系统给每类资源赋予一个序号,每个进程按编号递增的请求资源,释放则相反,从而破坏环路等待的条件 665 | 666 | ## **三、Java基础篇** 667 | 668 | **1、Java面向对象特性介绍、与C++区别** 669 | 670 | **特性:**封装、继承、多态 671 | 672 | ​ **封装:**对抽象的事物抽象化成一个对象,并对其对象的属性私有化,同时提供一些能被外界访问属性的方法,这样一个对象便有存在的意义了; 673 | 674 | ​ **继承:**在已存在类的基础上,建立新类并对其增加新的数据域或功能,同时该类可以复用父类的属性与功能,这种思路可以称为继承;通过使用继承能够方便地复用旧代码,减少不必要的代码量; 675 | 676 | ​ **多态:**指程序中的某个引用变量,它所指向的具体类型以及该引用变量发出的方法调用,在编程时不能确定,要在程序运行并使用时由机器自己判别确定;实现多态的方式有两种方式,可以通过继承(多个⼦类对同⼀⽅法的重写)、也可以通过接⼝(实现接⼝并覆盖接⼝中同⼀⽅法) 677 | 678 | **Java与C++区别:** 679 | 680 | ​ 相同点:都是面向对象语言,并且都支持封装、继承、多态 681 | 682 | ​ 不同点:c++支持多继承,并且有指针的概念,由程序员自己管理内存;Java是单继承,可以用接口实现多继承,Java 不提供指针来直接访问内存,程序内存更加安全,并且Java有JVM⾃动内存管理机制,不需要程序员⼿动释放⽆⽤内存 683 | 684 | 685 | 686 | **2、多态实现原理** 687 | 688 | 参考:https://www.baidu.com/link?url=qCqeY8tJInTtjEXuotB712TJtEpKeLJ9ds1ACGEnYwMyJdjf6J0C7JvOEwbV7qNG&wd=&eqid=fe3b2106001ee6ab000000065f111b6c 689 | 690 | 多态的底层实现是动态绑定,即在运行时才把方法调用与方法实现关联起来。 691 | 692 | **静态绑定与动态绑定:** 693 | 694 | ​ JVM 的方法调用指令有五个,分别是: 695 | 696 | ​ invokestatic:调用静态方法; 697 | 698 | ​ invokespecial:调用实例构造器方法、私有方法和父类方法; 699 | 700 | ​ invokevirtual:调用虚方法; 701 | 702 | ​ invokeinterface:调用接口方法,运行时确定具体实现; 703 | 704 | ​ invokedynamic:运行时动态解析所引用的方法,然后再执行,用于支持动态类型语言。 705 | 706 | ​ invokestatic 和 invokespecial 用于静态绑定 707 | 708 | ​ invokevirtual 和 invokeinterface 用于动态绑定 709 | 710 | ​ 可以看出,动态绑定主要应用于虚方法和接口方法。 711 | 712 | ​ 虚方法的方法调用与方法实现的关联(也就是分派)有两种,一种是在编译期确定,被称为静态分派,比如方法的重载;一种是在运行时确定,被称为动态分派,比如方法的覆盖(重写)。对象方法基本上都是虚方法。 713 | 714 | **多态的实现** 715 | 716 | ​ 虚拟机栈中会存放当前方法调用的栈帧(局部变量表、操作栈、动态连接 、返回地址)。多态的实现过程,就是方法调用动态分派的过程,通过栈帧的信息去找到被调用方法的具体实现,然后使用这个具体实现的直接引用完成方法调用。 717 | 718 | 以 invokevirtual 指令为例,在执行时,大致可以分为以下几步: 719 | 720 | 1. 先从操作栈中找到对象的实际类型 class; 721 | 2. 找到 class 中与被调用方法签名相同的方法,如果有访问权限就返回这个方法的直接引用,如果没有访问权限就报错 java.lang.IllegalAccessError ; 722 | 3. 如果第 2 步找不到相符的方法,就去搜索 class 的父类,按照继承关系自下而上依次执行第 2 步的操作; 723 | 4. 如果第 3 步找不到相符的方法,就报错 java.lang.AbstractMethodError ; 724 | 725 | 可以看到,如果子类覆盖了父类的方法,则在多态调用中,动态绑定过程会首先确定实际类型是子类,从而先搜索到子类中的方法。这个过程便是方法覆盖的本质。 726 | 727 | 728 | 729 | **3、抽象类和接口区别,以及各自的使用场景** 730 | 731 | **抽象类:**包含抽象方法的类,即使用abstract修饰的类;不能使用final修饰,final修饰的类不能被继承;抽象类不能被实例化,只能被继承 732 | 733 | **接口:**接口是一个抽象类型,是抽象方法的集合,接口以interface来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法;接口只能继承接口,不能继承类,接口支持多继承;接口中的定义的成员变量,默认是public static final修饰的静态常量;接口中定义的方法,默认是public abstract修饰的抽象方法 734 | 735 | **相同点:** 736 | 737 | ​ ① 抽象类和接口都不能被实例化 738 | 739 | ​ ② 抽象类和接口都可以定义抽象方法,子类/实现类必须覆写这些抽象方法 740 | 741 | **不同点:** 742 | 743 | ​ ① 抽象类有构造方法,接口没有构造方法 744 | 745 | ​ ③抽象类可以包含普通方法,接口中只能是public abstract修饰抽象方法(Java8之后可以) 746 | 747 | ​ ③ 抽象类只能单继承,接口可以多继承 748 | 749 | ​ ④ 抽象类可以定义各种类型的成员变量,接口中只能是public static final修饰的静态常量 750 | 751 | 752 | **抽象类的使用场景:** 753 | 754 | ​ 既想约束子类具有共同的行为(但不再乎其如何实现),又想拥有缺省的方法,又能拥有实例变量 755 | 756 | **接口的应用场景:** 757 | 758 | ​ 约束多个实现类具有统一的行为,但是不在乎每个实现类如何具体实现;实现类需要具备很多不同的功能,但各个功能之间可能没有任何联系 759 | 760 | 761 | 762 | **4、泛型以及泛型擦除。List
类型的list,可以加入无继承关系的B类型对象吗?如何加入?** 763 | 764 | 参考:https://blog.csdn.net/baoyinwang/article/details/107341997 765 | 766 | **泛型:** 767 | 768 | ​ 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。 769 | 770 | **泛型擦除:** 771 | 772 | ​ Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。 773 | 774 | ​ 如在代码中定义的 List和 List等类型,在编译之后都会变成 List。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的。 775 | 776 | **如何加入:** 777 | 778 | ​ 通过反射添加其它类型元素 779 | 780 | ```java 781 | public class Test { 782 | public static void main(String[] args) throws Exception { 783 | 784 | ArrayList list = new ArrayList(); 785 | 786 | list.add(new A()); //这样调用 add 方法只能存储A,因为泛型类型的实例为 A 787 | 788 | list.getClass().getMethod("add", Object.class).invoke(list, new B()); 789 | 790 | for (int i = 0; i < list.size(); i++) { 791 | System.out.println(list.get(i)); 792 | } 793 | } 794 | 795 | } 796 | ``` 797 | 798 | 799 | 800 | **5、Java异常体系** 801 | 802 | ![](https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3137389296,1222888772&fm=26&gp=0.jpg) 803 | 804 | Throwable 是 Java 语言中所有错误或异常的超类。下一层分为 Error 和 Exception 805 | 806 | **Error :** 807 | 808 | ​ 是指 java 运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。如果出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。 809 | 810 | **Exception 包含:RuntimeException 、CheckedException** 811 | 812 | **RuntimeException:** 运 行 时 异 常 813 | 814 | ​ 如 NullPointerException 、 ClassCastException ; 815 | 816 | ​ RuntimeException 是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类,这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。 817 | 818 | 819 | 820 | **CheckedException:**受检异 常 821 | 822 | ​ 如 I/O 错误导致的 IOException、SQLException; 823 | 824 | ​ CheckedException:一般是外部错误,这种异常都发生在编译阶段,Java 编译器会强制程序去捕获此类 825 | 826 | 异常,即会出现要求你把这段可能出现异常的程序进行 try catch,该类异常一般包括几个方面: 827 | 828 | ​ ①试图在文件尾部读取数据 829 | 830 | ​ ②试图打开一个错误格式的 URL 831 | 832 | ​ ③试图根据给定的字符串查找 class 对象,而这个字符串表示的类并不存在 833 | 834 | 835 | 836 | **6、反射原理以及使用场景** 837 | 838 | **Java反射:** 839 | 840 | ​ 是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为 Java 语言的反射机制。 841 | 842 | **反射原理:** 843 | 844 | ​ 反射首先是能够获取到Java中的反射类的字节码,然后将字节码中的方法,变量,构造函数等映射成 相应的 Method、Filed、Constructor 等类 845 | 846 | ​ **如何得到Class的实例:** 847 | 848 | 1.类名.class(就是一份字节码) 849 | 2.Class.forName(String className);根据一个类的全限定名来构建Class对象 850 | 3.每一个对象多有getClass()方法:obj.getClass();返回对象的真实类型 851 | 852 | **使用场景:** 853 | 854 | ​ 逆向代码 ,例如反编译; 855 | 856 | ​ 动态生成类框架,如Spring:xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中; 2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的Class实例; 4)动态配置实例的属性 857 | 858 | 859 | 860 | **7、ThreadLocal原理,如何使用?** 861 | 862 | **ThreadLocal简介:** 863 | 864 | ​ 通常情况下,我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有⾃⼰的 865 | 专属本地变量该如何解决呢? JDK中提供的 ThreadLocal 类正是为了解决这样的问题。 866 | 867 | **原理:** 868 | 869 | ​ 首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。 870 | 871 | ​ 最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 872 | 873 | ​ 我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。例如下面的 874 | 875 | **set 方法:** 876 | 877 | ```java 878 | public void set(T value) { 879 | Thread t = Thread.currentThread(); 880 | ThreadLocalMap map = getMap(t); 881 | if (map != null) 882 | map.set(this, value); 883 | else 884 | createMap(t, value); 885 | } 886 | ``` 887 | 888 | **get方法:** 889 | 890 | ```java 891 | public T get() { 892 | Thread t = Thread.currentThread(); 893 | ThreadLocalMap map = getMap(t); 894 | if (map != null) 895 | return (T)map.get(this); 896 | 897 | // 如果不存在,则创建它 898 | T value = initialValue(); 899 | createMap(t, value); 900 | return value; 901 | } 902 | ``` 903 | 904 | **createMap方法:** 905 | 906 | ```java 907 | void createMap(Thread t, T firstValue) { 908 | t.threadLocals = new ThreadLocalMap(this, firstValue); 909 | } 910 | ``` 911 | 912 | **ThreadLocalMap是个静态的内部类:** 913 | 914 | ```java 915 | static class ThreadLocalMap { 916 | …… 917 | } 918 | ``` 919 | 920 | **如何使用:** 921 | 922 | ​ 1)存储用户Session 923 | 924 | ```java 925 | private static final ThreadLocal threadSession = new ThreadLocal(); 926 | 927 | public static Session getSession() throws InfrastructureException { 928 | Session s = (Session) threadSession.get(); 929 | try { 930 | if (s == null) { 931 | s = getSessionFactory().openSession(); 932 | threadSession.set(s); 933 | } 934 | } catch (HibernateException ex) { 935 | throw new InfrastructureException(ex); 936 | } 937 | return s; 938 | } 939 | ``` 940 | 941 | ​ 2)解决线程安全的问题 942 | 943 | ```java 944 | public class DateUtil { 945 | //SimpleDateFormat不是线程安全的,所以每个线程都要有⾃⼰独⽴的副本 946 | private static ThreadLocal format1 = new ThreadLocal() { 947 | @Override 948 | protected SimpleDateFormat initialValue() { 949 | return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 950 | } 951 | }; 952 | 953 | public static String formatDate(Date date) { 954 | return format1.get().format(date); 955 | } 956 | } 957 | ``` 958 | 959 | 960 | 961 | **8、ThreadLocal内存泄漏的场景** 962 | 963 | ​ 实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,⽽ value 是强引⽤。弱引用的特点是,如果这个对象持有弱引用,那么在下一次垃圾回收的时候必然会被清理掉。 964 | 965 | ​ 所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。 假如我们不做任何措施的话,value 永远⽆法被GC 回收,这个时候就可能会产⽣内存泄露。 966 | 967 | ​ ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。 968 | 969 | ​ 因此使⽤完ThreadLocal ⽅法后,最好⼿动调⽤ remove() ⽅法。 970 | 971 | 972 | 973 | **9、static关键字和final关键字使用情况,一个类不能被继承,除了final关键字之外,还有什么方法(从构造函数考虑)?** 974 | 975 | **static:**可以修饰属性、方法 976 | 977 | ​ **static修饰属性:** 978 | 979 | ​ 所有对象共享一份,一个对象对其修改,其他的调用也会受到影响,类级别;随着类的加载而加载(只加载一次),先于对象的创建;可以使用类名直接调用。 980 | 981 | ​ **static修饰方法:** 982 | 983 | ​ 随着类的加载而加载;可以使用类名直接调用;静态方法中,只能调用静态的成员;非静态的方法中,可以调用静态和非静态的成员;在静态方法中,不会出现this。 984 | 985 | **final:**关键字主要⽤在三个地⽅:变量、⽅法、类。 986 | 987 | ​ **final修饰变量:** 988 | 989 | ​ 对于⼀个 final 变量,如果是基本数据类型的变量,则其数值⼀旦在初始化之后便不能更改;如果是引⽤类型的变量,则在对其初始化之后便不能再让其指向另⼀个对象。 990 | 991 | ​ **final修饰方法:** 992 | 993 | ​ 把⽅法锁定,以防任何继承类修改它的含义(重写);类中所有的 private ⽅法都隐式地指定为 final。 994 | 995 | ​ **final修饰类:** 996 | 997 | ​ final 修饰类时,表明这个类不能被继承。final 类中的所有成员⽅法都会被隐式地指定为 final ⽅法。 998 | 999 | 1000 | 1001 | **10、序列化和反序列化。反序列化失败的场景。** 1002 | 1003 | ​ 序列化的意思就是将对象的状态转化成字节流,以后可以通过这些值再生成相同状态的对象。对象序列化是对象持久化的一种实现方法,它是将对象的属性和方法转化为一种序列化的形式用于存储和传输。反序列化就是根据这些保存的信息重建对象的过程。 1004 | 1005 | **序列化:**将java对象转化为字节序列的过程。 1006 | 1007 | **反序列化:**将字节序列转化为java对象的过程。 1008 | 1009 | **优点:** 1010 | 1011 | ​ a、实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里) 1012 | 1013 | ​ b、利用序列化实现远程通信,即在网络上传送对象的字节序列。 1014 | 1015 | **反序列化失败的场景:** 1016 | 1017 | ​ 序列化ID:serialVersionUID不一致的时候,导致反序列化失败 1018 | 1019 | 1020 | 1021 | **11、ArrayList和LinkedList的区别和底层实现?如何实现线程安全?** 1022 | 1023 | **ArrayList:** 1024 | 1025 | ​ 底层基于数组实现,支持对元素进行快速随机访问,支持元素重复;默认初始大小为10,当数组容量不够时,会触发扩容机制(扩大到当前的1.5倍),需要将原来数组的数据复制到新的数组中;当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。 1026 | 1027 | **LinkedList:** 1028 | 1029 | ​ 底层基于双向链表实现,适合数据的动态插入和删除;内部提供了 List 接口中没有定义的方法,用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。 1030 | 1031 | **ArrayList与LinkedList区别:** 1032 | 1033 | ​ 都是线程不安全的,ArrayList 适用于查找的场景,LinkedList 适用于 增加、删除多的场景 1034 | 1035 | **实现线程安全:** 1036 | 1037 | ​ 可以使用原生的**Vector**,或者是**Collections.synchronizedList(List list)**函数返回一个线程安全的ArrayList集合,或者使用concurrent并发包下的**CopyOnWriteArrayList**的。 1038 | 1039 | ​ ①、**Vector:** 底层通过synchronize修饰保证线程安全,效率较差 1040 | 1041 | ​ ② 、**Collections.synchronizedList(List list):** 1042 | 1043 | ```java 1044 | //使用Collections.synchronizedList(List list)方法实现线程安全 1045 | List list=Collections.synchronizedList(new ArrayList<>()); 1046 | ``` 1047 | 1048 | ​ ③、**CopyOnWriteArrayList:**写时加锁,使用了一种叫**写时复制**的方法;读操作是可以不用加锁的 1049 | 1050 | ​ 1051 | 1052 | **12、List遍历时如何删除元素?fail—fast是什么?fail—safe是什么?** 1053 | 1054 | **①、普通for循环遍历List删除指定元素** 1055 | 1056 | ```java 1057 | for(int i=0; i < list.size(); i++){ 1058 | if(list.get(i) == 5) 1059 | list.remove(i); 1060 | } 1061 | ``` 1062 | 1063 | **② 、迭代遍历,用list.remove(i)方法删除元素** 1064 | 1065 | ```JAVA 1066 | Iterator it = list.iterator(); 1067 | while(it.hasNext()){ 1068 | Integer value = it.next(); 1069 | if(value == 5){ 1070 | list.remove(value); 1071 | } 1072 | } 1073 | ``` 1074 | 1075 | **③、foreach遍历List删除元素** 1076 | 1077 | ```JAVA 1078 | for(Integer i:list){ 1079 | if(i==3) list.remove(i); 1080 | } 1081 | ``` 1082 | 1083 | **fail—fast:**快速失败 1084 | 1085 | ​ 当异常产生时,直接抛出异常,程序终止; 1086 | 1087 | ​ fail-fast只要是体现在当我们在遍历集合元素的时候,经常会使用迭代器,但在迭代器遍历元素的过程中,如果集合的结构被改变的话,就会抛出异常ConcurrentModificationException,防止继续遍历。这就是所谓的快速失败机制。这里要注意的这里说的结构被改变,是例如插入和删除这种操作,只是改变集合里的值的话并不会抛出异常。 1088 | 1089 | **fail—safe:**安全失败 1090 | 1091 |     采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。 1092 | 1093 |     原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException。 1094 | 1095 |     缺点:基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。 1096 | 1097 |     场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。 1098 | 1099 | 1100 | 1101 | **13、详细介绍HashMap。** 1102 | 1103 | 角度:数据结构+扩容情况+put查找的详细过程+哈希函数+容量为什么始终都是2^N,JDK1.7与1.8的区别。 1104 | 1105 | 参考:https://www.jianshu.com/p/9fe4cb316c05 1106 | 1107 | **数据结构:** 1108 | 1109 | ​ HashMap在底层数据结构上采用了数组+链表+红黑树,通过散列映射来存储键值对数据 1110 | 1111 | **扩容情况:** 1112 | 1113 | ​ 默认的负载因子是0.75,表示的是,如果数组中已经存储的元素个数大于数组长度的75%,将会引发扩容操作。 1114 | 1115 | ​ 【1】创建一个长度为原来数组长度**两倍的新数组**。 1116 | 1117 | ​ 【2】重新对原数组中的Entry对象进行哈希运算,以确定他们各自在新数组中的新位置。 1118 | 1119 | **put操作步骤:** 1120 | 1121 | ​ 1、判断数组是否为空,为空进行初始化; 1122 | 1123 | ​ 2、不为空,则计算 key 的 hash 值,通过(n - 1) & hash计算应当存放在数组中的下标 index; 1124 | 1125 | ​ 3、查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在 table[index] 中; 1126 | 1127 | ​ 4、存在数据,说明发生了hash冲突(存在二个节点key的hash值一样), 继续判断key是否相等,相等,用新的value替换原数据; 1128 | 1129 | ​ 5、若不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中; 1130 | 1131 | ​ 6、若不是红黑树,创建普通Node加入链表中;判断链表长度是否大于 8,大于则将链表转换为红黑树; 1132 | 1133 | ​ 7、插入完成之后判断当前节点数是否大于阈值,若大于,则扩容为原数组的二倍 1134 | 1135 | **哈希函数:** 1136 | 1137 | ​ hash函数是先拿到 key 的hashcode,是一个32位的值,然后让hashcode的高16位和低16位进行**异或**操作。该函数也称为扰动函数,做到尽可能降低hash碰撞。 1138 | 1139 | **容量为什么始终都是2^N:** 1140 | 1141 | ​ 为了能让 HashMap 存取⾼效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上⾯也讲到了过了,Hash 值的范围值-2147483648到2147483647,前后加起来⼤概40亿的映射空间,只要哈希函数映射得⽐较均匀松散,⼀般应⽤是很难出现碰撞的。但问题是⼀个40亿⻓度的数组,内存是放不下的。所以这个散列值是不能直接拿来⽤的。⽤之前还要先做对数组的⻓度取模运算,得到的余数才能⽤来要存放的位置也就是对应的数组下标。这个数组下标的计算⽅法是“ (n - 1) & hash ”。(n代表数组⻓度)。这也就解释了 HashMap 的⻓度为什么是2的幂次⽅。 1142 | 1143 | **JDK1.7与1.8的区别:** 1144 | 1145 | J**DK1.7 HashMap:** 1146 | 1147 | ​ 底层是 **数组和链表** 结合在⼀起使⽤也就是 链表散列。HashMap 通过 key 的hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这⾥的 n 指的是数组的⻓度),如果当前位置存在元素的话,就判断该元素与要存⼊的元素的 hash值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。 1148 | 1149 | **DK1.8 HashMap:** 1150 | 1151 | ​ HashMap在底层数据结构上采用了数组+链表+红黑树,通过散列映射来存储键值对数据;当链表⻓度⼤于阈值(默认为 8),数组的⻓度大于 64时,将链表转化为红⿊树,以减少搜索时间 1152 | 1153 | 1154 | 1155 | 1156 | 1157 | **14、HashMap如何实现线程安全?ConcurrentHashMap的底层实现?JDK1.7与JDK1.8的区别** 1158 | 1159 | ​ 可以通过**ConcurrentHashMap** 和 **Hashtable**来实现线程安全;Hashtable 是原始API类,通过synchronize同步修饰,效率低下;ConcurrentHashMap 通过分段锁实现,效率较比Hashtable要好; 1160 | 1161 | **ConcurrentHashMap的底层实现:** 1162 | 1163 | ​ **JDK1.7的 ConcurrentHashMap** 底层采⽤ 分段的数组+链表 实现;采用 **分段锁**(Sagment) 对整个桶数组进⾏了分割分段(Segment),每⼀把锁只锁容器其中⼀部分数据,多线程访问容器⾥不同数据段的数据,就不会存在锁竞争,提⾼并发访问率。 1164 | 1165 | ![](https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=1035283471,1167301443&fm=26&gp=0.jpg) 1166 | 1167 | ​ **JDK1.8的 ConcurrentHashMap** 采⽤的数据结构跟HashMap1.8的结构⼀样,数组+链表/红⿊⼆叉树;摒弃了Segment的概念,⽽是直接⽤ Node 数组+链表+红⿊树的数据结构来实现,通过并发控制 **synchronized 和CAS**来操作保证线程的安全。 1168 | 1169 | 1170 | 1171 | **15、正则表达式会写吗?** 1172 | 1173 | 参考:https://blog.csdn.net/qq_39331713/article/details/82871510 1174 | 1175 | ​ 正则通过一些特定的符号与数字来表示一串字符,其中有:元字符、重复限定符、分组、转义、条件或、区间; 1176 | 1177 | 1178 | 1179 | **16、设计模式了解吗?** 1180 | 1181 | 单例模式、工厂模式、代理模式 1182 | 1183 | 1184 | 1185 | **17、linux指令知道哪些?** 1186 | 1187 | 文件管理:ls、cd、touch创建普通文件、rm删除、mkdir新建目录、mv移动、cp拷贝、chmod修改权限 1188 | 1189 | 进程管理:ps显示进程信息、kill杀死进程 1190 | 1191 | 系统管理:top、free显示系统运行信息、vmstat输出各资源使用情况 1192 | 1193 | 网络通讯:ping测试网络连通性、netstat显示网络相关信息 1194 | 1195 | 1196 | 1197 | **18、JVM相关** 1198 | 1199 | **1、JVM运行时内存划分?** 1200 | 1201 | **JVM运行时数据区域:**堆、方法区(元空间)、虚拟机栈、本地方法栈、程序计数器 1202 | 1203 | ![](https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3099290709,1184193923&fm=26&gp=0.jpg) 1204 | 1205 | **Heap(堆):** 1206 | 1207 | ​ 对象的实例以及数组的内存都是要在堆上进行分配的,堆是线程共享的一块区域,用来存放对象实例,也是垃圾回收(GC)的主要区域; 1208 | 1209 | ​ 堆细分:新生代、老年代,对于新生代又分为:Eden区和Surviver1和Surviver2区; 1210 | 1211 | **方法区:** 1212 | 1213 | ​ 对于JVM的方法区也可以称之为永久区,它储存的是已经被java虚拟机加载的类信息、常量、静态变量;Jdk1.8以后取消了方法区这个概念,称之为元空间(MetaSpace); 1214 | 1215 | **虚拟机栈:** 1216 | 1217 | ​ 虚拟机栈**是线程私有的**,他的生命周期和线程的生命周期是一致的。里面装的是一个一个的**栈帧**,每一个方法在执行的时候都会创建一个栈帧,栈帧中用来存放(**局部变量表**、**操作数栈** 、**动态链接** 、**返回地址**);在Java虚拟机规范中,对此区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出**StackOverflowError**异常;如果虚拟机栈动态扩展时无法申请到足够的内存,就会抛出**OutOfMemoryError**异常。 1218 | 1219 | - **局部变量表:**局部变量表是一组变量值存储空间,用来存放**方法参数**、方法内部定义的**局部变量**。局部变量表的容量是以变量槽(variable slot)为最小的单位。Java虚拟机没有明确规定一个slot所占的空间大小。只是导向性的说了每一个slot能存放8种基本数据类型中的一种(long 和double这种64位的需要两个slot); 1220 | 1221 | - **操作数栈:**是用来记录一个方法在执行的过程中,**字节码指令向操作数栈中进行入栈和出栈的过程**。大小在编译的时候已经确定了,当一个方法刚开始执行的时候,操作数栈中是空发的,在方法执行的过程中会有各种**字节码指令**往操作数栈中**入栈和出栈**。 1222 | 1223 | - **动态链接:**因为字节码文件中有很多符号的引用,这些符号引用一部分会在**类加载的解析阶段**或**第一次使用**的时候转化成**直接引用**,这种称为**静态解析**;另一部分会**在运行期间**转化为直接引用,称为**动态链接**。 1224 | 1225 | - **返回地址(returnAddress):**类型(指向了一条字节码指令的地址) 1226 | 1227 | 1228 | 1229 | **本地方法栈:** 1230 | 1231 | ​ 本地方法栈和虚拟机栈类似,不同的是虚拟机栈服务的是Java方法,而**本地方法栈服务的是Native方法**。在HotSpot虚拟机实现中是把本地方法栈和虚拟机栈合二为一的,同理它也会抛出**StackOverflowError**和**OOM**异常。 1232 | 1233 | **PC程序计数器:** 1234 | 1235 | ​ PC,指的是存放下一条指令的位置的这么一个区域。它是一块较小的内存空间,且是**线程私有**的。由于线程的切换,CPU在执行的过程中,一个线程执行完了,接下来CPU切换到另一个线程去执行,另外一个线程执行完再切回到之前的线程,这时需要记住原线程的下一条指令的位置,所以每一个线程都需要有自己的PC。 1236 | 1237 | 1238 | 1239 | **2、堆内存分配策略** 1240 | 1241 | ![](https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3784749479,768026745&fm=26&gp=0.jpg) 1242 | 1243 | - 对象优先分配在Eden区,如果Eden区没有足够的空间进行分配时,虚拟机执行一次MinorGC。而那些无需回收的存活对象,将会进到 Survivor 的 From 区(From 区内存不足时,直接进入 Old 区)。 1244 | 1245 | - 大对象直接进入老年代(需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。 1246 | 1247 | - 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄(Age Count)计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,直到达到阀值(默认15次),对象进入老年区。 1248 | 1249 | - 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。 1250 | 1251 | 1252 | 1253 | **3、Full GC触发条件** 1254 | 1255 | ​ 每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小,则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC 1256 | 1257 | 1258 | 1259 | **4、如何判断对象是否存活?回收对象的两次标记过程。** 1260 | 1261 | **引用计数法:** 1262 | 1263 | ​ 给对象添加一个引用计数器,每当由一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。 1264 | 1265 | ​ 优点:实现简单,判定效率也很高 1266 | 1267 | ​ 缺点:他很难解决对象之间相互循环引用的问题。 1268 | 1269 | **对象可达性:** 1270 | 1271 | ​ 通过一系列的成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC ROOTS没有任何引用链相连时,则证明此对象时不可用的; 1272 | 1273 | **两次标记过程:** 1274 | 1275 | ​ 对象被回收之前,该对象的finalize()方法会被调用;两次标记,即第一次标记不在“关系网”中的对象。第二次的话就要先判断该对象有没有实现finalize()方法了,如果没有实现就直接判断该对象可回收;如果实现了就会先放在一个队列中,并由虚拟机建立的一个低优先级的线程去执行它,随后就会进行第二次的小规模标记,在这次被标记的对象就会真正的被回收了。 1276 | 1277 | 1278 | 1279 | **5、垃圾回收算法以及垃圾回收器介绍,尤其是G1和CMS的优缺点** 1280 | 1281 | **垃圾回收算法**:复制算法、标记清除、标记整理、分代收集 1282 | 1283 | **复制算法:** 1284 | 1285 | ​ 将内存分为⼤⼩相同的两块,每次使⽤其中的⼀块。当这⼀块的内存使⽤完后,就将还存活的对象复制到另⼀块去,然后再把使⽤的空间⼀次清理掉。这样就使每次的内存回收都是对内存区间的⼀半进⾏回收; 1286 | 1287 | ​ 优点:实现简单,内存效率高,不易产生碎片 1288 | 1289 | ​ 缺点:内存压缩了一半,倘若存活对象多,Copying 算法的效率会大大降低 1290 | 1291 | **标记清除:** 1292 | 1293 | ​ 标记出所有需要回收的对象,在标记完成后统⼀回收所有被标记的对象 1294 | 1295 | ​ 缺点:效率低,标记清除后会产⽣⼤量不连续的碎⽚,可能发生大对象不能找到可利用空间的问题。 1296 | 1297 | **标记整理:** 1298 | 1299 | ​ 标记过程仍然与“标记-清除”算法⼀样,再让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存;解决了产生大量不连续碎片问题 1300 | 1301 | **分代收集:** 1302 | 1303 | ​ 根据各个年代的特点选择合适的垃圾收集算法。 1304 | 1305 | ​ 新生代采用复制算法,新生代每次垃圾回收都要回收大部分对象,存活对象较少,即要复制的操作比较少,一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。 1306 | 1307 | ​ 老年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进⾏垃圾收集。 1308 | 1309 | 1310 | 1311 | **垃圾收集器:Serial、Parnew、parallel Scavenge、Serialold 、Parnewold、CMS、G1** 1312 | 1313 | **Serial:** 1314 | 1315 | ​ Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。 1316 | 1317 | **Parnew:** 1318 | 1319 | ​ ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。 1320 | 1321 | **parallel Scavenge:** 1322 | 1323 | ​ Parallel Scavenge收集器关注点是吞吐量(⾼效率的利⽤CPU)。CMS等垃圾收集器的关注点更多的是⽤户线程的停顿时间(提⾼⽤户体验);高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。 1324 | 1325 | **Serial old:** 1326 | 1327 | Serial收集器的⽼年代版本,它同样是⼀个单线程收集器,使用标记-整理算法。主要有两个用途: 1328 | 1329 | - 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。 1330 | 1331 | - 作为年老代中使用 CMS 收集器的后备垃圾收集方案。 1332 | 1333 | **parallel old:** 1334 | 1335 | ​ Parallel Scavenge收集器的⽼年代版本。使⽤多线程和“标记-整理”算法。 1336 | 1337 | **CMS:**重要 1338 | 1339 | ​ CMS收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段: 1340 | 1341 | ​ **初始标记:**只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。 1342 | 1343 | ​ **并发标记:**进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。 1344 | 1345 | ​ **重新标记:**为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。 1346 | 1347 | ​ **并发清除:**清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。 1348 | 1349 | ​ **优点:**并发收集、低停顿 1350 | 1351 | ​ **缺点:**对CPU资源敏感;⽆法处理浮动垃圾;使⽤“标记清除”算法,会导致⼤量空间碎⽚产⽣。 1352 | 1353 | 1354 | 1355 | **G1:**重要 1356 | 1357 | ​ 是⼀款⾯向服务器的垃圾收集器,主要针对配备多颗处理器及⼤容量内存的机器.以极⾼概率满⾜GC停顿时间要求的同时,还具备⾼吞吐量性能特征;相比与 CMS 收集器,G1 收集器两个最突出的改进是: 1358 | 1359 | ​ 【1】基于标记-整理算法,不产生内存碎片。 1360 | 1361 | ​ 【2】可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。 1362 | 1363 | ​ G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。**区域划分**和**优先级区域**回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。 1364 | 1365 | 1366 | 1367 | **6、创建一个对象的步骤** 1368 | 1369 | **步骤:类加载检查、分配内存、初始化零值、设置对象头、执行init方法** 1370 | 1371 | **①类加载检查:** 1372 | 1373 | ​ 虚拟机遇到⼀条 new 指令时,⾸先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执⾏相应的类加载过程。 1374 | 1375 | **②分配内存:** 1376 | 1377 | ​ 在类加载检查通过后,接下来虚拟机将为新⽣对象分配内存。对象所需的内存⼤⼩在类加载完成后便可确定,为对象分配空间的任务等同于把⼀块确定⼤⼩的内存从 Java 堆中划分出来。分配⽅式有 **“指针碰撞”** 和 **“空闲列表”** 两种,选择那种分配⽅式由 Java 堆是否规整决定,⽽Java堆是否规整⼜由所采⽤的垃圾收集器是否带有压缩整理功能决定。 1378 | 1379 | **③初始化零值:** 1380 | 1381 | ​ 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,这⼀步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使⽤,程序能访问到这些字段的数据类型所对应的零值。 1382 | 1383 | **④设置对象头:** 1384 | 1385 | ​ 初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运⾏状态的不同,如是否启⽤偏向锁等,对象头会有不同的设置⽅式。 1386 | 1387 | **⑤执⾏ init ⽅法:** 1388 | 1389 | ​ 在上⾯⼯作都完成之后,从虚拟机的视⻆来看,⼀个新的对象已经产⽣了,但从Java 程序的视⻆来看,对象创建才刚开始, ⽅法还没有执⾏,所有的字段都还为零。所以⼀般来说,执⾏ new 指令之后会接着执⾏ ⽅法,把对象按照程序员的意愿进⾏初始化,这样⼀个真正可⽤的对象才算完全产⽣出来。 1390 | 1391 | 1392 | 1393 | **7、详细介绍类加载过程** 1394 | 1395 | **过程:加载、验证、准备、解析、初始化** 1396 | 1397 | ![](https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2723431542,2953407468&fm=26&gp=0.jpg) 1398 | 1399 | **加载阶段:** 1400 | 1401 | ​ 1.通过一个类的全限定名来获取定义此类的二进制字节流。 1402 | 1403 | ​ 2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。 1404 | 1405 | ​ 3.在Java堆中生成一个代表这个类的java.lang.class对象,作为方法区这些数据的访问入口。 1406 | 1407 | **验证阶段:** 1408 | 1409 | ​ 1.文件格式验证(是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理) 1410 | 1411 | ​ 2.元数据验证(对字节码描述的信息进行语意分析,以保证其描述的信息符合Java语言规范要求) 1412 | 1413 | ​ 3.字节码验证(保证被校验类的方法在运行时不会做出危害虚拟机安全的行为) 1414 | 1415 | ​ 4.符号引用验证(虚拟机将符号引用转化为直接引用时,解析阶段中发生) 1416 | 1417 | **准备阶段:** 1418 | 1419 | ​ 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。将对象初始化为“零”值 1420 | 1421 | **解析阶段:** 1422 | 1423 | ​ 解析阶段时虚拟机将常量池内的符号引用替换为直接引用的过程。 1424 | 1425 | **初始化阶段:** 1426 | 1427 | ​ 初始化阶段时加载过程的最后一步,而这一阶段也是真正意义上开始执行类中定义的Java程序代码。 1428 | 1429 | 1430 | 1431 | **8、双亲委派机制,使用这个机制的好处?如何破坏?** 1432 | 1433 | ​ 每⼀个类都有⼀个对应它的类加载器。系统中的 ClassLoder 在协同⼯作的时候会默认使⽤ 双亲委派模型 。即在类加载的时候,系统会⾸先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,⾸先会把该请求委派该⽗类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当⽗类加载器⽆法处理时,才由⾃⼰来处理。当⽗类加载器为null时,会使⽤启动类加载器 BootstrapClassLoader 作为⽗类加载器。 1434 | 1435 | ![](https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1595226330267&di=5597292eb6bf5505abbec489fd54dacd&imgtype=0&src=http%3A%2F%2Fpics4.baidu.com%2Ffeed%2Fca1349540923dd54fb52494e811379d89c824890.jpeg%3Ftoken%3Dc280035219d435ec0f1c3a3a0a071101) 1436 | 1437 | **使用好处:** 1438 | 1439 | ​ 此机制保证JDK核心类的优先加载;使得Java程序的稳定运⾏,可以避免类的重复加载,也保证了 Java 的核⼼ API 不被篡改。如果不⽤没有使⽤双亲委派模型,⽽是每个类加载器加载⾃⼰的话就会出现⼀些问题,⽐如我们编写⼀个称为 java.lang.Object 类的话,那么程序运⾏的时候,系统就会出现多个不同的Object 类。 1440 | 1441 | **破坏双亲委派机制:** 1442 | 1443 | ​ 可以⾃⼰定义⼀个类加载器,重写loadClass方法; 1444 | 1445 | 1446 | 1447 | **9、了解下tomcat的类加载机制** 1448 | 1449 | **步骤:** 1450 | 1451 | 1. 先在本地cache查找该类是否已经加载过,看看 Tomcat 有没有加载过这个类。 1452 | 2. 如果Tomcat 没有加载过这个类,则从系统类加载器的cache中查找是否加载过。 1453 | 3. 如果没有加载过这个类,尝试用ExtClassLoader类加载器类加载,重点来了,这里并没有首先使用 AppClassLoader 来加载类。这个Tomcat 的 WebAPPClassLoader 违背了双亲委派机制,直接使用了 ExtClassLoader来加载类。这里注意 ExtClassLoader 双亲委派依然有效,ExtClassLoader 就会使用 Bootstrap ClassLoader 来对类进行加载,保证了 Jre 里面的核心类不会被重复加载。 比如在 Web 中加载一个 Object 类。WebAppClassLoader → ExtClassLoader → Bootstrap ClassLoader,这个加载链,就保证了 Object 不会被重复加载。 1454 | 4. 如果 BoostrapClassLoader,没有加载成功,就会调用自己的 findClass 方法由自己来对类进行加载,findClass 加载类的地址是自己本 web 应用下的 class。 1455 | 5. 加载依然失败,才使用 AppClassLoader 继续加载。 1456 | 6. 都没有加载成功的话,抛出异常。 1457 | 1458 | 总结一下以上步骤,WebAppClassLoader 加载类的时候,故意打破了JVM 双亲委派机制,绕开了 AppClassLoader,直接先使用 ExtClassLoader 来加载类。 1459 | 1460 | 1461 | 1462 | **10、JVM性能调优,常用命令,以及工具** 1463 | 1464 | 对应进程的JVM状态以定位问题和解决问题并作出相应的优化 1465 | 1466 | **常用命令:**jps、jinfo、jstat、jstack、jmap 1467 | 1468 | **jps:查看java进程及相关信息** 1469 | 1470 | ```java 1471 | jps -l 输出jar包路径,类全名 1472 | jps -m 输出main参数 1473 | jps -v 输出JVM参数 1474 | ``` 1475 | 1476 | **jinfo:查看JVM参数** 1477 | 1478 | ```java 1479 | jinfo 11666 1480 | jinfo -flags 11666 1481 | ``` 1482 | 1483 | **jstat:查看JVM运行时的状态信息,包括内存状态、垃圾回收** 1484 | 1485 | ```java 1486 | 命令格式: 1487 | jstat [option] LVMID [interval] [count] 1488 | 其中LVMID是进程id,interval是打印间隔时间(毫秒),count是打印次数(默认一直打印) 1489 | 1490 | option参数解释: 1491 | -class class loader的行为统计 1492 | -compiler HotSpt JIT编译器行为统计 1493 | -gc 垃圾回收堆的行为统计 1494 | -gccapacity 各个垃圾回收代容量(young,old,perm)和他们相应的空间统计 1495 | -gcutil 垃圾回收统计概述 1496 | -gccause 垃圾收集统计概述(同-gcutil),附加最近两次垃圾回收事件的原因 1497 | -gcnew 新生代行为统计 1498 | -gcnewcapacity 新生代与其相应的内存空间的统计 1499 | -gcold 年老代和永生代行为统计 1500 | -gcoldcapacity 年老代行为统计 1501 | -gcpermcapacity 永生代行为统计 1502 | -printcompilation HotSpot编译方法统计 1503 | ``` 1504 | 1505 | **jstack:查看JVM线程快照,jstack命令可以定位线程出现长时间卡顿的原因,例如死锁,死循环** 1506 | 1507 | ```java 1508 | 命令格式: 1509 | jstack [-l] (连接运行中的进程) 1510 | jstack -F [-m] [-l] (连接挂起的进程) 1511 | jstack [-m] [-l] (连接core文件) 1512 | jstack [-m] [-l] [server_id@] (连接远程debug服务器) 1513 | 1514 | option参数解释: 1515 | -F 当使用jstack 无响应时,强制输出线程堆栈。 1516 | -m 同时输出java和本地堆栈(混合模式) 1517 | -l 额外显示锁信息 1518 | ``` 1519 | 1520 | **jmap:可以用来查看内存信息** 1521 | 1522 | ```java 1523 | 命令格式: 1524 | jmap [option] (连接正在执行的进程) 1525 | jmap [option] (连接一个core文件) 1526 | jmap [option] [server_id@] (链接远程服务器) 1527 | 1528 | option参数解释: 1529 | to print same info as Solaris pmap 1530 | -heap 打印java heap摘要 1531 | -histo[:live] 打印堆中的 java对象统计信息 1532 | -clstats 打印类加载器统计信息 1533 | -finalizerinfo 打印在f-queue中等待执行finalizer方法的对象 1534 | -dump: 生成java堆的dump文件 1535 |       dump-options: 1536 |       live 只转储存活的对象,如果没有指定则转储所有对象 1537 |       format=b 二进制格式 1538 |       file= 转储文件到 1539 | -F 强制选项 1540 | ``` 1541 | 1542 | 1543 | 1544 | ## **四、多线程并发篇** 1545 | 1546 | **1、进程线程区别,线程安全和非线程安全区别** 1547 | 1548 | ​ 进程是程序的运行过程,是资源分配的基本单位,进程中可以包含多个线程,多个线程共享进程中堆、方法区资源 1549 | 1550 | ​ 线程是cpu任务调度的最小执行单位,每个线程拥有自己独立的程序计数器、虚拟机栈、本地方法栈 1551 | 1552 | **线程安全:**多个线程对同一资源操作,不会互相影响 1553 | 1554 | **非线程安全:**多个线程对同一资源操作,会互相影响 1555 | 1556 | 1557 | 1558 | 1559 | 1560 | **2、线程状态,start,run,wait,notify,yiled,sleep,join等方法的作用以及区别** 1561 | 1562 | **线程状态:创建、就绪、运行、阻塞、死亡** 1563 | 1564 | | 方法 | 作用 | 区别 | 1565 | | --------- | ------------------------------------------------------ | ---------------- | 1566 | | start | 启动线程,由虚拟机自动调度执行run()方法 | 线程处于就绪状态 | 1567 | | run | 线程逻辑代码块处理,JVM调度执行 | 线程处于运行状态 | 1568 | | sleep | 让当前正在执行的线程休眠(暂停执行) | 不释放锁 | 1569 | | wait | 使得当前线程等待 | 释放同步锁 | 1570 | | notify | 唤醒在此对象监视器上等待的单个线程 | 唤醒单个线程 | 1571 | | notifyAll | 唤醒在此对象监视器上等待的所有线程 | 唤醒多个线程 | 1572 | | yiled | 停止当前线程,让同等优先权的线程运行 | 用Thread类调用 | 1573 | | join | 使当前线程停下来等待,直至另一个调用join方法的线程终止 | 用线程对象调用 | 1574 | 1575 | 1576 | 1577 | **3、wait,notify,notifyAll阻塞唤醒确切过程?** 1578 | 1579 | 在哪阻塞,在哪唤醒?为什么要出现在同步代码块中? 1580 | 1581 | **阻塞:** 1582 | 1583 | ​ 这三个方法的调用都会使当前线程阻塞。该线程将会被放置到对该Object的请求等待队列中,然后让出当前对Object所拥有的所有的同步请求。线程会一直暂停所有线程调度,直到下面其中一种情况发生: 1584 | 1585 |     ① 其他线程调用了该Object的notify方法,而该线程刚好是那个被唤醒的线程; 1586 | 1587 |     ② 其他线程调用了该Object的notifyAll方法; 1588 | 1589 | **唤醒:** 1590 | 1591 | ​ 线程将会从等待队列中移除,重新成为可调度线程。它会与其他线程以常规的方式竞争对象同步请求。**一旦它重新获得对象的同步请求,所有之前的请求状态都会恢复,也就是线程调用wait的地方的状态。线程将会在之前调用wait的地方继续运行下去。** 1592 | 1593 | **原因:** 1594 | 1595 | ​ 由于`wait()属于Object方法,调用之后会强制释放当前对象锁,所以在wait()` 调用时必须拿到当前对象的监视器monitor对象。因此,wait()方法在同步方法/代码块中调用。 1596 | 1597 | 1598 | 1599 | **4、守护线程,线程中断** 1600 | 1601 | **守护线程:** 1602 | 1603 | ​ t.setDaemon(true)为守护线程,也叫精灵线程,若主线程启动t线程,则t线程是主线程的守护线程,当主线程执行完了,则守护线程也随之结束。 1604 | 1605 | ```java 1606 | 1607 | public class ThreadDaemon extends Thread{ 1608 | 1609 | public ThreadDaemon(String name){ 1610 | super(name); 1611 | } 1612 | 1613 | @Override 1614 | public void run() { 1615 | while(true){ 1616 | System.out.println(Thread.currentThread().getName() + "线程运行了。。。"); 1617 | try { 1618 | Thread.sleep(1000); 1619 | } catch (InterruptedException e) { 1620 | e.printStackTrace(); 1621 | } 1622 | } 1623 | } 1624 | 1625 | public static void main(String[] args) { 1626 | Thread t1 = new ThreadDaemon("线程一"); 1627 | Thread t2 = new ThreadDaemon("线程二"); 1628 | //设置为守护线程 1629 | t1.setDaemon(true); 1630 | t2.setDaemon(true); 1631 | //启动线程 1632 | t1.start(); 1633 | t2.start(); 1634 | //主线程2s后退出 1635 | try { 1636 | Thread.sleep(2000); 1637 | } catch (InterruptedException e) { 1638 | e.printStackTrace(); 1639 | } 1640 | } 1641 | 1642 | 1643 | } 1644 | ``` 1645 | 1646 | **线程中断:** 1647 | 1648 | ​ t.interrupt();调用interrupt()不会让线程立即中断,只是线程的中断状态发生变化,系统会在后续中断该线程 1649 | 1650 | ```java 1651 | 1652 | public class ThreadInterrupt extends Thread{ 1653 | 1654 | public ThreadInterrupt(String name){ 1655 | super(name); 1656 | } 1657 | 1658 | @Override 1659 | public void run() { 1660 | while(!interrupted()){//中断状态判断 1661 | System.err.println(Thread.currentThread().getName() + "线程运行了。。。"); 1662 | try { 1663 | Thread.sleep(1000); 1664 | } catch (InterruptedException e) { 1665 | e.printStackTrace(); 1666 | } 1667 | } 1668 | } 1669 | 1670 | public static void main(String[] args) { 1671 | Thread t1 = new ThreadInterrupt("线程一"); 1672 | Thread t2 = new ThreadInterrupt("线程二"); 1673 | //启动线程 1674 | t1.start(); 1675 | t2.start(); 1676 | t1.interrupt(); 1677 | } 1678 | 1679 | 1680 | } 1681 | ``` 1682 | 1683 | 1684 | 1685 | **5、Java乐观锁机制,CAS思想?缺点?是否原子性?如何保证?** 1686 | 1687 | **java乐观锁机制:** 1688 | 1689 | ​ 乐观锁体现的是悲观锁的反面。它是一种积极的思想,它总是认为数据是不会被修改的,所以是不会对数据上锁的。但是乐观锁在更新的时候会去判断数据是否被更新过。乐观锁的实现方案一般有两种(版本号机制和CAS)。乐观锁适用于**读多写少的场景,这样可以提高系统的并发量**。在Java中 **java.util.concurrent.atomic**下的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。 1690 | 1691 |   乐观锁,大多是基于数据版本 (Version)记录机制实现。即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来 实现。 读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提 交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据 版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。 1692 | 1693 | **CAS思想:** 1694 | 1695 | ​ CAS就是compare and swap(**比较交换**),是一种很出名的无锁的算法,就是可以不使用锁机制实现线程间的同步。使用CAS线程是不会被阻塞的,所以又称为非阻塞同步。CAS算法涉及到三个操作: 1696 | 1697 | ​ 需要读写内存值V; 1698 | 1699 | ​ 进行比较的值A; 1700 | 1701 | ​ 准备写入的值B 1702 | 1703 | ​ 当且仅当V的值等于A的值等于V的值的时候,才用B的值去更新V的值,否则不会执行任何操作(比较和替换是一个原子操作-A和V比较,V和B替换),一般情况下是一个**自旋操作**,即**不断重试** 1704 | 1705 | **缺点:** 1706 | 1707 | ​ [ABA问题-知乎](https://www.zhihu.com/question/23281499/answer/854522984) 1708 | 1709 | ​ 高并发的情况下,很容易发生并发冲突,如果CAS一直失败,那么就会一直重试,浪费CPU资源 1710 | 1711 | **原子性:** 1712 | 1713 | ​ 功能限制CAS是能保证单个变量的操作是原子性的,在Java中要配合使用volatile关键字来保证线程的安全;当涉及到多个变量的时候CAS无能为力;除此之外CAS实现需要硬件层面的支持,在Java的普通用户中无法直接使用,只能**借助atomic包下的原子类**实现,灵活性受到了限制 1714 | 1715 | 1716 | 1717 | **6、synchronized使用方法?底层实现?** 1718 | 1719 | **使用方法:**主要的三种使⽤⽅式 1720 | 1721 | ​ **修饰实例⽅法:** 作⽤于当前对象实例加锁,进⼊同步代码前要获得当前对象实例的锁 1722 | 1723 | ​ **修饰静态⽅法:** 也就是给当前类加锁,会作⽤于类的所有对象实例,因为静态成员不属于任何⼀个实例对象,是类成员( static 表明这是该类的⼀个静态资源,不管new了多少个对象,只有⼀份)。所以如果⼀个线程A调⽤⼀个实例对象的⾮静态 synchronized ⽅法,⽽线程B需要调⽤这个实例对象所属类的静态 synchronized ⽅法,是允许的,不会发⽣互斥现象,因为访问静态synchronized ⽅法占⽤的锁是当前类的锁,⽽访问⾮静态 synchronized ⽅法占⽤的锁是当前实例对象锁。 1724 | 1725 | ​ **修饰代码块:** 指定加锁对象,对给定对象加锁,进⼊同步代码库前要获得给定对象的锁。 1726 | 1727 | ​ **总结:**synchronized锁住的资源只有两类:一个是**对象**,一个是**类**。 1728 | 1729 | **底层实现:** 1730 | 1731 | ​ 对象头是我们需要关注的重点,它是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由`Mark Word` 和 `Class Metadata Address`组成,**其中`Mark Word`存储对象的hashCode、锁信息或分代年龄或GC标志等信息**,**`Class Metadata Address`是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例**。 1732 | 1733 | ​ 锁也分不同状态,JDK6之前只有两个状态:无锁、有锁(重量级锁),而在JDK6之后对synchronized进行了优化,新增了两种状态,总共就是四个状态:**无锁状态、偏向锁、轻量级锁、重量级锁**,其中无锁就是一种状态了。锁的类型和状态在对象头`Mark Word`中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的`Mark Word`数据。 1734 | 1735 | ​ 每一个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现)。每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态。 1736 | 1737 | 1738 | 1739 | 1740 | 1741 | **7、ReenTrantLock使用方法?底层实现?和synchronized区别?** 1742 | 1743 | ​ 由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下三项: 1744 | 1745 | ```java 1746 | 1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。通过lock.lockInterruptibly()来实现这个机制。 1747 | 1748 | 2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。 1749 | 3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。 1750 | 1751 | ``` 1752 | 1753 | **使用方法:** 1754 | 1755 | ​ 基于API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成 1756 | 1757 | **底层实现:** 1758 | 1759 | ​ ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。 1760 | 1761 | **和synchronized区别:** 1762 | 1763 | ​ 1、**底层实现**上来说,synchronized 是**JVM**层面的锁,是**Java关键字**,通过monitor对象来完成(monitorenter与monitorexit),对象只有在同步块或同步方法中才能调用wait/notify方法;ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的**API层面**的锁。synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁;ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。 1764 | 1765 | ​ 3、**是否可手动释放:**synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。 1766 | 1767 | ​ 4、**是否可中断**synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。 1768 | 1769 | ​ 5、**是否公平锁**synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。 1770 | 1771 | 1772 | 1773 | **8、公平锁和非公平锁区别?为什么公平锁效率低?** 1774 | 1775 | **公平锁:** 1776 | 1777 | ​ 公平锁自然是遵循**FIFO**(先进先出)原则的,先到的线程会优先获取资源,后到的会进行排队等待 1778 | 1779 | ​ **优点:**所有的线程都能得到资源,不会饿死在队列中。 1780 | 1781 | ​ **缺点:**吞吐量会下降,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销大 1782 | 1783 | 1784 | 1785 | **非公平锁:** 1786 | 1787 | ​ 多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。 1788 | 1789 | ​ **优点:**可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。 1790 | 1791 | ​ **缺点:**你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁 1792 | 1793 | 1794 | 1795 | **公平锁效率低原因:** 1796 | 1797 | ​ 公平锁要维护一个队列,后来的线程要加锁,即使锁空闲,也要先检查有没有其他线程在 wait,如果有自己要挂起,加到队列后面,然后唤醒队列最前面线程。这种情况下相比较非公平锁多了一次**挂起和唤醒**。 1798 | 1799 | ​ **线程切换的开销**,其实就是非公平锁效率高于公平锁的原因,因为**非公平锁减少了线程挂起的几率**,后来的线程有一定几率逃离被挂起的开销。 1800 | 1801 | 1802 | 1803 | 1804 | 1805 | **9、锁优化。自旋锁、自适应自旋锁、锁消除、锁粗化、偏向锁、轻量级锁、重量级锁解释** 1806 | 1807 | **锁优化:** 1808 | 1809 | ​ 【1】**减少锁的时间:** 1810 | ​ 不需要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可以让锁尽快释放; 1811 | 1812 | ​ 【2】**减少锁的粒度:** 1813 | ​ 它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。它的思想也是用空间来换时间;java中很多数据结构都是采用这种方法提高并发操作的效率,比如: 1814 | 1815 | ​ **ConcurrentHashMap:** 1816 | 1817 | ​ java中的ConcurrentHashMap在jdk1.8之前的版本,使用一个Segment 数组:Segment< K,V >[] segments 1818 | 1819 | ​ Segment继承自ReenTrantLock,所以每个Segment是个可重入锁,每个Segment 有一个HashEntry< K,V >数组用来存放数据,put操作时,先确定往哪个Segment放数据,只需要锁定这个Segment,执行put,其它的Segment不会被锁定;所以数组中有多少个Segment就允许同一时刻多少个线程存放数据,这样增加了并发能力。 1820 | 1821 | Segment继承自ReenTrantLock,所以每个Segment就是个可重入锁,每个Segment 有一个HashEntry< K,V >数组用来存放数据,put操作时,先确定往哪个Segment放数据,只需要锁定这个Segment,执行put,其它的Segment不会被锁定;所以数组中有多少个Segment就允许同一时刻多少个线程存放数据,这样增加了并发能力。 1822 | 1823 | ​ 【3】**锁粗化:** 1824 | ​ 大部分情况下我们是要让锁的粒度最小化,锁的粗化则是要增大锁的粒度; 1825 | 1826 | ​ 在以下场景下需要粗化锁的粒度: 1827 | 1828 | ​ 假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的; 1829 | 1830 | ​ 【4】**使用读写锁:** 1831 | 1832 | ​ ReentrantReadWriteLock 是一个读写锁,读操作加读锁,可并发读,写操作使用写锁,只能单线程写; 1833 | 1834 | ​ 【5】**使用cas:** 1835 | 1836 | ​ 如果需要同步的操作执行速度非常快,并且线程竞争并不激烈,这时候使用cas效率会更高,因为加锁会导致线程的上下文切换,如果上下文切换的耗时比同步操作本身更耗时,且线程对资源的竞争不激烈,使用volatiled+cas操作会是非常高效的选择; 1837 | 1838 | 1839 | 1840 | **自旋锁:** 1841 | 1842 | ​ 自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做**内核态和用户态之间的切换**进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就**避免用户线程和内核的切换的消耗**。 1843 | 1844 | ​ **缺点:**如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销;自旋次数默认值是10 1845 | 1846 | **自适应自旋锁:** 1847 | 1848 | ​ 对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点 1849 | 1850 | **锁消除:** 1851 | 1852 | ​ 锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。 1853 | 1854 | **锁粗化:** 1855 | 1856 | ​ 假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的; 1857 | 1858 | **偏向锁:** 1859 | 1860 | ​ 所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程;也就是说,这个线程已经占有这个锁,当他在次试图去获取这个锁的时候,他会已最快的方式去拿到这个锁,而不需要在进行一些monitor操作,因此这方面他是会对性能有所提升的,因为在大部分情况下是没有竞争的,所以锁此时是没用的,所以使用偏向锁是可以提高性能的; 1861 | 1862 | **重量级锁:** 1863 | 1864 | ​ 重量级锁的加锁、解锁过程和轻量级锁差不多,区别是:竞争失败后,线程阻塞,释放锁后,唤醒阻塞的线程,不使用自旋锁,不会那么消耗CPU,所以重量级锁适合用在同步块执行时间长的情况下。 1865 | 1866 | 1867 | 1868 | **10、Java内存模型** 1869 | 1870 | ​ Java 内存模型(Java Memory Model,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了 Java 程序在各种平台下对内存的访问都能保证效果一致的机制及规范。 1871 | 1872 | ​ JMM 是一种规范,是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。目的是保证并发编程场景中的原子性、可见性和有序性。 1873 | 1874 | ​ 所以,Java 内存模型,除了定义了一套规范,还提供了一系列原语,封装了底层实现后,供开发者直接使用。我们前面提到,并发编程要解决**原子性**、**有序性**和**一致性**的问题。 1875 | 1876 | **原子性:** 1877 | 1878 | ​ 在 Java 中,为了保证原子性,提供了两个高级的字节码指令 Monitorenter 和 Monitorexit。这两个字节码,在 Java 中对应的关键字就是 Synchronized。因此,在 Java 中可以使用 Synchronized 来保证方法和代码块内的操作是原子性的。 1879 | 1880 | **可见性:** 1881 | 1882 | ​ Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。Java 中的 Volatile 关键字修饰的变量在被修改后可以立即同步到主内存。被其修饰的变量在每次使用之前都从主内存刷新。因此,可以使用 Volatile 来保证多线程操作时变量的可见性。除了 Volatile,Java 中的 Synchronized 和 Final 两个关键字也可以实现可见性。只不过实现方式不同 1883 | 1884 | **有序性** 1885 | 1886 | ​ 在 Java 中,可以使用 Synchronized 和 Volatile 来保证多线程之间操作的有序性。区别:Volatile 禁止指令重排。Synchronized 保证同一时刻只允许一条线程操作。 1887 | 1888 | 1889 | 1890 | **11、volatile作用?底层实现?单例模式中volatile的作用?** 1891 | 1892 | **作用:** 1893 | 1894 | ​ 保证数据的“可见性”:被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。 1895 | 1896 | ​ 禁止指令重排:在多线程操作情况下,指令重排会导致计算结果不一致 1897 | 1898 | **底层实现:** 1899 | 1900 | ​ “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令” 1901 | 1902 |   lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能: 1903 | 1904 |   1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成; 1905 | 1906 |   2)它会强制将对缓存的修改操作立即写入主存; 1907 | 1908 |   3)如果是写操作,它会导致其他CPU中对应的缓存行无效。 1909 | 1910 | **单例模式中volatile的作用:** 1911 | 1912 | 防止代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化。 1913 | 1914 | ```java 1915 | class Singleton{ 1916 | private volatile static Singleton instance = null; //禁止指令重排 1917 | private Singleton() { 1918 | 1919 | } 1920 | public static Singleton getInstance() { 1921 | if(instance==null) { 1922 | synchronized (Singleton.class) { 1923 | if(instance==null) 1924 | instance = new Singleton(); 1925 | } 1926 | } 1927 | return instance; 1928 | } 1929 | } 1930 | ``` 1931 | 1932 | 1933 | 1934 | **12、AQS思想,以及基于AQS实现的lock, CountDownLatch、CyclicBarrier、Semaphore介绍** 1935 | 1936 | ​ AQS的全称为(AbstractQueuedSynchronizer)抽象的队列式的同步器,是⼀个⽤来构建锁和同步器的框架,使⽤AQS能简单且⾼效地构造出应⽤⼴泛的⼤量的同步器,如:基于AQS实现的lock, CountDownLatch、CyclicBarrier、Semaphore 1937 | 1938 | ​ AQS核⼼思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的⼯作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占⽤,那么就需要⼀套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是⽤CLH(虚拟的双向队列)队列锁实现的,即将暂时获取不到锁的线程加⼊到队列中。 1939 | 1940 | **lock:** 1941 | 1942 | ​ 是一种可重入锁,除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。默认为非公平锁,但可以初始化为公平锁; 通过方法 lock()与 unlock()来进行加锁与解锁操作; 1943 | 1944 | **CountDownLatch:** 1945 | 1946 | ​ 通过计数法(倒计时器),让一些线程堵塞直到另一个线程完成一系列操作后才被唤醒;该⼯具通常⽤来控制线程等待,它可以让某⼀个线程等待直到倒计时结束,再开始执⾏。 1947 | 1948 | ​ 假设我们有这么一个场景,教室里有班长和其他6个人在教室上自习,怎么保证班长等其他6个人都走出教室在把教室门给关掉。 1949 | 1950 | ```java 1951 | public class CountDownLanchDemo { 1952 | public static void main(String[] args) { 1953 | for (int i = 0; i < 6; i++) { 1954 | new Thread(() -> { 1955 | System.out.println(Thread.currentThread().getName() + " 离开了教室..."); 1956 | }, String.valueOf(i)).start(); 1957 | } 1958 | System.out.println("班长把门给关了,离开了教室..."); 1959 | } 1960 | } 1961 | ``` 1962 | 1963 | **此时输出:** 1964 | 1965 | ```java 1966 | 0 离开了教室... 1967 | 1 离开了教室... 1968 | 2 离开了教室... 1969 | 3 离开了教室... 1970 | 班长把门给关了,离开了教室... 1971 | 5 离开了教室... 1972 | 4 离开了教室... 1973 | ``` 1974 | 1975 | 发现班长都没有等其他人理他教室就把门给关了,此时我们就可以使用 CountDownLatch 来控制 1976 | 1977 | ```java 1978 | public class CountDownLanchDemo { 1979 | public static void main(String[] args) throws InterruptedException { 1980 | CountDownLatch countDownLatch = new CountDownLatch(6); 1981 | for (int i = 0; i < 6; i++) { 1982 | new Thread(() -> { 1983 | countDownLatch.countDown(); 1984 | System.out.println(Thread.currentThread().getName() + " 离开了教室..."); 1985 | }, String.valueOf(i)).start(); 1986 | } 1987 | countDownLatch.await(); 1988 | System.out.println("班长把门给关了,离开了教室..."); 1989 | } 1990 | } 1991 | ``` 1992 | 1993 | **此时输出:** 1994 | 1995 | ```java 1996 | 0 离开了教室... 1997 | 1 离开了教室... 1998 | 2 离开了教室... 1999 | 3 离开了教室... 2000 | 4 离开了教室... 2001 | 5 离开了教室... 2002 | 班长把门给关了,离开了教室... 2003 | ``` 2004 | 2005 | 2006 | 2007 | **CyclicBarrier:** 2008 | 2009 | ​ 字面意思是可循环(Cyclic)使用的屏障(Barrier)。他要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法。 2010 | 2011 | 我们假设有这么一个场景,每辆车只能坐4个人,当车满了,就发车。 2012 | 2013 | ```java 2014 | public class CyclicBarrierDemo { 2015 | public static void main(String[] args) { 2016 | CyclicBarrier cyclicBarrier = new CyclicBarrier(4, () -> { 2017 | System.out.println("车满了,开始出发..."); 2018 | }); 2019 | for (int i = 0; i < 8; i++) { 2020 | new Thread(() -> { 2021 | System.out.println(Thread.currentThread().getName() + " 开始上车..."); 2022 | try { 2023 | cyclicBarrier.await(); 2024 | } catch (InterruptedException e) { 2025 | e.printStackTrace(); 2026 | } catch (BrokenBarrierException e) { 2027 | e.printStackTrace(); 2028 | } 2029 | }).start(); 2030 | } 2031 | } 2032 | } 2033 | ``` 2034 | 2035 | **输出结果:** 2036 | 2037 | ```java 2038 | Thread-0 开始上车... 2039 | Thread-1 开始上车... 2040 | Thread-3 开始上车... 2041 | Thread-4 开始上车... 2042 | 车满了,开始出发... 2043 | Thread-5 开始上车... 2044 | Thread-7 开始上车... 2045 | Thread-2 开始上车... 2046 | Thread-6 开始上车... 2047 | 车满了,开始出发... 2048 | ``` 2049 | 2050 | 2051 | 2052 | **Semaphore:** 2053 | 2054 | ​ 信号量主要用于两个目的,一个是用于多个共享资源的互斥作用,另一个用于并发线程数的控制。 2055 | 2056 | 假设我们有 3 个停车位,6 辆车去抢;指定多个线程同时访问某个资源。 2057 | 2058 | ```java 2059 | public class SemaphoreDemo { 2060 | public static void main(String[] args) { 2061 | Semaphore semaphore = new Semaphore(3); 2062 | for (int i = 0; i < 6; i++) { 2063 | new Thread(() -> { 2064 | try { 2065 | semaphore.acquire(); // 获取一个许可 2066 | System.out.println(Thread.currentThread().getName() + " 抢到车位..."); 2067 | Thread.sleep(3000); 2068 | System.out.println(Thread.currentThread().getName() + " 离开车位"); 2069 | } catch (InterruptedException e) { 2070 | e.printStackTrace(); 2071 | } finally { 2072 | semaphore.release(); // 释放一个许可 2073 | } 2074 | }).start(); 2075 | } 2076 | } 2077 | } 2078 | 2079 | /**输出 2080 | Thread-1 抢到车位... 2081 | Thread-2 抢到车位... 2082 | Thread-0 抢到车位... 2083 | Thread-2 离开车位 2084 | Thread-0 离开车位 2085 | Thread-3 抢到车位... 2086 | Thread-1 离开车位 2087 | Thread-4 抢到车位... 2088 | Thread-5 抢到车位... 2089 | Thread-3 离开车位 2090 | Thread-5 离开车位 2091 | Thread-4 离开车位 2092 | */ 2093 | ``` 2094 | 2095 | 2096 | 2097 | 2098 | 2099 | **13、线程池构造函数7大参数,线程处理任务过程,线程拒绝策略** 2100 | 2101 | ```java 2102 | /** 2103 | * 线程池构造函数7大参数 2104 | */ 2105 | public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime, 2106 | TimeUnit unit,BlockingQueue workQueue,ThreadFactory threadFactory, 2107 | RejectedExecutionHandler handler) { 2108 | if (corePoolSize < 0 ||maximumPoolSize <= 0 ||maximumPoolSize < corePoolSize || 2109 | keepAliveTime < 0) 2110 | throw new IllegalArgumentException(); 2111 | if (workQueue == null || threadFactory == null || handler == null) 2112 | throw new NullPointerException(); 2113 | this.corePoolSize = corePoolSize; 2114 | this.maximumPoolSize = maximumPoolSize; 2115 | this.workQueue = workQueue; 2116 | this.keepAliveTime = unit.toNanos(keepAliveTime); 2117 | this.threadFactory = threadFactory; 2118 | this.handler = handler; 2119 | } 2120 | ``` 2121 | 2122 | **参数介绍:** 2123 | 2124 | | 参数 | 作用 | 2125 | | ------------------------ | ------------------------------------------------------------ | 2126 | | corePoolSize | 核心线程池大小 | 2127 | | maximumPoolSize | 最大线程池大小 | 2128 | | keepAliveTime | 线程池中超过 corePoolSize 数目的空闲线程最大存活时间;可以allowCoreThreadTimeOut(true) 使得核心线程有效时间 | 2129 | | TimeUnit | keepAliveTime 时间单位 | 2130 | | workQueue | 阻塞任务队列 | 2131 | | threadFactory | 新建线程工厂 | 2132 | | RejectedExecutionHandler | 拒绝策略。当提交任务数超过 maxmumPoolSize+workQueue 之和时,任务会交给RejectedExecutionHandler 来处理 | 2133 | 2134 | 2135 | 2136 | **线程拒绝策略:** 2137 | 2138 | ​ 线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。 2139 | 2140 | JDK 内置的拒绝策略如下: 2141 | 2142 | ​ **AbortPolicy:**直接抛出异常,阻止系统正常运行。 2143 | 2144 | ​ **CallerRunsPolicy :**只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。 2145 | 2146 | ​ **DiscardOldestPolicy :**丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。 2147 | 2148 | ​ **DiscardPolicy :**该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。 2149 | 2150 | 2151 | 2152 | **线程处理任务过程:** 2153 | 2154 | 1. 当线程池小于corePoolSize,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。 2155 | 2. 当线程池达到corePoolSize时,新提交任务将被放入 workQueue 中,等待线程池中任务调度执行。 2156 | 3. 当workQueue已满,且 maximumPoolSize 大于 corePoolSize 时,新提交任务会创建新线程执行任务。 2157 | 4. 当提交任务数超过 maximumPoolSize 时,新提交任务由 RejectedExecutionHandler 处理。 2158 | 5. 当线程池中超过corePoolSize 线程,空闲时间达到 keepAliveTime 时,关闭空闲线程 。 2159 | 2160 | 2161 | 2162 | **14、Execuors类实现的几种线程池类型,阿里为啥不让用?** 2163 | 2164 | - **Executors.newSingleThreadExecutor():**只有一个线程的线程池,因此所有提交的任务是顺序执行,适用于一个一个任务执行的场景 2165 | - **Executors.newCachedThreadPool():**线程池里有很多线程需要同时执行,老的可用线程将被新的任务触发重新执行,如果线程超过60秒内没执行,那么将被终止并从池中删除,适用执行很多短期异步的小程序或者负载较轻的服务 2166 | - **Executors.newFixedThreadPool():**拥有固定线程数的线程池,如果没有任务执行,那么线程会一直等待,适用执行长期的任务,性能好很多。 2167 | - **Executors.newScheduledThreadPool():**用来调度即将执行的任务的线程池 2168 | 2169 | **因为以上方式都存在弊端:** 2170 | 2171 | ​ FixedThreadPool 和 SingleThreadExecutor : 允许请求的**队列⻓度**为 Integer.MAX_VALUE,可能堆积⼤量的请求,从⽽导致OOM。 2172 | ​ CachedThreadPool 和 ScheduledThreadPool : 允许创建的**线程数量**为 Integer.MAX_VALUE,可能会创建⼤量线程,从⽽导致OOM。 2173 | 2174 | 2175 | 2176 | **15、线程池大小如何设置?** 2177 | 2178 | - CPU 密集型 2179 | 2180 | - - CPU 密集的意思是该任务需要大量的运算,而没有阻塞,CPU 一直全速运行。 2181 | - CPU 密集型任务尽可能的少的线程数量,一般为 CPU 核数 + 1 个线程的线程池。 2182 | 2183 | - IO 密集型 2184 | 2185 | - - 由于 IO 密集型任务线程并不是一直在执行任务,可以多分配一点线程数,如 CPU * 2 2186 | - 也可以使用公式:CPU 核数 / (1 - 阻塞系数);其中阻塞系数在 0.8 ~ 0.9 之间。 2187 | 2188 | 2189 | 2190 | **16、手写简单的线程池,体现线程复用** 2191 | 2192 | https://blog.csdn.net/hongtaolong/article/details/87808009 2193 | 2194 | 2195 | 2196 | **17、手写消费者生产者模式** 2197 | 2198 | https://www.cnblogs.com/liuqing576598117/p/11233250.html 2199 | 2200 | 2201 | 2202 | **18、手写阻塞队列** 2203 | 2204 | https://www.cnblogs.com/keeya/p/9713686.html 2205 | 2206 | 2207 | 2208 | **19、手写多线程交替打印ABC** 2209 | 2210 | https://blog.csdn.net/xiaokang123456kao/article/details/77331878 2211 | 2212 | 2213 | 2214 | ## **五、MySQL篇** 2215 | 2216 | **1、事务4大特性?这4个特性mysql如何保证实现的?** 2217 | 2218 | **事务4大特性:**原子性、一致性、隔离性、持久性 2219 | 2220 | ​ **原⼦性:** 事务是最⼩的执⾏单位,不允许分割。事务的原⼦性确保动作要么全部完成,要么全不执行 2221 | 2222 | ​ **一致性:** 执⾏事务前后,数据保持⼀致,多个事务对同⼀个数据读取的结果是相同的; 2223 | 2224 | ​ **隔离性:** 并发访问数据库时,⼀个⽤户的事务不被其他事务所⼲扰,各并发事务之间数据库是独⽴的; 2225 | 2226 | ​ **持久性:** ⼀个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发⽣故障也不应该对其有任何影响。 2227 | 2228 | **实现保证:** 2229 | 2230 | ​ MySQL的存储引擎InnoDB使用重做日志保证一致性与持久性,回滚日志保证原子性,使用各种锁来保证隔离性。 2231 | 2232 | 2233 | 2234 | **2、事务隔离级别,4个隔离级别分别有什么并发问题?** 2235 | 2236 | **读未提交:**最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。 2237 | 2238 | **读已提交:**允许读取并发事务已经提交的数据,可以阻⽌脏读,但是幻读或不可重复读仍有可能发⽣。 2239 | 2240 | **可重复读:**同⼀字段的多次读取结果都是⼀致的,除⾮数据是被本身事务⾃⼰所修改,可以阻⽌脏读和不可重复读,但幻读仍有可能发⽣。 2241 | 2242 | **可串行化:**最⾼的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执⾏,这样事务之间就完全不可能产⽣⼲扰,也就是说,该级别可以防⽌脏读、不可重复读以及幻读。 2243 | 2244 | | 隔离级别 | 并发问题 | 2245 | | -------- | -------------------------------- | 2246 | | 读未提交 | 可能会导致脏读、幻读或不可重复读 | 2247 | | 读已提交 | 可能会导致幻读或不可重复读 | 2248 | | 可重复度 | 可能会导致幻读 | 2249 | | 可串行化 | 不会产⽣⼲扰 | 2250 | 2251 | 2252 | 2253 | **3、Mysql默认隔离级别?如何保证并发安全?** 2254 | 2255 | **默认隔离级别:**可重复读; 2256 | 2257 | ​ 同⼀字段的多次读取结果都是⼀致的,除⾮数据是被本身事务⾃⼰所修改; 2258 | 2259 | ​ 可重复读是有可能出现幻读的,如果要保证绝对的安全只能把隔离级别设置成SERIALIZABLE;这样所有事务都只能顺序执行,自然不会因为并发有什么影响了,但是性能会下降许多。 2260 | 2261 | ​ 第二种方式,使用更新的版本控制。维护一个字段作为updateversion,修改时updateversion也作为一个参数传入,在条件语句中添加例如`where id=? and update_version = ?` 当然set里面要update_version+1。这样可以控制到每次只能有一个人更新一个版本。 2262 | 2263 | 2264 | 2265 | **4、RR和RC如何实现的?RR使用场景?** 2266 | 2267 | ​ 事务隔离级别RC(read commit)和RR(repeatable read)两种事务隔离级别基于多版本并发控制MVCC(multi-version concurrency control)来实现。 2268 | 2269 | ​ 由于RC隔离级别需要保持语句级别的一致性,事务中每一次读取都是访问当前时间点的已提交数据,因此事务中多条查询语句会创建多个不同的ReadView,开销较大,复杂度更高;而对于RR隔离级别,仅需要一个版本的ReadView,消耗更少,因此Mysql默认使用RR隔离级别。 2270 | 2271 | ​ RC隔离级别获得的是语句级读一致性;RR隔离级别获得的是事务级读一致性 2272 | 2273 | ​ 对于RC隔离级别,访问的数据是每次语句执行时间点的数据,而对于RR隔离级别,访问的数据是事务中第一条语句执行时间点的数据。 2274 | 2275 | 2276 | 2277 | **5、隔离级别的单位是数据表还是数据行?如串行化级别,两个事务访问不同的数据行,能并发?** 2278 | 2279 | ​ 读未提交:不加锁 2280 | 2281 | ​ 读已提交:加行锁,只锁要修改的行 2282 | 2283 | ​ 可重复读:加行锁,锁定的是查询的行 2284 | 2285 | ​ 可串行化:加表锁,在读取的每张表上加锁 2286 | 2287 | ​ **串行化级别:读不同的行,可以并发** 2288 | 2289 | 2290 | 2291 | **6、存储引擎Innodb和Myisam的区别以及使用场景** 2292 | 2293 | **Myisam:**支持表锁,适合读密集的场景,不支持外键,不支持事务,索引与数据在不同的文件 2294 | 2295 | **Innodb:**支持行、表锁,默认为行锁,适合并发场景,支持外键,支持事务,索引与数据同一文件 2296 | 2297 | 2298 | 2299 | **7、 介绍Inodb锁机制,行锁,表锁,意向锁** 2300 | 2301 | **InnoDB⽀持⾏级锁(row-level locking)和表级锁,默认为⾏级锁** 2302 | 2303 | InnoDB按照不同的分类的锁: 2304 | 2305 | 共享/排它锁(Shared and Exclusive Locks):行级别锁, 2306 | 2307 | 意向锁(Intention Locks),表级别锁 2308 | 2309 | 间隙锁(Gap Locks),锁定一个区间 2310 | 2311 | 记录锁(Record Locks),锁定一个行记录 2312 | 2313 | **表级锁:** 2314 | 2315 | ​ Mysql中锁定 粒度最大 的一种锁,对当前操作的整张表加锁,实现简单 ,资源消耗也比较少,加锁快,不会出现死锁 。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM和 InnoDB引擎都支持表级锁。 2316 | 2317 | **行级锁:** 2318 | 2319 | ​ Mysql中锁定 粒度最小 的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。 InnoDB支持的行级锁,包括如下几种: 2320 | 2321 | ​ **记录锁(Record Lock):** 对索引项加锁,锁定符合条件的行。其他事务不能修改和删除加锁项; 2322 | 2323 | ​ **间隙锁(Gap Lock):** 对索引项之间的“间隙”加锁,锁定记录的范围(对第一条记录前的间隙或最后一条将记录后的间隙加锁),不包含索引项本身。其他事务不能在锁范围内插入数据,这样就防止了别的事务新增幻影行。 2324 | 2325 | ​ **Next-key Lock:** 锁定索引项本身和索引范围。即Record Lock和Gap Lock的结合。可解决幻读问题。 2326 | 2327 | **意向锁:** 2328 | 2329 | ​ 当一个事务在需要获取资源的锁定时,如果该资源已经被排他锁占用,则数据库会自动给该事务申请一个该表的意向锁。如果自己需要一个共享锁定,就申请一个**意向共享锁**。如果需要的是某行(或者某些行)的排他锁定,则申请一个**意向排他锁**。 2330 | 2331 | 2332 | 2333 | **8、介绍MVCC.** 2334 | 2335 | ​ MVCC是一种多版本并发控制机制,在大多数情况下代替行级锁,使用MVCC,能降低其系统开销. 2336 | 2337 | ​ MVCC是通过保存数据在某个时间点的快照来实现的. 不同存储引擎的MVCC实现是不同的,典型的有乐观并发控制和悲观并发控制. 2338 | 2339 | ​ InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的,这两个列,分别保存了这个行的创建时间,一个保存的是行的删除时间。这里存储的并不是实际的时间值,而是系统版本号(可以理解为事务的ID),每开始一个新的事务,系统版本号就会自动递增,事务开始时刻的系统版本号会作为事务的ID. 2340 | 2341 | ​ InnoDB只会查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的. 2342 | 2343 | ​ 1.MVCC手段只适用于Msyql隔离级别中的读已提交(Read committed)和可重复读(Repeatable Read). 2344 | 2345 | ​ 2.Read uncimmitted由于存在脏读,即能读到未提交事务的数据行,所以不适用MVCC. 2346 | 2347 | ​ 原因是MVCC的创建版本和删除版本只要在事务提交后才会产生。客观上,我们认为他就是乐观锁的一整实现方式,就是每行都有版本号,保存时根据版本号决定是否成功。 2348 | 2349 | 2350 | 2351 | 2352 | **9、哈希索引是如何实现的?** 2353 | 2354 | ​ 哈希索引用索引列的值计算该值的hashCode,然后在hashCode相应的位置存执该值所在行数据的物理位置,因为使用散列算法,因此访问速度非常快,但是一个值只能对应一个hashCode,而且是散列的分布方式,因此哈希索引不支持范围查找和排序的功能 2355 | 2356 | 2357 | 2358 | **10、数据库索引为什么使用B+树,相对于B树有什么优点?为什么不能红黑树?** 2359 | 2360 | **因为:** 2361 | 2362 | ​ B+树的磁盘读写代价低,更少的查询次数,查询效率更加稳定,有利于对数据库的扫描 2363 | 2364 | ​ 相对B树,B+树是B树的升级版,只是把非叶子节点冗余一下,这么做的好处是**为了提高范围查找的效率,解决数据库遍历效率低下问题**;**B+树只有叶节点存放数据,其余节点用来索引,而B树是每个索引节点都会有Data域。** 2365 | 2366 | ​ 在大规模数据存储的时候,红黑树往往出现由于**树的深度过大**而造成磁盘IO读写过于频繁,进而导致效率低下的情况。所以,只要我们通过某种较好的树结构减少树的结构尽量减少树的高度,B树与B+树可以有多个子女,从几十到上千,可以降低树的高度。 2367 | 2368 | ​ **磁盘预读原理**:将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。为了达到这个目的,在实际实现B-Tree还需要使用如下技巧:每次新建节点时,直接申请一个页的空间,这样就保证**一个节点物理上也存储在一个页里**,加之计算机存储分配都是按页对齐的,就实现了一个node只需一次I/O。 2369 | 2370 | 2371 | 2372 | **11、聚簇索引和非聚簇索引区别** 2373 | 2374 | ​ **聚簇索引:**将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据 2375 | 2376 | ​ **非聚簇索引:**将数据与索引分开存储,索引结构的叶子节点指向了数据对应的位置 2377 | 2378 | ​ 聚簇索引的叶子节点就是数据节点,而非聚簇索引的叶子节点仍然是索引节点,只不过有指向对应数据块的指针。 2379 | 2380 | 2381 | 2382 | **12、回表查询和覆盖索引** 2383 | 2384 | 普通索引 需要扫描两遍索引树 2385 | 2386 | (1)先通过普通索引定位到主键值id=5; 2387 | 2388 | (2)在通过聚集索引定位到行记录; 2389 | 2390 | 这就是所谓的**回表查询**,先定位主键值,再定位行记录,它的性能较扫一遍索引树更低。 2391 | 2392 | **覆盖索引**:如果where条件的列和返回的数据在一个索引中,那么不需要回查表,那么就叫覆盖索引。 2393 | 2394 | **实现覆盖索引**:常见的方法是,将被查询的字段,建立到联合索引里去。 2395 | 2396 | 2397 | 2398 | **13、如何创建索引?** 2399 | 2400 | ```sql 2401 | CREATE TABLE 表名( 2402 | 字段名 数据类型 [完整性约束条件], 2403 | ……, 2404 | [UNIQUE | FULLTEXT | SPATIAL] INDEX | KEY 2405 | [索引名](字段名1 [(长度)] [ASC | DESC]) [USING 索引方法] 2406 | ); 2407 | 2408 | 说明: 2409 | UNIQUE:可选。表示索引为唯一性索引。 2410 | FULLTEXT:可选。表示索引为全文索引。 2411 | SPATIAL:可选。表示索引为空间索引。 2412 | INDEX和KEY:用于指定字段为索引,两者选择其中之一就可以了,作用是一样的。 2413 | 索引名:可选。给创建的索引取一个新名称。 2414 | 字段名1:指定索引对应的字段的名称,该字段必须是前面定义好的字段。 2415 | 长度:可选。指索引的长度,必须是字符串类型才可以使用。 2416 | ASC:可选。表示升序排列。 2417 | DESC:可选。表示降序排列。 2418 | 注:索引方法默认使用B+TREE。 2419 | 2420 | ALTER TABLE 表名 ADD [UNIQUE | FULLTEXT | SPATIAL] INDEX | KEY [索引名] (字段名1 [(长度)] [ASC | DESC]) [USING 索引方法]; 2421 | 2422 | 或 2423 | 2424 | CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX 索引名 ON 表名(字段名) [USING 索引方法]; 2425 | ``` 2426 | 2427 | 2428 | 2429 | 2430 | 2431 | **14、如何避免全表扫描?** 2432 | 2433 | 1.对查询进行优化,应考虑在 where 及 order by 涉及的列上建立索引。 2434 | 2435 | 2.应尽量避免在 where 子句中对字段进行 null 值判断 2436 | 2437 | 3.应尽量避免在 where 子句中使用!=或<>操作符 2438 | 2439 | 4.in 和 not in 要慎用 2440 | 2441 | 否则将导致引擎放弃使用索引而进行全表扫描 2442 | 2443 | 2444 | 2445 | 2446 | 2447 | **15、Explain语句各字段的意义** 2448 | 2449 | 参考:https://www.jianshu.com/p/8fab76bbf448 2450 | 2451 | ```mysql 2452 | mysql> explain select * from staff; 2453 | +----+-------------+-------+------+---------------+------+---------+------+------+-------+ 2454 | | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | 2455 | +----+-------------+-------+------+---------------+------+---------+------+------+-------+ 2456 | | 1 | SIMPLE | staff | ALL | NULL | NULL | NULL | NULL | 2 | NULL | 2457 | +----+-------------+-------+------+---------------+------+---------+------+------+-------+ 2458 | 1 row in set 2459 | ``` 2460 | 2461 | | 列 | 含义 | 2462 | | ------------- | -------------------------------------------- | 2463 | | id | 查询序号,序号越大越先执行,一样则按顺序执行 | 2464 | | select_type | 查询类型,SIMPLE、PRIMARY、UNION、SUBQUERY等 | 2465 | | table | 表名 | 2466 | | type | join类型,const,eq_ref,ref等 | 2467 | | possible_keys | join类型 | 2468 | | key | 实际选择的索引 | 2469 | | ken_len | 索引的长度 | 2470 | | ref | 与索引作比较的列 | 2471 | | rows | 要检索的行数 | 2472 | | Extra | 额外信息 | 2473 | 2474 | 2475 | 2476 | **16、最左前缀!!联合索引B+树是如何建立的?是如何查询的?当where子句中出现>时,联合索引命中是如何的? ** 2477 | 2478 | ​ 最左前缀原则主要使用在联合索引中,联合索引的B+Tree是按照第一个关键字进行索引排列的。 2479 | 2480 | ​ 联合索引的底层是一颗B+树,那么联合索引的底层也就是一颗B+树,只不过联合索引的B+树节点中存储的是键值。由于构建一棵B+树只能根据一个值来确定索引关系,所以数据库依赖联合索引最左的字段来构建。 2481 | 2482 | ​ 采用>、<等进行匹配都会导致后面的列无法走索引,因为通过以上方式匹配到的数据是不可知的。 2483 | 2484 | 2485 | 2486 | **17、MySQL中一条SQL语句的执行过程** 2487 | 2488 | 参考:https://zhuanlan.zhihu.com/p/126722329 2489 | 2490 | **查询语句:** 2491 | 2492 | ```mysql 2493 | select * from student A where A.age='18' and A.name='张三'; 2494 | ``` 2495 | 2496 | 结合上面的说明,我们分析下这个语句的执行流程: 2497 | 2498 | - 先检查该语句是否有权限,如果没有权限,直接返回错误信息,如果有权限,在mysql8.0版本以前,会先查询缓存,以这条sql语句为key在内存中查询是否有结果,如果有直接缓存,如果没有,执行下一步。 2499 | - 通过分析器进行词法分析,提取sql语句的关键元素,比如提取上面这个语句是查询select,提取需要查询的表名为tb_student,需要查询所有的列,查询条件是这个表的id='1'。然后判断这个sql语句是否有语法错误,比如关键词是否正确等等,如果检查没问题就执行下一步。 2500 | - 接下来就是优化器进行确定执行方案,上面的sql语句,可以有两种执行方案: a.先查询学生表中姓名为“张三”的学生,然后判断是否年龄是18。 b.先找出学生中年龄18岁的学生,然后再查询姓名为“张三”的学生。 那么优化器根据自己的优化算法进行选择执行效率最好的一个方案(优化器认为,有时候不一定最好)。那么确认了执行计划后就准备开始执行了。 2501 | - 进行权限校验,如果没有权限就会返回错误信息,如果有权限就会调用数据库引擎接口,返回引擎的执行结果。 2502 | 2503 | **更新语句:** 2504 | 2505 | ```mysql 2506 | update student A set A.age='19' where A.name='张三'; 2507 | ``` 2508 | 2509 | 我们来给张三修改下年龄,在实际数据库肯定不会设置年龄这个字段的,不然要被技术负责人打的。其实这条语句也基本上会沿着上一个查询的流程走,只不过执行更新的时候肯定要记录日志啦,这就会引入日志模块了,mysql 自带的日志模块式binlog(归档日志),所有的存储引擎都可以使用,我们常用的InnoDB引擎还自带了一个日志模块redo log,我们就以InnoDB模式下来探讨这个语句的执行流程。流程如下: 2510 | 2511 | - 先查询到张三这一条数据,如果有缓存,也是会用到缓存。 2512 | - 然后拿到查询的语句,把 age 改为19,然后调用引擎API接口,写入这一行数据,InnoDB引擎把数据保存在内存中,同时记录redo log,此时redo log进入prepare状态,然后告诉执行器,执行完成了,随时可以提交。 2513 | - 执行器收到通知后记录binlog,然后调用引擎接口,提交redo log 为提交状态。 2514 | - 更新完成。 2515 | 2516 | 2517 | 2518 | **18、数据库几大范式** 2519 | 2520 | 参考:[数据库范式](https://blog.csdn.net/w__yi/article/details/19934319?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param) 2521 | 2522 | **第一范式(1NF)列不可分割** 2523 | 2524 | **第二范式(2NF)属性完全依赖于主键 [ 消除部分子函数依赖 ]** 2525 | 2526 | **第三范式(3NF)属性不依赖于其它非主属性 [ 消除传递依赖 ]** 2527 | 2528 | 2529 | 2530 | **19、left join,right join,inner join,outer join的含义及区别** 2531 | 2532 | ![](https://image-static.segmentfault.com/276/780/2767807589-5c122586a23c4_articlex) 2533 | 2534 | left join(左联接) 返回包括左表中的所有记录和右表中关联字段相等的记录 2535 | 2536 | right join(右联接) 返回包括右表中的所有记录和左表中关联字段相等的记录 2537 | 2538 | inner join(等值连接) 只返回两个表中关联字段相等的行 2539 | 2540 | 2541 | 2542 | **20、mysql主从复制过程,binlog记录格式,异步复制、同步复制、半同步复制模式区别** 2543 | 2544 | **MySQl主从复制:** 2545 | 2546 | - **原理**:将主服务器的binlog日志复制到从服务器上执行一遍,达到主从数据的一致状态。 2547 | - **过程**:从库开启一个I/O线程,向主库请求Binlog日志。主节点开启一个binlog dump线程,检查自己的二进制日志,并发送给从节点;从库将接收到的数据保存到中继日志(Relay log)中,另外开启一个SQL线程,把Relay中的操作在自身机器上执行一遍 2548 | - **优点**: 2549 | - 作为备用数据库,并且不影响业务 2550 | - 可做读写分离,一般是一个写库,一个或多个读库,分布在不同的服务器上,充分发挥服务器和数据库的性能,但要保证数据的一致性 2551 | 2552 | **binlog记录格式:**statement、row、mixed 2553 | 2554 | ​ 基于语句statement的复制、基于行row的复制、基于语句和行(mix)的复制。其中基于row的复制方式更能保证主从库数据的一致性,但日志量较大,在设置时考虑磁盘的空间问题 2555 | 2556 | **异步复制:** 2557 | 2558 | ​ 在异步复制中,主库执行完操作后,写入binlog日志后,就返回客户端,这一动作就结束了,并不会验证从库有没有收到,完不完整,所以这样可能会造成数据的不一致。 2559 | 2560 | **半同步复制:** 2561 | 2562 | ​ 当主库每提交一个事务后,不会立即返回,而是等待其中一个从库接收到Binlog并成功写入Relay-log中才返回客户端,所以这样就保证了一个事务至少有两份日志,一份保存在主库的Binlog,另一份保存在其中一个从库的Relay-log中,从而保证了数据的安全性和一致性。 2563 | 2564 | **全同步复制:** 2565 | 2566 | ​ 指当主库执行完一个事务,所有的从库都执行了该事务才返回给客户端。因为需要等待所有从库执行完该事务才能返回,所以全同步复制的性能必然会收到严重的影响。 2567 | 2568 | 2569 | 2570 | **21、主从复制或读写分离等数据不一致性问题以及如何解决** 2571 | 2572 | "主从复制有延时",这个延时期间读取从库,可能读到不一致的数据。 2573 | 2574 | **半同步复制法:** 2575 | 2576 | ​ 当主库每提交一个事务后,不会立即返回,而是等待其中一个从库接收到Binlog并成功写入Relay-log中才返回客户端,所以这样就保证了一个事务至少有两份日志,一份保存在主库的Binlog,另一份保存在其中一个从库的Relay-log中,从而保证了数据的安全性和一致性。 2577 | 2578 | **全同步复制法:** 2579 | 2580 | ​ 指当主库执行完一个事务,所有的从库都执行了该事务才返回给客户端。因为需要等待所有从库执行完该事务才能返回,所以全同步复制的性能必然会收到严重的影响。 2581 | 2582 | **缓存记录写key法:** 2583 | 2584 | ​ 在cache里记录哪些记录发生过的写请求,来路由读主库还是读从库 2585 | 2586 | 2587 | 2588 | **22、银行的话,可能会考mysql数据类型,如余额要用decimal** 2589 | 2590 | ​ 2591 | 2592 | ## **六、Redis篇** 2593 | 2594 | **1、为什么使用Redis** 2595 | 2596 | ​ 速度快,完全基于内存,使用C语言实现,网络层使用epoll解决高并发问题,单线程模型避免了不必要的上下文切换及竞争条件; 2597 | 2598 | ​ 与传统数据库不同的是 Redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。另外,Redis 也经常用来做分布式锁。除此之外,Redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。 2599 | 2600 | ​ 2601 | 2602 | **2、分布式缓存和本地缓存有啥区别?让你自己设计本地缓存怎么设计?如何解决缓存过期问题?如何解决内存溢出问题?** 2603 | 2604 | ​ 分布式缓存一致性更好一点,用于集群环境下多节点使用同一份缓存的情况;有网络IO,吞吐率与缓存的数据大小有较大关系; 2605 | 2606 | ​ 本地缓存非常高效,本地缓存会占用堆内存,影响垃圾回收、影响系统性能。 2607 | 2608 | **本地缓存设计:** 2609 | 2610 | ​ 以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况,每个实例都需要各自保存一份缓存,缓存不具有一致性。 2611 | 2612 | **解决缓存过期:** 2613 | 2614 | ​ 1、将缓存过期时间调为永久 2615 | 2616 | ​ 2、将缓存失效时间分散开,不要将缓存时间长度都设置成一样;比如我们可以在原有的失效时间基础上增加一个随机值,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。 2617 | 2618 | **解决内存溢出:** 2619 | 2620 | ​ **第一步**,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。) 2621 | 2622 |  **第二步**,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。 2623 | 2624 |  **第三步**,对代码进行走查和分析,找出可能发生内存溢出的位置。 2625 | 2626 | 2627 | 2628 | **3、redis和*Mem*cached的区别** 2629 | 2630 | | redis | Memcached | 2631 | | ------------------------------------- | -------------------------------------------- | 2632 | | 内存高速数据库 | 高性能分布式内存缓存数据库,可缓存图片、视频 | 2633 | | 支持hash、list、set、zset、string结构 | 只支持key-value结构 | 2634 | | 将大部分数据放到内存 | 全部数据放到内存中 | 2635 | | 支持持久化、主从复制备份 | 不支持数据持久化及数据备份 | 2636 | | 数据丢失可通过AOF恢复 | 挂掉后,数据不可恢复 | 2637 | 2638 | **使用场景:** 2639 | 2640 | ​ 1、如果有持久方面的需求或对数据类型和处理有要求的应该选择redis。 2641 | ​ 2、如果简单的key/value 存储应该选择memcached。 2642 | 2643 | 2644 | 2645 | **4、redis常用数据结构和使用场景** 2646 | 2647 | Redis主要有5种数据类型,包括String,List,Set,Zset,Hash 2648 | 2649 | | 类型 | 存储值 | 应用场景 | 2650 | | ------ | -------------------- | -------------------------------------------- | 2651 | | String | 字符串、整数、浮点数 | 简单的键值对缓存 | 2652 | | List | 列表 | 存储列表型数据结构,例如:评论列表、商品列表 | 2653 | | Set | 无序集合 | 适合交集、并集、查集操作,例如朋友关系 | 2654 | | Zset | 有序集合 | 去重后排序,适合排名场景 | 2655 | | Hash | 哈希 | 结构化数据,比如存储对象 | 2656 | 2657 | **Redis的应用场景:** 2658 | 2659 | **计数器** 2660 | 2661 | ​ 可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。 2662 | 2663 | **缓存** 2664 | 2665 | ​ 将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。 2666 | 2667 | **会话缓存** 2668 | 2669 | ​ 可以使用 Redis 来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。 2670 | 2671 | **其它** 2672 | 2673 | ​ Set 可以实现交集、并集等操作,从而实现共同好友等功能。ZSet 可以实现有序性操作,从而实现排行榜等功能。 2674 | 2675 | 2676 | 2677 | **5、Zset底层实现?跳表搜索插入删除过程?** 2678 | 2679 | ​ 跳表(skip List)是一种随机化的数据结构,基于并联的链表,实现简单,插入、删除、查找的复杂度均为O(logN)。简单说来跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能,正是这个跳跃的功能,使得在查找元素时,跳表能够提供O(logN)的时间复杂度 2680 | 2681 | ​ Zset数据量少的时候使用压缩链表ziplist实现,有序集合使用紧挨在一起的压缩列表节点来保存,第一个节点保存member,第二个保存score。ziplist内的集合元素按score从小到大排序,score较小的排在表头位置。 2682 | 2683 | ​ 数据量大的时候使用跳跃列表skiplist和哈希表hash_map结合实现,查找删除插入的时间复杂度都是O(longN) 2684 | 2685 | **搜索** 2686 | 2687 | ​ 跳跃表按 score 从小到大保存所有集合元素,查找时间复杂度为平均 *O(logN),最坏 O(N) 。* 2688 | 2689 | **插入** 2690 | 2691 |   之前就说了,之所以选用链表作为底层结构支持,也是为了高效地动态增删。单链表在知道删除的节点是谁时,时间复杂度为O(1),因为跳表底层的单链表是有序的,为了维护这种有序性,在插入前需要遍历链表,找到该插入的位置,单链表遍历查找的时间复杂度是O(n),同理可得,跳表的遍历也是需要遍历索引数,所以是O(logn)。 2692 | 2693 | **删除** 2694 | 2695 |   删除的节点要分两种情况,如果该节点还在索引中,那删除时不仅要删除单链表中的节点,还要删除索引中的节点;另一种情况是删除的节点只在链表中,不在索引中,那只需要删除链表中的节点即可。但针对单链表来说,删除时都需要拿到前驱节点才可改变引用关系从而删除目标节点。 2696 | 2697 | 2698 | 2699 | 2700 | 2701 | **6、redis过期淘汰策略** 2702 | 2703 | **1)全局的键空间选择性移除** 2704 | 2705 | ​ **noeviction**:当内存不足以容纳新写入数据时,新写入操作会报错。 2706 | 2707 | ​ **allkeys-lru**:在键空间中,移除最近最少使用的key。(这个是最常用的) 2708 | 2709 | ​ **allkeys-random**:在键空间中,随机移除某个key。 2710 | 2711 | **2)设置过期时间的键空间选择性移除** 2712 | 2713 | ​ **volatile-lru**:在设置了过期时间的键空间中,移除最近最少使用的key。 2714 | 2715 | ​ **volatile-random**:在设置了过期时间的键空间中,随机移除某个key。 2716 | 2717 | ​ **volatile-ttl**:在设置了过期时间的键空间中,有更早过期时间的key优先移除。 2718 | 2719 | **总结** 2720 | 2721 | ​ Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。 2722 | 2723 | 2724 | 2725 | **7、redis持久化机制?都有什么优缺点?持久化的时候还能接受请求吗?** 2726 | 2727 | 持久化就是把内存中的数据持久化到本地磁盘,防止服务器宕机了内存数据丢失 2728 | 2729 | Redis 提供两种持久化机制 **RDB(默认)** 和 **AOF 机制** 2730 | 2731 | **RDB:**是Redis DataBase缩写快照 2732 | 2733 | ​ RDB是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。 2734 | 2735 | ​ **优点:** 2736 | 2737 | ​ 1)只有一个文件 dump.rdb,方便持久化; 2738 | 2739 | ​ 2)容灾性好,一个文件可以保存到安全的磁盘。 2740 | 2741 | ​ 3)性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能。 2742 | 2743 | ​ 4)相对于数据集大时,比 AOF 的启动效率更高。 2744 | 2745 | ​ **缺点:** 2746 | 2747 | ​ 1)数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候 2748 | 2749 | **AOF:持久化** 2750 | 2751 | ​ AOF持久化(即Append Only File持久化),则是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。 2752 | 2753 | ​ **优点:** 2754 | 2755 | ​ 1)数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次 命令操作就记录到 aof 文件中一次。 2756 | 2757 | ​ 2)通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。 2758 | 2759 | **缺点:** 2760 | 2761 | ​ 1)AOF 文件比 RDB 文件大,且恢复速度慢。 2762 | 2763 | ​ 2)数据集大的时候,比 rdb 启动效率低。 2764 | 2765 | 2766 | 2767 | **8、redis事务** 2768 | 2769 | ​ 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。 2770 | 2771 | **Redis事务的概念** 2772 | 2773 | ​ Redis 事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。 2774 | 2775 | Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的 2776 | 2777 | Redis会将一个事务中的所有命令序列化,然后按顺序执行。 2778 | 2779 | Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。 2780 | 2781 | **事务命令:** 2782 | 2783 | **MULTI:**用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。 2784 | 2785 | **EXEC:**执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。 2786 | 2787 | **WATCH :**是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。 2788 | 2789 | **DISCARD:**调用该命令,客户端可以清空事务队列,并放弃执行事务,且客户端会从事务状态中退出。 2790 | 2791 | **UNWATCH**:命令可以取消watch对所有key的监控。 2792 | 2793 | 2794 | 2795 | **9、缓存雪崩和缓存穿透,以及解决方法** 2796 | 2797 | **【1】缓存雪崩:** 2798 | 2799 | ​ 指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。 2800 | 2801 | ​ **解决方案:** 2802 | 2803 | ​ 1)缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。 2804 | 2805 | ​ 2)一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。 2806 | 2807 | ​ 3)给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存。 2808 | 2809 | **【2】缓存穿透:** 2810 | 2811 | ​ 缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。 2812 | 2813 | ​ **解决方案:** 2814 | 2815 | ​ 1)接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截; 2816 | 2817 | ​ 2)从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击; 2818 | 2819 | ​ 3)采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。 2820 | 2821 | **【3】缓存击穿:** 2822 | 2823 | ​ 这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库 2824 | 2825 | ​ **解决方案:** 2826 | 2827 | ​ 1)设置热点数据永远不过期 2828 | 2829 | ​ 2)加互斥锁,互斥锁 2830 | 2831 | 2832 | 2833 | 2834 | 2835 | **10、如何保证缓存和数据库的数据一致性?** 2836 | 2837 | **方式一:** 2838 | 2839 | ​ 读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。 2840 | 2841 | **方式二:** 2842 | 2843 | ​ 先更新数据库,假如读缓存失败,先读数据库,再回写缓存的方式实现 2844 | 2845 | 2846 | 2847 | **11、redis是单线程还是多线程?为什么那么快?** 2848 | 2849 | redis是单线程,快的原因: 2850 | 2851 | ​ 1)完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1); 2852 | 2853 | ​ 2)数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的; 2854 | 2855 | ​ 3)采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗; 2856 | 2857 | ​ 4)使用多路 I/O 复用模型,非阻塞 IO; 2858 | 2859 | ​ 5)使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求; 2860 | 2861 | 2862 | 2863 | **12、五种IO模型的区别** 2864 | 2865 | **阻塞I/O:** 2866 | 2867 | ​ 当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。 2868 | 2869 | **非阻塞I/O:** 2870 | 2871 | ​ 在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。 2872 | 2873 | **多路复用I/O(select和poll):** 2874 | 2875 | ​ IO多路转接是多了一个select函数,select函数有一个参数是文件描述符集合,对这些文件描述符进行循环监听,当某个文件描述符就绪时,就对这个文件描述符进行处理。其中,select只负责等,recvfrom只负责拷贝。 IO多路转接是属于阻塞IO,但可以对多个文件描述符进行阻塞监听,所以效率较阻塞IO的高。 2876 | 2877 | **信号驱动I/O(SIGIO):** 2878 | 2879 | ​ 信号驱动IO模型,应用进程告诉内核:当数据报准备好的时候,给我发送一个信号,对SIGIO信号进行捕捉,并且调用我的信号处理函数来获取数据报。 2880 | 2881 | **异步I/O(Posix.1的aio_系列函数):** 2882 | 2883 | ​ 当应用程序调用aio_read时,内核一方面去取数据报内容返回,另一方面将程序控制权还给应用进程,应用进程继续处理其他事情,是一种非阻塞的状态。当内核中有数据报就绪时,由内核将数据报拷贝到应用程序中,返回aio_read中定义好的函数处理程序。 2884 | 2885 | 2886 | 2887 | **可以看出,阻塞程度:阻塞IO>非阻塞IO>多路转接IO>信号驱动IO>异步IO,效率是由低到高的**。 2888 | 2889 | 2890 | 2891 | 2892 | 2893 | **13、select、poll、epoll的区别?** 2894 | 2895 | 参考:https://www.jianshu.com/p/dfd940e7fca2 2896 | 2897 | **select** 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。 2898 | 2899 | **poll **本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。 2900 | 2901 | **epoll **支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。 2902 | 2903 | 2904 | 2905 | **14、redis热key问题?如何发现以及如何解决?** 2906 | 2907 | ​ 缓存中的一个Key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。 2908 | 2909 | **解决方案:** 2910 | 2911 | ​ 对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询 2912 | 2913 | 2914 | 2915 | **15、redis数据分布方式?有什么优点?一致性hash呢?** 2916 | 2917 | **Hash:** 2918 | 2919 | ​ 客户端分片:哈希+取余 2920 | 2921 | ​ 节点伸缩:数据节点关系变化,导致数据迁移 2922 | 2923 | ​ 迁移数量和添加节点数量有关:建议翻倍扩容 2924 | 2925 | ​ 一个简单直观的想法是直接用Hash来计算,以Key做哈希后对节点数取模。可以看出,在key足够分散的情况下,均匀性可以获得,但一旦有节点加入或退出,所有的原有节点都会受到影响,稳定性无从谈起。 2926 | 2927 | **一致性Hash:** 2928 | 2929 | ​ 客户端分片:哈希+顺时针(优化取余) 2930 | 2931 | ​ 节点伸缩:只影响邻近节点,但是还是有数据迁移 2932 | 2933 | ​ 翻倍伸缩:保证最小迁移数据和负载均衡 2934 | 2935 | ​ 一致性Hash可以很好的解决稳定问题,可以将所有的存储节点排列在收尾相接的Hash环上,每个key在计算Hash后会顺时针找到先遇到的一组存储节点存放。而当有节点加入或退出时,仅影响该节点在Hash环上顺时针相邻的后续节点,将数据从该节点接收或者给予。但这有带来均匀性的问题,即使可以将存储节点等距排列,也会在**存储节点个数变化时带来数据的不均匀**。而这种可能成倍数的不均匀在实际工程中是不可接受的。 2936 | 2937 | 2938 | 2939 | **16、redis主从复制** 2940 | 2941 | **主从复制原理:** 2942 | 2943 | 当启动一个 slave node 的时候,它会发送一个 PSYNC 命令给 master node。 2944 | 2945 | 如果这是 slave node 初次连接到 master node,那么会触发一次 full resynchronization 全量复制。此时 master 会启动一个后台线程,开始生成一份 RDB 快照文件, 2946 | 2947 | 同时还会将从客户端 client 新收到的所有写命令缓存在内存中。RDB 文件生成完毕后, master 会将这个 RDB 发送给 slave,slave 会先写入本地磁盘,然后再从本地磁盘加载到内存中, 2948 | 2949 | 接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。 2950 | 2951 | slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。 2952 | 2953 | **过程原理** 2954 | 2955 | ​ 1、当从库和主库建立MS关系后,会向主数据库发送SYNC命令 2956 | 2957 | ​ 2、主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令缓存起来 2958 | 2959 | ​ 3、当快照完成后,主Redis会将快照文件和所有缓存的写命令发送给从Redis 2960 | 2961 | ​ 4、从Redis接收到后,会载入快照文件并且执行收到的缓存的命令 2962 | 2963 | ​ 5、之后,主Redis每当接收到写命令时就会将命令发送从Redis,从而保证数据的一致 2964 | 2965 | **缺点** 2966 | 2967 | ​ 所有的slave节点数据的复制和同步都由master节点来处理,会照成master节点压力太大,使用主从从结构来解决 2968 | 2969 | ## **七、Spring 篇** 2970 | 2971 | **1、Spring IOC** 2972 | 2973 | ​ IoC(Inverse of Control:控制反转)是⼀种设计思想,就是 将原本在程序中⼿动创建对象的控制 2974 | 权,交由Spring框架来管理。 IoC 在其他语⾔中也有应⽤,并⾮ Spring 特有。 2975 | 2976 | ​ IoC 容器是 Spring⽤来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注⼊。这样可以很⼤程度上简化应⽤的开发,把应⽤从复杂的依赖关系中解放出来。 IoC 容器就像是⼀个⼯⼚⼀样,当我们需要创建⼀个对象的时候,只需要配置好配置⽂件/注解即可,完全不⽤考虑对象是如何被创建出来的。 2977 | 2978 | 2979 | 2980 | **2、Spring AOP,动态代理** 2981 | 2982 | ​ AOP(Aspect-Oriented Programming:⾯向切⾯编程)能够将那些与业务⽆关,却为业务模块所共同调⽤ 2983 | 的逻辑或责任(例如事务处理、⽇志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模 2984 | 块间的耦合度,并有利于未来的可拓展性和可维护性。 2985 | 2986 | ​ Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接⼝,那么Spring AOP会使⽤JDK 2987 | Proxy,去创建代理对象,⽽对于没有实现接⼝的对象,就⽆法使⽤ JDK Proxy 去进⾏代理了,这时候 2988 | Spring AOP会使⽤Cglib ,这时候Spring AOP会使⽤ Cglib ⽣成⼀个被代理对象的⼦类来作为代理, 2989 | 2990 | 2991 | 2992 | **3、Bean生命周期** 2993 | 2994 | **单例对象:** singleton 2995 | 2996 | 出生:当容器创建时对象出生 2997 | 2998 | 活着:只要容器还在,对象一直或者 2999 | 3000 | 死亡:容器销毁,对象消亡 3001 | 3002 | 总结:单例对象的生命周期和容器相同 3003 | 3004 | **多例对象:** prototype 3005 | 3006 | 出生: 使用对象时spring框架为我们创建 3007 | 3008 | 活着:对象只要是在使用过程中就一直活着 3009 | 3010 | 死亡:当对象长时间不用且没有其它对象引用时,由java的垃圾回收机制回收 3011 | 3012 | 3013 | 3014 | **4、Bean作用域?默认什么级别?是否线程安全?Spring如何保障线程安全的?** 3015 | 3016 | | 名称 | | 3017 | | -------------- | ------------------------------------------------------------ | 3018 | | singleton | 单例对象,默认值的作用域 | 3019 | | prototype | 每次获取都会创建⼀个新的 bean 实例 | 3020 | | request | 每⼀次HTTP请求都会产⽣⼀个新的bean,该bean仅在当前HTTP request内有效。 | 3021 | | session | 在一次 HTTP session 中,容器将返回同一个实例 | 3022 | | global-session | 将对象存入到web项目集群的session域中,若不存在集群,则global session相当于session | 3023 | 3024 | 默认作用域是singleton,多个线程访问同一个bean时会存在线程不安全问题 3025 | 3026 | **保障线程安全方法:** 3027 | 3028 | 1. 在Bean对象中尽量避免定义可变的成员变量(不太现实)。 3029 | 3030 | 2. 在类中定义⼀个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中 3031 | 3032 | **ThreadLocal**: 3033 | 3034 | ​ 每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。 3035 | 3036 | ​ 将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后**在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象**,避免了将这个对象作为参数传递的麻烦。 3037 | 3038 | 3039 | 3040 | **5、Spring事务隔离级别和事务传播属性** 3041 | 3042 | **隔离级别:** 3043 | 3044 | **1) DEFAULT (默认)** 3045 | 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别。另外四个与JDBC的隔离级别相对应。 3046 | 3047 | **2) READ_UNCOMMITTED (读未提交)** 3048 | 这是事务最低的隔离级别,它允许另外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。 3049 | 3050 | **3) READ_COMMITTED (读已提交)** 3051 | 保证一个事务修改的数据提交后才能被另外一个事务读取,另外一个事务不能读取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻像读。 3052 | 3053 | **4) REPEATABLE_READ (可重复读)** 3054 | 这种事务隔离级别可以防止脏读、不可重复读,但是可能出现幻像读。它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了不可重复读。 3055 | 3056 | **5) SERIALIZABLE(串行化)** 3057 | 这是花费最高代价但是最可靠的事务隔离级别,事务被处理为顺序执行。除了防止脏读、不可重复读外,还避免了幻像读。 3058 | 3059 | 3060 | 3061 | **Spring事务传播属性(Propagation):** 3062 | 3063 | **1) REQUIRED(默认属性)** 3064 | 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。 被设置成这个级别时,会为每一个被调用的方法创建一个逻辑事务域。如果前面的方法已经创建了事务,那么后面的方法支持当前的事务,如果当前没有事务会重新建立事务。 3065 | 3066 | **2) MANDATORY** 3067 | 支持当前事务,如果当前没有事务,就抛出异常。 3068 | 3069 | **3) NEVER** 3070 | 以非事务方式执行,如果当前存在事务,则抛出异常。 3071 | 3072 | **4) NOT_SUPPORTED** 3073 | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 3074 | 3075 | **5) REQUIRES_NEW** 3076 | 新建事务,如果当前存在事务,把当前事务挂起。 3077 | 3078 | **6) SUPPORTS** 3079 | 支持当前事务,如果当前没有事务,就以非事务方式执行。 3080 | 3081 | **7) NESTED** 3082 | 支持当前事务,新增Savepoint点,与当前事务同步提交或回滚。 嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。 3083 | 3084 | 3085 | 3086 | **6、Spring以及Spring MVC常见注解** 3087 | 3088 | ***Spring部分:*** 3089 | 3090 | ​ **声明bean的注解** 3091 | 3092 | ​ **@Component** 通⽤的注解,可标注任意类为 Spring 组件 3093 | 3094 | ​ **@Service** 在业务逻辑层使用(service层) 3095 | 3096 | ​ **@Repository** 在数据访问层使用(dao层) 3097 | 3098 | ​ **@Controller** 在展现层使用,控制器的声明(controller层) 3099 | 3100 | ​ **注入bean的注解** 3101 | 3102 | ​ **@Autowired**:可以对类成员变量、方法、构造方法进行标注 3103 | 3104 | ​ 默认按照类型注入,若要按照名称注入,需要搭配**@Qualifier**注解一起使用 3105 | 3106 | ​ **@Resource**:默认按照名称来装配注入 3107 | 3108 | **Spring MVC部分:** 3109 | 3110 | ​ **@Controller** 声明该类为SpringMVC中的Controller 3111 | 3112 | ​ **@RequestMapping** 用于映射Web请求 3113 | 3114 | ​ **@ResponseBody** 支持将返回值放在response内,而不是一个页面,通常用户返回json数据 3115 | 3116 | ​ **@RequestBody** 允许request的参数在request体中,而不是在直接连接在地址后面。 3117 | 3118 | ​ **@PathVariable** 用于接收路径参数,比如@RequestMapping("/hello/{name}")申明的路径,将注解放在参数中前,即可获取该值,通常作为Restful的接口实现方法。 3119 | 3120 | 3121 | 3122 | **7、@autowired和@resource的区别?** 3123 | 3124 | **@Autowired**:可以对类成员变量、方法、构造方法进行标注 3125 | 3126 | ​ 默认按照类型注入,若要按照名称注入,需要搭配**@Qualifier**注解一起使用 3127 | 3128 | **@Resource**:默认按照名称来装配注入 3129 | 3130 | 3131 | 3132 | **8、mybatis如何防止sql注入?$和#的区别是什么?传入表名用哪个?** 3133 | 3134 | **防止sql注入:** 3135 | 3136 | ​ 在编写mybatis的映射语句时,尽量采用“#{xxx}”这样的格式 3137 | 3138 | **#和$区别:** 3139 | 3140 | | # | $ | 3141 | | ------------------------------------------------------- | ----------------------------------- | 3142 | | 相当于对数据加上双引号 | 相当于直接显示数据 | 3143 | | 很大程度上防止SQL注入 | 无法防止SQL注入 | 3144 | | #{xxx},使用的是PreparedStatement,会有类型转换,比较安全 | ${xxx},使用字符串拼接,容易SQL注入 | 3145 | 3146 | ​ 简单的说就是#{}是经过预编译的,是安全的,**$**{}是未经过预编译的,仅仅是取变量的值,是非安全的,存在SQL注入。 3147 | 3148 | 要实现动态传入表名、列名,需要做如下修改: 3149 | 3150 | **添加属性statementType="STATEMENT"**,**同时sql里的属有变量取值都改成${xxxx}** 3151 | 3152 | 3153 | 3154 | **9、Spring MVC工作原理** 3155 | 3156 | ![](https://img-blog.csdn.net/20181022224058617?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2F3YWtlX2xxaA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) 3157 | 3158 | 1. 客户端(浏览器)发送请求,直接请求到 DispatcherServlet 。 3159 | 2. DispatcherServlet 根据请求信息调⽤ HandlerMapping ,解析请求对应的 Handler 。 3160 | 3. 解析到对应的 Handler (也就是 Controller 控制器)后,开始由HandlerAdapter 适配器处理。 3161 | 4. HandlerAdapter 会根据 Handler 来调⽤真正的处理器开处理请求,并处理相应的业务逻辑。 3162 | 5. 处理器处理完业务后,会返回⼀个 ModelAndView 对象, Model 是返回的数据对象 3163 | 6. ViewResolver 会根据逻辑 View 查找实际的 View 。 3164 | 7. DispaterServlet 把返回的 Model 传给 View (视图渲染)。 3165 | 8. 把 View 返回给请求者(浏览器) 3166 | 3167 | 3168 | 3169 | **10、SpringBoot自动配置的原理是什么?介绍SpringBootApplication注解.** 3170 | 3171 | 启动类: 3172 | 3173 | ```java 3174 | @SpringBootApplication 3175 | public class JpaApplication { 3176 | public static void main(String[] args) { 3177 | SpringApplication.run(JpaApplication.class, args); 3178 | } 3179 | } 3180 | ``` 3181 | 3182 | 它主要加载了@SpringBootApplication注解主配置类,这个@SpringBootApplication注解主配置类里边最主要的功能就是SpringBoot开启了一个@EnableAutoConfiguration注解的自动配置功能。 3183 | 3184 | **@EnableAutoConfiguration作用:** 3185 | 3186 | 它主要利用了一个 3187 | 3188 | EnableAutoConfigurationImportSelector选择器给Spring容器中来导入一些组件。 3189 | 3190 | ```java 3191 | @Import(EnableAutoConfigurationImportSelector.class) 3192 | public @interface EnableAutoConfiguration 3193 | ``` 3194 | 3195 | 3196 | 3197 | **@SpringBootApplication**注解等同于下面三个注解: 3198 | 3199 | - **@SpringBootConfiguration:** 底层是**Configuration**注解,说白了就是支持**JavaConfig**的方式来进行配置 3200 | - **@EnableAutoConfiguration:**开启**自动配置**功能 3201 | - **@ComponentScan:**就是**扫描**注解,默认是扫描**当前类下**的package 3202 | 3203 | 其中`@EnableAutoConfiguration`是关键(启用自动配置),内部实际上就去加载`META-INF/spring.factories`文件的信息,然后筛选出以`EnableAutoConfiguration`为key的数据,加载到IOC容器中,实现自动配置功能! 3204 | 3205 | 3206 | 3207 | **11、Mybatis和Hibernate的区别** 3208 | 3209 | **Hibernate 框架:** 3210 | 3211 | ​ **Hibernate**是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,建立对象与数据库表的映射。是一个全自动的、完全面向对象的持久层框架。 3212 | 3213 | **Mybatis框架:** 3214 | 3215 | ​ **Mybatis**是一个开源对象关系映射框架,原名:ibatis,2010年由谷歌接管以后更名。是一个半自动化的持久层框架。 3216 | 3217 | **区别:** 3218 | 3219 | **开发方面** 3220 | 3221 | ​ 在项目开发过程当中,就速度而言: 3222 | 3223 | ​ hibernate开发中,sql语句已经被封装,直接可以使用,加快系统开发; 3224 | 3225 | ​ Mybatis 属于半自动化,sql需要手工完成,稍微繁琐; 3226 | 3227 | ​ 但是,凡事都不是绝对的,如果对于庞大复杂的系统项目来说,复杂语句较多,hibernate 就不是好方案。 3228 | 3229 | **sql优化方面** 3230 | 3231 | ​ Hibernate 自动生成sql,有些语句较为繁琐,会多消耗一些性能; 3232 | 3233 | ​ Mybatis 手动编写sql,可以避免不需要的查询,提高系统性能; 3234 | 3235 | **对象管理比对** 3236 | 3237 | ​ Hibernate 是完整的对象-关系映射的框架,开发工程中,无需过多关注底层实现,只要去管理对象即可; 3238 | 3239 | ​ Mybatis 需要自行管理 映射关系; 3240 | 3241 | 3242 | 3243 | **12、spring中的@Autowired注解原理?** 3244 | 3245 | ​ @Autowired的使用简化了我们的开发,其原理是使用 AutowiredAnnotationBeanPostProcessor 类来实现,该类实现了 Spring 框架的一些扩展接口,通过实现 BeanFactoryAware 接口使其内部持有了 BeanFactory(可轻松的获取需要依赖的的 Bean);通过实现 MergedBeanDefinitionPostProcessor 扩展接口,在 BeanFactory 里面的每个 Bean 实例化前获取到每个 Bean 里面的 @Autowired 信息并缓存下来;通过实现 Spring 框架的 postProcessPropertyValues 扩展接口在 BeanFactory 里面的每个 Bean 实例后从缓存取出对应的注解信息,获取依赖对象,并通过反射设置到 Bean 属性里面。 3246 | 3247 | 3248 | 3249 | **13、Spring中用到了哪些设计模式?单例、工厂、代理、适配、观察者之类的说一说就行** 3250 | 3251 | 参考:[spring中的设计模式](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485303&idx=1&sn=9e4626a1e3f001f9b0d84a6fa0cff04a&chksm=cea248bcf9d5c1aaf48b67cc52bac74eb29d6037848d6cf213b0e5466f2d1fda970db700ba41&token=255050878&lang=zh_CN%23rd) 3252 | 3253 | **单例设计模式 :** Spring 中的 Bean 默认都是单例的。 3254 | 3255 | **⼯⼚设计模式 :** Spring使⽤⼯⼚模式通过 BeanFactory 、 ApplicationContext 创建bean 对象。 3256 | 3257 | **代理设计模式 :** Spring AOP 功能的实现。 3258 | 3259 | **观察者模式:** Spring 事件驱动模型就是观察者模式很经典的⼀个应⽤。 3260 | 3261 | **适配器模式:**Spring AOP 的增强或通知(Advice)使⽤到了适配器模式、spring MVC 中也是⽤到了适配器模式适配 Controller 。 3262 | 3263 | 3264 | 3265 | ## 八、**客观题** 3266 | 3267 | 3268 | 3269 | **1、100亿黑名单URL,每个64B,问这个黑名单要怎么存?判断一个URL是否在黑名单中** 3270 | 3271 | ​ **散列表:** 3272 | 3273 | ​ 如果把黑名单看成一个集合,将其存在 hashmap 中,貌似太大了,需要 640G,明显不科学。 3274 | 3275 | ​ **布隆过滤器:** 3276 | 3277 | ​ 它实际上是一个很长的二进制矢量和一系列随机映射函数。 3278 | 3279 | ​ 它**可以用来判断一个元素是否在一个集合中**。它的优势是只需要占用很小的内存空间以及有着高效的查询效率。对于布隆过滤器而言,它的本质是一个**位数组**:位数组就是数组的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1。 3280 | 3281 | ​ 在数组中的每一位都是二进制位。布隆过滤器除了一个位数组,还有 K 个哈希函数。当一个元素加入布隆过滤器中的时候,会进行如下操作: 3282 | 3283 | - 使用 K 个哈希函数对元素值进行 K 次计算,得到 K 个哈希值。 3284 | - 根据得到的哈希值,在位数组中把对应下标的值置为 1。 3285 | 3286 | 3287 | 3288 | **2、2GB内存在20亿整数中找到出现次数最多的数** 3289 | 3290 | ​ 通常做法是使用哈希表对出现的每一个数做词频统计,哈希表的key是某个整数,value记录整数出现的次数。本题的数据量是20亿,有可能一个数出现20亿次,则为了避免溢出,哈希表的key是32位(4B),value也是 32位(4B),那么一条哈希表的记录就需要占用8B。 3291 | 3292 | ​ 当哈希表记录数为2亿个时,需要16亿个字节数(8\*2亿),需要至少1.6GB内存(16亿/2^30,1GB== 2 ^30个字节 == 10亿)。则20亿个记录,至少需要16GB的内存,不符合题目要求。 3293 | 3294 | ​ 解决办法是将20亿个数的大文件利用哈希函数分成16个小文件,根据哈希函数可以把20亿条数据均匀分布到16个文件上,同一种数不可能被哈希函数分到不同的小文件上,假设哈希函数够好。然后对每一个小文件用哈希函数来统计其中每种数出现的次数,这样我们就得到16个文件中出现次数最多的数,接着从16个数中选出次数最大的那个key即可。 3295 | 3296 | 3297 | 3298 | **3、40亿个非负整数中找到没有出现的数** 3299 | 3300 | ​ 对于原问题,如果使用哈希表来保存出现过的数,那么最坏情况下是40亿个数都不相同,那么哈希表则需要保存40亿条数据,一个32位整数需要4B,那么40亿*4B = 160亿个字节,一般大概10亿个字节的数据需要1G的空间,那么大概需要16G的空间,这不符合要求。 3301 | 3302 |   我们换一种方式,申请一个bit数组,数组大小为4294967295,大概为40亿bit,40亿/8 = 5亿字节,那么需要0.5G空间, bit数组的每个位置有两种状态0和1,那么怎么使用这个bit数组呢?呵呵,数组的长度刚好满足我们整数的个数范围,那么数组的每个下标值对应4294967295中的一个数,逐个遍历40亿个无符号数,例如,遇到20,则bitArray[20] = 1;遇到666,则bitArray[666] = 1,遍历完所有的数,将数组相应位置变为1。 3303 | 3304 | 3305 | 3306 | **4、40亿个非负整数中找到一个没有出现的数,内存限制10MB** 3307 | 3308 | ​ 本题将内存空间缩小为10MB,对于40亿个数据来说那是明显不够用的,那么我们只有将数据分块处理,分块应该怎么分,分多少块合理呢?根据我做过的题经验来看,10亿个字节的数据大概需要1GB空间处理(如果这个结论不正确欢迎读者指出),那么10MB内存换算过来就是可以处理1千万字节的数据,也就是8千万bit,对于40亿非负整数如果申请bit数组的话,40亿bit / 0.8亿bit = 50,那么这样最少也得分50块来处理,处理每块数据的时候几乎用完了内存空间,这样也不太好。看书上解说是分成了64块,至于为什么是64我目前也不是很了解,我只知道最少50块。所以下面就以64块来进行分析解答吧。 3309 | 3310 |   首先,将0 - 4294967259这个范围平均分成64个区间,每个区间是67108864个数,为了定位更加准确一些,我们先开辟一个大小为64的整型数组intArray,将40亿个数进行区间划分,第0区间(0-67108863)、第一区间(67108864-134217728)、第i区间(67108864*i-67108864*(i+1)-1),......,第63区间(4227858432 - 4294967259)。intArray分别记录每个区间出现的数的个数,肯定至少有一个区间上的计数少于67108864.利用这一点可以快速找出一个没有出现过的数。 3311 | 3312 | ​ 第一次遍历时,先申请长度为64的整型数组countArr[0..63],countArr[i]用来统计区间i 上的数有多少。遍历40亿个数,根据当前数是多少来决定哪一个区间上的计数增加。例如,如果当前数是3422552090,3422552090/67108864=51,所以第51区间上的计数增加countArr[51]++。遍历完40亿个数之后,遍历countArr,必然会有某一个位置上的值(countArr[i])小于67108864,表示第i 区间上至少有一个数没出现过。我们肯定会至少找到一个这样的区间。此时使用的内存就是countArr的大小(64×4B),是非常小的。 3313 | 3314 | 假设我们找到第37区间上的计数小于67108864,以下为第二次遍历的过程: 3315 | 3316 | 1.申请长度为67108864的bit map,这占用大约8MB的空间,记为bitArr[0..67108863]; 3317 | 3318 | 2.再遍历一次40亿个数,此时的遍历只关注落在第37区间上的数,记为num(num/67108864==37),其他区间的数全部忽略。 3319 | 3320 | 3.如果步骤2的num在第37区间上,将bitArr[num - 67108864*37]的值设置为1,也就是只做第37区间上的数的bitArr映射。 3321 | 3322 | 4.遍历完40亿个数之后,在bitArr上必然存在没被设置成1的位置,假设第i 个位置上的值没设置成1,那么67108864×37+i 这个数就是一个没出现过的数。 3323 | 3324 | **总结一下进阶的解法:** 3325 | 3326 | 1.根据10MB的内存限制,确定统计区间的大小,就是第二次遍历时的bitArr大小。 3327 | 3328 | 2.利用区间计数的方式,找到那个计数不足的区间,这个区间上肯定有没出现的数。 3329 | 3330 | 3.对这个区间上的数做bit map映射,再遍历bit map,找到一个没出现的数即可。 3331 | 3332 | 3333 | 3334 | **5、找到100亿个URL中重复的URL** 3335 | 3336 | ​ 原问题的解法使用解决大数据问题的一种常规方法:把大文件通过哈希函数分配到机器,或者通过哈希函数把大文件拆成小文件。一直进行这种划分,直到划分的结果满足资源限制的要求。首先,你要向面试官询问在资源上的限制有哪些,包括内存、计算时间等要求。在明确了限制要求之后,可以将每条URL通过哈希函数分配到若干机器或者拆分成若干小文件,这里的“若干”由具体的资源限制来计算出精确的数量。 3337 | 3338 | ​ 例如,将100亿字节的大文件通过哈希函数分配到100台机器上,然后每一台机器分别统计分给自己的URL中是否有重复的URL,**同时哈希函数的性质决定了同一条URL不可能分给不同的机器;**或者在单机上将大文件通过哈希函数拆成1000个小文件,对每一个小文件再利用哈希表遍历,找出重复的URL;或者在分给机器或拆完文件之后,进行排序,排序过后再看是否有重复的URL出现。总之,牢记一点,很多大数据问题都离不开分流,要么是哈希函数把大文件的内容分配给不同的机器,要么是哈希函数把大文件拆成小文件,然后处理每一个小数量的集合。 3339 | 3340 | 3341 | 3342 | **6、海量搜索词汇,找到最热TOP100词汇的方法** 3343 | 3344 | ​ 最开始还是用哈希分流的思路来处理,把包含百亿数据量的词汇文件分流到不同的机器上,具体多少台机器由面试官规定或者由更多的限制来决定。对每一台机器来说,如果分到的数据量依然很大,比如,内存不够或其他问题,可以再用哈希函数把每台机器的分流文件拆成更小的文件处理。 3345 | 3346 | ​ 处理每一个小文件的时候,哈希表统计每种词及其词频,哈希表记录建立完成后,再遍历哈希表,遍历哈希表的过程中使用大小为100的小根堆来选出每一个小文件的top 100(整体未排序的top 100)。每一个小文件都有自己词频的小根堆(整体未排序的top 100),将小根堆里的词按照词频排序,就得到了每个小文件的排序后top 100。然后把各个小文件排序后的top 100进行外排序或者继续利用小根堆,就可以选出每台机器上的top 100。不同机器之间的top100再进行外排序或者继续利用小根堆,最终求出整个百亿数据量中的top 100。对于top K 的问题,除哈希函数分流和用哈希表做词频统计之外,还经常用堆结构和外排序的手段进行处理。 3347 | 3348 | 3349 | 3350 | **7、40亿个无符号整数,1GB内存,找到所有出现两次的数** 3351 | 3352 | ​ 对于原问题,可以用bit map的方式来表示数出现的情况。具体地说,是申请一个长度为4294967295×2的bit类型的数组bitArr,用2个位置表示一个数出现的词频,1B占用8个bit,所以长度为4294967295×2的bit类型的数组占用1GB空间。怎么使用这个bitArr数组呢?遍历这40亿个无符号数,如果初次遇到num,就把bitArr[num*2 + 1]和bitArr[num*2]设置为01,如果第二次遇到num,就把bitArr[num*2+1]和bitArr[num*2]设置为10,如果第三次遇到num,就把bitArr[num*2+1]和bitArr[num*2]设置为11。以后再遇到num,发现此时bitArr[num*2+1]和bitArr[num*2]已经被设置为11,就不再做任何设置。遍历完成后,再依次遍历bitArr,如果发现bitArr[i*2+1]和bitArr[i*2]设置为10,那么i 就是出现了两次的数。 3353 | 3354 | 3355 | 3356 | **8、10MB内存,找到40亿整数的中位数** 3357 | 3358 | ①内存够:内存够还慌什么啊,直接把100亿个全部排序了,你用冒泡都可以...然后找到中间那个就可以了。但是你以为面试官会给你内存?? 3359 | 3360 | ②内存不够:题目说是整数,我们认为是带符号的int,所以4字节,占32位。 3361 | 3362 | 假设100亿个数字保存在一个大文件中,依次读一部分文件到内存(不超过内存的限制),将每个数字用二进制表示,比较二进制的最高位(第32位,符号位,0是正,1是负),如果数字的最高位为0,则将这个数字写入 file_0文件中;如果最高位为 1,则将该数字写入file_1文件中。 3363 | 3364 | 从而将100亿个数字分成了两个文件,假设 file_0文件中有 60亿 个数字,file_1文件中有 40亿 个数字。那么中位数就在 file_0 文件中,并且是 file_0 文件中所有数字排序之后的第 10亿 个数字。(file_1中的数都是负数,file_0中的数都是正数,也即这里一共只有40亿个负数,那么排序之后的第50亿个数一定位于file_0中) 3365 | 3366 | 现在,我们只需要处理 file_0 文件了(不需要再考虑file_1文件)。对于 file_0 文件,同样采取上面的措施处理:将file_0文件依次读一部分到内存(不超内存限制),将每个数字用二进制表示,比较二进制的 次高位(第31位),如果数字的次高位为0,写入file_0_0文件中;如果次高位为1,写入file_0_1文件 中。 3367 | 3368 | 现假设 file_0_0文件中有30亿个数字,file_0_1中也有30亿个数字,则中位数就是:file_0_0文件中的数字从小到大排序之后的第10亿个数字。 3369 | 3370 | 抛弃file_0_1文件,继续对 file_0_0文件 根据 次次高位(第30位) 划分,假设此次划分的两个文件为:file_0_0_0中有5亿个数字,file_0_0_1中有25亿个数字,那么中位数就是 file_0_0_1文件中的所有数字排序之后的 第 5亿 个数。 3371 | 3372 | 按照上述思路,直到划分的文件可直接加载进内存时,就可以直接对数字进行快速排序,找出中位数了。 3373 | 3374 | 3375 | 3376 | **9、设计短域名系统,将长URL转化成短的URL.** 3377 | 3378 | (1)利用放号器,初始值为0,对于每一个短链接生成请求,都递增放号器的值,再将此值转换为62进制(a-zA-Z0-9),比如第一次请求时放号器的值为0,对应62进制为a,第二次请求时放号器的值为1,对应62进制为b,第10001次请求时放号器的值为10000,对应62进制为sBc。 3379 | 3380 | (2)将短链接服务器域名与放号器的62进制值进行字符串连接,即为短链接的URL,比如:[t.cn/sBc。](http://t.cn/sBc。) 3381 | 3382 | (3)重定向过程:生成短链接之后,需要存储短链接到长链接的映射关系,即sBc -> URL,浏览器访问短链接服务器时,根据URL Path取到原始的链接,然后进行302重定向。映射关系可使用K-V存储,比如Redis或Memcache。 3383 | 3384 | 3385 | 3386 | **10、让你系统的设计一个高并发的架构,你会从哪几个方面考虑?** 3387 | 3388 | **系统拆分** 3389 | 3390 | ​ 将一个系统拆分为多个子系统,用 dubbo 来搞。然后每个系统连一个数据库, 这样本来就一个库,现在多个数据库,不也可以扛高并发么。 3391 | 3392 | **缓存** 3393 | 3394 | ​ 缓存,必须得用缓存。大部分的高并发场景,都是读多写少,那你完全可以在数 据库和缓存里都写一份,然后读的时候大量走缓存不就得了。毕竟人家 redis 轻 轻松松单机几万的并发。所以你可以考虑考虑你的项目里,那些承载主要请求的 读场景,怎么用缓存来抗高并**发。** 3395 | 3396 | **MQ** 3397 | 3398 | ​ MQ,必须得用 MQ。可能你还是会出现高并发写的场景,比如说一个业务操作 里要频繁搞数据库几十次,增删改增删改,疯了。那高并发绝对搞挂你的系统, 你要是用 redis 来承载写那肯定不行,人家是缓存,数据随时就被 LRU 了,数 据格式还无比简单,没有事务支持。所以该用 mysql 还得用 mysql 啊。那你 咋办?用 MQ 吧,大量的写请求灌入 MQ 里,排队慢慢玩儿,后边系统消费 后慢慢写,控制在 mysql 承载范围之内。所以你得考虑考虑你的项目里,那些 承载复杂写业务逻辑的场景里,如何用 MQ 来异步写,提升并发性。MQ 单机 抗几万并发也是 ok 的,这个之前还特意说过。 3399 | 3400 | **分库分表** 3401 | 3402 | ​ 分库分表,可能到了最后数据库层面还是免不了抗高并发的要求,好吧,那么就 将一个数据库拆分为多个库,多个库来扛更高的并发;然后将一个表拆分为多个 表,每个表的数据量保持少一点,提高 sql 跑的性能。 3403 | 3404 | **读写分离** 3405 | 3406 | ​ 读写分离,这个就是说大部分时候数据库可能也是读多写少,没必要所有请求都 集中在一个库上吧,可以搞个主从架构,主库写入,从库读取,搞一个读写分离。 读流量太多的时候,还可以加更多的从库。 3407 | 3408 | **ElasticSearch** 3409 | 3410 | ​ Elasticsearch,简称 es。es 是分布式的,可以随便扩容,分布式天然就可以支 撑高并发,因为动不动就可以扩容加机器来扛更高的并发。那么一些比较简单的 查询、统计类的操作,可以考虑用 es 来承载,还有一些全文搜索类的操作,也 可以考虑用 es 来承载。 3411 | 3412 | 3413 | 3414 | **11、假设有这么一个场景,有一条新闻,新闻的评论量可能很大,如何设计评论的读和写** 3415 | 3416 | 可以进行读写分离、加载到缓存 3417 | 3418 | 3419 | 3420 | **12、显示网站的用户在线数的解决思路** 3421 | 3422 | 1. 维护在线用户表 3423 | 3424 | 2. 使用Redis统计 3425 | 3426 | 3427 | 3428 | ## **九、个人项目** 3429 | 3430 | #### 一、秒杀项目: 3431 | 3432 | **项目架构介绍:** 3433 | 3434 | ​ 系统主要通过缓存,异步,限流来保证系统的高并发和高可用。 3435 | 3436 | https://blog.csdn.net/awake_lqh/article/details/83306983 3437 | 3438 | **1、如何解决超卖?** 3439 | 3440 | mysql乐观锁+redis预减库存+redis缓存卖完标记 3441 | 3442 | 3443 | 3444 | **2、如何解决重复下单?** 3445 | 3446 | mysql唯一索引+分布式锁 3447 | 3448 | 3449 | 3450 | **3、如何防刷?** 3451 | 3452 | IP限流+验证码 3453 | 3454 | 3455 | 3456 | **4、热key问题如何解决?** 3457 | 3458 | redis集群+本地缓存+限流+key加随机值分布在多个实例中 3459 | 3460 | 3461 | 3462 | **5、消息队列的作用?如何保证消息的不丢失?** 3463 | 3464 | 异步削峰;发送方开启confirm+消息队列持久化+消费方关闭自动ACK,确保消费成功之后自动调用API进行确认。 3465 | 3466 | 3467 | 3468 | **6、缓存和数据库数据一致性如何保证?** 3469 | 3470 | 秒杀项目不用保证,其他项目就用延时双删或者先更新数据再是缓存失效,为防缓存失效这一信息丢失,可用消息队列确保。 3471 | 3472 | 3473 | 3474 | 7、压测没有?用什么压测?什么情况? 3475 | 3476 | 3477 | 3478 | 8、系统瓶颈在哪?如何查找,如何再优化? 3479 | 3480 | 3481 | 3482 | 1.秒杀流程 3483 | 2.库存预减用的是哪个redis方法 3484 | 3.如果项目中的redis服务挂掉,如何减轻数据库的压力 3485 | 4.如何避免消息队列的消费方重复消费消息 3486 | 5.消息的消费结果如何返回给消息发送方 3487 | 6.说一下你的秒杀系统设计,怎么解决超卖的 3488 | 7.你说你用到了redis,redis有哪些数据结构,你为什么要用redis,哪里用到了,为什么说redis快,多路io复用详细原理可以说说嘛? 3489 | 8.redis,深入讲一讲,并说一下它的应用场景 3490 | 9.秒杀系统怎么做的,架构图画了一下 3491 | 10.秒杀模块怎么设计的,如何压测,抗压手段,如何保证数据库与redis缓存一致的,消息队列怎么用的 3492 | 11.秒杀系统服务器抗压思路,从哪些方面去优化 3493 | 12.如何解决超卖 3494 | 13.讲讲你做的秒杀项目 3495 | 14.你的秒杀项目,别说你里面的优化,你还有什么优化策略吗?多服务器负载均衡,把秒杀商品平均分给服务器。 3496 | 15.秒杀项目部分实现怎么做的 3497 | 16.秒杀系统的前端设计怎么做? 3498 | 17.说说秒杀如何实现的?(用redis预库存的减少,然后方式异步消息队列rabbitMQ中) 3499 | 18.如何解决商城中超卖问题?秒杀场景呢? 3500 | 19.秒杀过程中怎么保证redis缓存和数据库的一致性 3501 | 20.具体的秒杀细节怎么做的?秒杀的核心技术在哪儿?你怎么保证的? 3502 | 21.秒杀商品的库存放在哪里,如何保证redis和DB的一致性 3503 | 22.设计秒杀方案(从高并发、快速响应、高可用三方面回答,高并发(增加网络带宽、DNS域名解析分发多台服务器、使用前置代理服务器ngnix、CDN内容分发、数据库查询优化(读写分离、分库分表)),快速响应(缓存服务器(memcached、redis)、能使用静态页面就用静态页面,减少容器解析、把常访问的图片等内容缓存)、高可用(热备,如数据库服务器的热备、集群监控(如使用zabbix,重点关注IO、内存、带宽和机器load))) 3504 | 23.秒杀时如果机器资源有限怎么办 3505 | 24.秒杀接口防刷怎么做 3506 | 25.如何防止超卖和少卖 3507 | 26.秒杀系统场景下怎么防止超卖,redis和数据库数据不一致怎么办,以什么为准 3508 | 27.秒杀流程图 如何保证不超卖 以及对应SQL 3509 | --------------------------------------------------------------------------------