├── Collections ├── CSnet.md ├── Exceptiom.md ├── JVM.md ├── JavaBasis.md ├── MySQL.md ├── bingfa.md ├── collections.md └── cs.md └── README.md /Collections/CSnet.md: -------------------------------------------------------------------------------- 1 | # 计算机网络面试题 2 | 3 | ## 1、为什么要对网络协议分层?计算机网络有哪些层? 4 | 5 | ### 原因: 6 | 7 | 简化问题难度和复杂度。由于各层之间独立,我们可以分割大问题为小问题。 8 | 9 | 灵活性好。当其中一层的技术变化时,只要层间接口关系保持不变,其他层不受影响。 10 | 11 | 易于实现和维护。 12 | 13 | 促进标准化工作。分开后,每层功能可以相对简单地被描述。 14 | 15 | 网络协议分层的缺点: 功能可能出现在多个层里,产生了额外开销。 16 | 17 | ### 分类: 18 | 19 | 为了使不同体系结构的计算机网络都能互联,国际标准化组织 ISO 于1977年提出了一个试图使各种计算机在世界范围内互联成网的标准框架,即著名的开放系统互联基本参考模型 OSI/RM,简称为OSI。 20 | OSI 的七层协议体系结构的概念清楚,理论也较完整,但它既复杂又不实用,TCP/IP 体系结构则不同,但它现在却得到了非常广泛的应用。TCP/IP 是一个四层体系结构,它包含应用层,运输层,网际层和网络接口层(用网际层这个名字是强调这一层是为了解决不同网络的互连问题),不过从实质上讲,TCP/IP 只有最上面的三层,因为最下面的网络接口层并没有什么具体内容,因此在学习计算机网络的原理时往往采用折中的办法,即综合 OSI 和 TCP/IP 的优点,采用一种只有五层协议的体系结构,这样既简洁又能将概念阐述清楚,有时为了方便,也可把最底下两层称为网络接口层。 21 | 四层协议,五层协议和七层协议的关系如下: 22 | 23 | ![Image text](https://mubu.com/document_image/23bd60f0-d23c-447a-ab38-99ddee3312ac-4943374.jpg) 24 | 25 | TCP/IP是一个四层的体系结构,主要包括:应用层、运输层、网际层和网络接口层。 26 | 27 | 五层协议的体系结构主要包括:应用层、运输层、网络层,数据链路层和物理层。 28 | 29 | OSI七层协议模型主要包括是:应用层(Application)、表示层(Presentation)、会话层(Session)、运输层(Transport)、网络层(Network)、数据链路层(Data Link)、物理层(Physical)。 30 | 31 | 注:五层协议的体系结构只是为了介绍网络原理而设计的,实际应用还是 TCP/IP 四层体系结构 32 | 33 | 34 | ## 2、详细说说TCP/IP协议族? 35 | 36 | ### 应用层 37 | 38 | 应用层( application-layer )的任务是通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程(进程:主机中正在运行的程序)间的通信和交互的规则。 39 | 对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多,如域名系统 DNS,支持万维网应用的 HTTP 协议,支持电子邮件的 SMTP 协议等等。 40 | 41 | ### 运输层 42 | 43 | 运输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务。应用进程利用该服务传送应用层报文。 44 | 45 | 运输层主要使用一下两种协议 46 | 47 | 传输控制协议-TCP:提供面向连接的,可靠的数据传输服务。 48 | 49 | 用户数据协议-UDP:提供无连接的,尽最大努力的数据传输服务(不保证数据传输的可靠性)。 50 | 51 | 52 | 每一个应用层(TCP/IP参考模型的最高层)协议一般都会使用到两个传输层协议之一: 53 | 54 | 运行在TCP协议上的协议: 55 | 56 | + HTTP(Hypertext Transfer Protocol,超文本传输协议),主要用于普通浏览。 57 | + HTTPS(HTTP over SSL,安全超文本传输协议),HTTP协议的安全版本。 58 | + FTP(File Transfer Protocol,文件传输协议),用于文件传输。 59 | + POP3(Post Office Protocol, version 3,邮局协议),收邮件用。 60 | + SMTP(Simple Mail Transfer Protocol,简单邮件传输协议),用来发送电子邮件。 61 | + TELNET(Teletype over the Network,网络电传),通过一个终端(terminal)登陆到网络。 62 | + SSH(Secure Shell,用于替代安全性差的TELNET),用于加密安全登陆用。 63 | 64 | 运行在UDP协议上的协议: 65 | 66 | + BOOTP(Boot Protocol,启动协议),应用于无盘设备。 67 | + NTP(Network Time Protocol,网络时间协议),用于网络同步。 68 | + DHCP(Dynamic Host Configuration Protocol,动态主机配置协议),动态配置IP地址。 69 | + 运行在TCP和UDP协议上: 70 | + DNS(Domain Name Service,域名服务),用于完成地址查找,邮件转发等工作。 71 | 72 | ![Image text](https://mubu.com/document_image/0a128a5c-86cd-4618-b3d0-b6b214a8e7f3-4943374.jpg) 73 | 74 | ### 网络层 75 | 76 | 网络层的任务就是选择合适的网间路由和交换结点,确保计算机通信的数据及时传送。在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。在 TCP/IP 体系结构中,由于网络层使用 IP 协议,因此分组也叫 IP 数据报 ,简称数据报。 77 | 互联网是由大量的异构(heterogeneous)网络通过路由器(router)相互连接起来的。互联网使用的网络层协议是无连接的网际协议(Intert Prococol)和许多路由选择协议,因此互联网的网络层也叫做网际层或 IP 层。 78 | 79 | ### 数据链路层 80 | 81 | 数据链路层(data link layer)通常简称为链路层。两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。 82 | 在两个相邻节点之间传送数据时,数据链路层将网络层交下来的 IP 数据报组装成帧,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息(如同步信息,地址信息,差错控制等)。 83 | 在接收数据时,控制信息使接收端能够知道一个帧从哪个比特开始和到哪个比特结束。 84 | 85 | ### 物理层 86 | 在物理层上所传送的数据单位是比特。 物理层(physical layer)的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。使其上面的数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的。 87 | 88 | ## 3、TCP的三次握手四次挥手? 89 | 90 | 三次握手(三次握手的本质是确认通信双方收发数据的能力) 91 | 92 | ![Image text](https://mubu.com/document_image/d59cc8c8-70cb-4703-80d9-bc8e85c7713c-4943374.jpg) 93 | 94 | 第一次握手:客户端要向服务端发起连接请求,首先客户端随机生成一个起始序列号ISN(比如是100),那客户端向服务端发送的报文段包含SYN标志位(也就是SYN=1),序列号seq=100。 95 | 96 | 第二次握手:服务端收到客户端发过来的报文后,发现SYN=1,知道这是一个连接请求,于是将客户端的起始序列号100存起来,并且随机生成一个服务端的起始序列号(比如是300)。然后给客户端回复一段报文,回复报文包含SYN和ACK标志(也就是SYN=1,ACK=1)、序列号seq=300、确认号ack=101(客户端发过来的序列号+1)。 97 | 98 | 第三次握手:客户端收到服务端的回复后发现ACK=1并且ack=101,于是知道服务端已经收到了序列号为100的那段报文;同时发现SYN=1,知道了服务端同意了这次连接,于是就将服务端的序列号300给存下来。然后客户端再回复一段报文给服务端,报文包含ACK标志位(ACK=1)、ack=301(服务端序列号+1)、seq=101(第一次握手时发送报文是占据一个序列号的,所以这次seq就从101开始,需要注意的是不携带数据的ACK报文是不占据序列号的,所以后面第一次正式发送数据时seq还是101)。当服务端收到报文后发现ACK=1并且ack=301,就知道客户端收到序列号为300的报文了,就这样客户端和服务端通过TCP建立了连接。 99 | 100 | ### 四次挥手(目的是关闭一个连接) 101 | 102 | ![Image text](https://mubu.com/document_image/6cfaec04-0481-479c-b9ce-98a6d50fd9e1-4943374.jpg) 103 | 104 | 比如客户端初始化的序列号ISA=100,服务端初始化的序列号ISA=300。TCP连接成功后客户端总共发送了1000个字节的数据,服务端在客户端发FIN报文前总共回复了2000个字节的数据。 105 | 第一次挥手:当客户端的数据都传输完成后,客户端向服务端发出连接释放报文(当然数据没发完时也可以发送连接释放报文并停止发送数据),释放连接报文包含FIN标志位(FIN=1)、序列号seq=1101(100+1+1000,其中的1是建立连接时占的一个序列号)。需要注意的是客户端发出FIN报文段后只是不能发数据了,但是还可以正常收数据;另外FIN报文段即使不携带数据也要占据一个序列号。 106 | 107 | 第二次挥手:服务端收到客户端发的FIN报文后给客户端回复确认报文,确认报文包含ACK标志位(ACK=1)、确认号ack=1102(客户端FIN报文序列号1101+1)、序列号seq=2300(300+2000)。此时服务端处于关闭等待状态,而不是立马给客户端发FIN报文,这个状态还要持续一段时间,因为服务端可能还有数据没发完。 108 | 109 | 第三次挥手:服务端将最后数据(比如50个字节)发送完毕后就向客户端发出连接释放报文,报文包含FIN和ACK标志位(FIN=1,ACK=1)、确认号和第二次挥手一样ack=1102、序列号seq=2350(2300+50)。 110 | 111 | 第四次挥手:客户端收到服务端发的FIN报文后,向服务端发出确认报文,确认报文包含ACK标志位(ACK=1)、确认号ack=2351、序列号seq=1102。注意客户端发出确认报文后不是立马释放TCP连接,而是要经过2MSL(最长报文段寿命的2倍时长)后才释放TCP连接。而服务端一旦收到客户端发出的确认报文就会立马释放TCP连接,所以服务端结束TCP连接的时间要比客户端早一些。 112 | 113 | ## TCP报文头结构? 114 | 115 | TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务端保存的一份关于对方的信息,如ip地址、端口号等。 116 | 117 | TCP可以看成是一种字节流,它会处理IP层或以下的层的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在TCP头部。 118 | 119 | 一个TCP连接由一个4元组构成,分别是两个IP地址和两个端口号。一个TCP连接通常分为三个阶段:连接、数据传输、退出(关闭)。通过三次握手建立一个链接,通过四次挥手来关闭一个连接。 120 | 当一个连接被建立或被终止时,交换的报文段只包含TCP头部,而没有数据。 121 | 122 | TCP报文头结构如下: 123 | 124 | ![Image text](https://mubu.com/document_image/3f5f9177-d5cd-45aa-bd5b-4a380b80df0b-4943374.jpg) 125 | 126 | 上图中有几个字段需要重点介绍下: 127 | (1)序号:seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。 128 | 129 | (2)确认序号:ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,ack=seq+1。 130 | 131 | (3)标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如下: 132 | 133 | + ACK:确认序号有效。 134 | + FIN:释放一个连接。 135 | + PSH:接收方应该尽快将这个报文交给应用层。 136 | + RST:重置连接。 137 | + SYN:发起一个新连接。 138 | + URG:紧急指针(urgent pointer)有效。 139 | 需要注意的是: 140 | 不要将确认序号ack与标志位中的ACK搞混了。 141 | 确认方ack=发起方seq+1,两端配对。 142 | 143 | ## 4、为什么TCP连接的时候是3次?2次不可以吗? 144 | 145 | 分两步说,第一步是序列号的确认,第二部是防止失效的报文段重新传到服务端导致服务端资源的浪费。 146 | 147 | 首先,为了实现可靠数据传输, TCP 协议的通信双方, 都必须维护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤 148 | 149 | 如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认 150 | 151 | 如果是三次握手,即便发生丢包也不会有问题,比如如果第三次握手客户端发的确认ack报文丢失,服务端在一段时间内没有收到确认ack报文的话就会重新进行第二次握手,也就是服务端会重发SYN报文段,客户端收到重发的报文段后会再次给服务端发送确认ack报文。 152 | 153 | 除此之外,假定A向B发送一个连接请求,由于一些原因,导致A发出的连接请求在一个网络节点逗留了比较多的时间。此时A会将此连接请求作为无效处理 又重新向B发起了一次新的连接请求,B正常收到此连接请求后建立了连接,数据传输完成后释放了连接。如果此时A发出的第一次请求又到达了B,B会以为A又发起了一次连接请求,如果是两次握手:此时连接就建立了,B会一直等待A发送数据,从而白白浪费B的资源。 如果是三次握手:由于A没有发起连接请求,也就不会理会B的连接响应,B没有收到A的确认连接,就会关闭掉本次连接。 154 | 155 | ## 5、为什么TCP连接的时候是三次,关闭的时候却是四次? 156 | 157 | 因为只有在客户端和服务端都没有数据要发送的时候才能断开TCP。而客户端发出FIN报文时只能保证客户端没有数据发了,服务端还有没有数据发客户端是不知道的。而服务端收到客户端的FIN报文后只能先回复客户端一个确认报文来告诉客户端我服务端已经收到你的FIN报文了,但我服务端还有一些数据没发完,等这些数据发完了服务端才能给客户端发FIN报文(所以不能一次性将确认报文和FIN报文发给客户端,就是这里多出来了一次)。 158 | 159 | ## 6、为什么客户端发出第四次握手的确认报文后要等待2msl才能关闭连接? 160 | 161 | 这里同样是要考虑丢包的问题,如果第四次挥手的报文丢失,服务端没收到确认ack报文就会重发第三次挥手的报文,这样报文一去一回最长时间就是2MSL,所以需要等这么长时间来确认服务端确实已经收到了。 162 | 163 | 同时,因为延迟关闭是一个非常长的过程,我们要尽量由客户端来关闭连接,否则会导致服务端的压力过大。 164 | 165 | ## 7、如果已经建立了连接,但是客户端突然出现故障了怎么办? 166 | 167 | TCP设有一个保活计时器,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。 168 | 169 | 而TCP一共有四种计时器,分别是: 170 | 171 | + 重传计时器:Retransmission Timer 172 | 173 | 重传定时器:为了控制丢失的报文段或丢弃的报文段,也就是对报文段确认的等待时间。当TCP发送报文段时,就创建这个特定报文段的重传计时器,可能发生两种情况:若在计时器超时之前收到对报文段的确认,则撤销计时器;若在收到对特定报文段的确认之前计时器超时,则重传该报文,并把计时器复位。 174 | 175 | + 坚持计时器:Persistent Timer 176 | 专门为对付零窗口通知而设立的。 177 | 178 | 当发送端收到零窗口的确认时,就启动坚持计时器,当坚持计时器截止期到时,发送端TCP就发送一个特殊的报文段,叫探测报文段,这个报文段只有一个字节的数据。探测报文段有序号,但序号永远不需要确认,甚至在计算对其他部分数据的确认时这个序号也被忽略。探测报文段提醒接收端TCP,确认已丢失,必须重传。 179 | 180 | 坚持计时器的截止期设置为重传时间的值,但若没有收到从接收端来的响应,则发送另一个探测报文段,并将坚持计时器的值加倍和并复位,发送端继续发送探测报文段,将坚持计时器的值加倍和复位,知道这个值增大到阈值为止(通常为60秒)。之后,发送端每隔60s就发送一个报文段,直到窗口重新打开为止。 181 | 182 | 183 | + 保活计时器:Keeplive Timer 184 | 185 | 每当服务器收到客户的信息,就将keeplive timer复位,超时通常设置2小时,若服务器超过2小时还没有收到来自客户的信息,就发送探测报文段,若发送了10个探测报文段(没75秒发送一个)还没收到响应,则终止连接。 186 | 187 | 188 | + 时间等待计时器:Time_Wait Timer。 189 | 190 | 在连接终止期使用,当TCP关闭连接时,并不认为这个连接就真正关闭了,在时间等待期间,连接还处于一种中间过度状态。这样就可以时重复的fin报文段在到达终点后被丢弃,这个计时器的值通常设置为一格报文段寿命期望值的两倍。 191 | 192 | ## 8、HTTP和HTTPS的区别? 193 | 194 | ![Image text](https://mubu.com/document_image/91ba9d1d-18ba-4d17-8aaa-e7e12e30cb89-4943374.jpg) 195 | 196 | ## 9、常用HTTP状态码 197 | 198 | HTTP状态码表示客户端HTTP请求的返回结果、标识服务器处理是否正常、表明请求出现的错误等. 199 | 200 | 状态码的类别: 201 | 202 | 类别 原因短语 203 | 204 | + 1XX Informational(信息性状态码) 接受的请求正在处理 205 | + 2XX Success(成功状态码) 请求正常处理完毕 206 | + 3XX Redirection(重定向状态码) 需要进行附加操作以完成请求 207 | + 4XX Client Error(客户端错误状态码) 服务器无法处理请求 208 | + 5XX Server Error(服务器错误状态码) 服务器处理请求出错 209 | 210 | 常用HTTP状态码 211 | 212 | 2XX 成功(这系列表明请求被正常处理了) 213 | 214 | 200 OK,表示从客户端发来的请求在服务器端被正确处理 215 | 216 | 204 No content,表示请求成功,但响应报文不含实体的主体部分 217 | 218 | 206 Partial Content,进行范围请求成功 219 | 220 | 3XX 重定向(表明浏览器要执行特殊处理) 221 | 222 | 301 moved permanently,永久性重定向,表示资源已被分配了新的 URL 223 | 224 | 302 found,临时性重定向,表示资源临时被分配了新的 URL 225 | 226 | 303 see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源(对于301/302/303响应,几乎所有浏览器都会删除报文主体并自动用GET重新请求) 227 | 228 | 304 not modified,表示服务器允许访问资源,但请求未满足条件的情况(与重定向无关) 229 | 230 | 307 temporary redirect,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求 231 | 232 | 4XX 客户端错误 233 | 234 | 400 bad request,请求报文存在语法错误 235 | 236 | 401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息 237 | 238 | 403 forbidden,表示对请求资源的访问被服务器拒绝,可在实体主体部分返回原因描述 239 | 240 | 404 not found,表示在服务器上没有找到请求的资源 241 | 242 | 5XX 服务器错误 243 | 244 | 500 internal sever error,表示服务器端在执行请求时发生了错误 245 | 246 | 501 Not Implemented,表示服务器不支持当前请求所需要的某个功能 247 | 248 | 503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求 249 | 250 | 10、get和post区别? 251 | 252 | 说道GET和POST,就不得不提HTTP协议,因为浏览器和服务器的交互是通过HTTP协议执行的,而GET和POST也是HTTP协议中的两种方法。 253 | HTTP全称为Hyper Text Transfer Protocol,中文翻译为超文本传输协议,目的是保证浏览器与服务器之间的通信。HTTP的工作方式是客户端与服务器之间的请求-应答协议。 254 | HTTP协议中定义了浏览器和服务器进行交互的不同方法,基本方法有4种,分别是GET,POST,PUT,DELETE。这四种方法可以理解为,对服务器资源的查,改,增,删。 255 | 256 | + GET:从服务器上获取数据,也就是所谓的查,仅仅是获取服务器资源,不进行修改。 257 | + POST:向服务器提交数据,这就涉及到了数据的更新,也就是更改服务器的数据。 258 | + PUT:英文含义是放置,也就是向服务器新添加数据,就是所谓的增。 259 | + DELETE:从字面意思也能看出,这种方式就是删除服务器数据的过程。 260 | 261 | GET和POST区别 262 | 263 | Get是不安全的,因为在传输过程,数据被放在请求的URL中;Post的所有操作对用户来说都是不可见的。 但是这种做法也不时绝对的,大部分人的做法也是按照上面的说法来的,但是也可以在get请求加上 request body,给 post请求带上 URL 参数。 264 | 265 | Get请求提交的url中的数据最多只能是2048字节,这个限制是浏览器或者服务器给添加的,http协议并没有对url长度进行限制,目的是为了保证服务器和浏览器能够正常运行,防止有人恶意发送请求。Post请求则没有大小限制。 266 | 267 | Get限制Form表单的数据集的值必须为ASCII字符;而Post支持整个ISO10646字符集。 268 | 269 | Get执行效率却比Post方法好。Get是form提交的默认方法。 270 | 271 | GET产生一个TCP数据包;POST产生两个TCP数据包。 272 | 273 | 对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据); 274 | 275 | 而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。 276 | 277 | ## 11、什么是对称加密和非对称加密? 278 | 279 | 对称密钥加密是指加密和解密使用同一个密钥的方式,这种方式存在的最大问题就是密钥发送问题,即如何安全地将密钥发给对方; 280 | 281 | 而非对称加密是指使用一对非对称密钥,即公钥和私钥,公钥可以随意发布,但私钥只有自己知道。发送密文的一方使用对方的公钥进行加密处理,对方接收到加密信息后,使用自己的私钥进行解密。 282 | 283 | 由于非对称加密的方式不需要发送用来解密的私钥,所以可以保证安全性;但是和对称加密比起来,非常的慢 284 | 285 | ## 12、Session和Cookie的区别? 286 | 287 | HTTP协议本身是无状态的。什么是无状态呢,即服务器无法判断用户身份。 288 | 289 | ### 什么是cookie 290 | 291 | cookie是由Web服务器保存在用户浏览器上的小文件(key-value格式),包含用户相关的信息。客户端向服务器发起请求,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户身份。 292 | 293 | ### 什么是session 294 | 295 | session是依赖Cookie实现的。session是服务器端对象 296 | 297 | session 是浏览器和服务器会话过程中,服务器分配的一块储存空间。服务器默认为浏览器在cookie中设置 sessionid,浏览器在向服务器请求过程中传输 cookie 包含 sessionid ,服务器根据 sessionid 获取出会话中存储的信息,然后确定会话的身份信息。 298 | 299 | ### cookie与session区别 300 | 301 | 存储位置与安全性:cookie数据存放在客户端上,安全性较差,session数据放在服务器上,安全性相对更高; 302 | 303 | 304 | 存储空间:单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie,session无此限制 305 | 306 | 占用服务器资源:session一定时间内保存在服务器上,当访问增多,占用服务器性能,考虑到服务器性能方面,应当使用cookie。 307 | 308 | ## 13、如何禁用cookie的情况下使用session? 309 | 310 | Cookie 与 Session,一般认为是两个独立的东西,Session采用的是在服务器端保持状态的方案,而Cookie采用的是在客户端保持状态的方案。 311 | 312 | 但为什么禁用Cookie就不能得到Session呢?因为Session是用Session ID来确定当前对话所对应的服务器Session,而Session ID是通过Cookie来传递的,禁用Cookie相当于失去了Session ID,也就得不到Session了。 313 | 314 | 假定用户关闭Cookie的情况下使用Session,其实现途径有以下几种: 315 | 316 | 手动通过URL传值、隐藏表单传递Session ID。 317 | 318 | 用文件、数据库等形式保存Session ID,在跨页过程中手动调用。 319 | 320 | ## 14、HTTP1.0,1.1, 2.0的区别 321 | 322 | ### http1.0: 323 | 324 | 最早在1996年在网页中使用,内容简单,所以浏览器的每次请求都需要与服务器建立一个TCP连接,服务器处理完成后立即断开TCP连接(无连接),服务器不跟踪每个客户端也不记录过去的请求(无状态)。 325 | 326 | ### HTTP1.1: 327 | 328 | 到1999年广泛在各大浏览器网络请求中使用,HTTP/1.0中默认使用Connection: close。在HTTP/1.1中已经默认使用Connection: keep-alive(长连接),避免了连接建立和释放的开销,但服务器必须按照客户端请求的先后顺序依次回送相应的结果,以保证客户端能够区分出每次请求的响应内容。通过Content-Length字段来判断当前请求的数据是否已经全部接收。不允许同时存在两个并行的响应。 329 | 330 | ### HTTP2.0: 331 | 332 | HTTP/2引入二进制数据帧和流的概念,其中帧对数据进行顺序标识,(流(stream):已建立连接上的双向字节流;消息:与逻辑消息对应的完整的一系列数据帧;帧:HTTP2.0通信的最小单位,每个帧包含帧头部,至少也会标识出当前帧所属的流(stream id))这样浏览器收到数据之后,就可以按照序列对数据进行合并,而不会出现合并后数据错乱的情况。同样是因为有了序列,服务器就可以并行的传输数据,这就是流所做的事情。这些数据帧都在一个tcp连接(可以承载任意数量的双向数据流)上并行发送。 333 | 334 | http1.0和http1.1的主要区别如下: 335 | 336 | 1、缓存处理:1.1添加更多的缓存控制策略(如:Entity tag,If-Match) 337 | 338 | 2、网络连接的优化:1.1支持断点续传 339 | 340 | 3、错误状态码的增多:1.1新增了24个错误状态响应码,丰富的错误码更加明确各个状态 341 | 342 | 4、Host头处理:支持Host头域,不在以IP为请求方标志 343 | 344 | 5、长连接:减少了建立和关闭连接的消耗和延迟。 345 | 346 | http1.1和http2.0的主要区别: 347 | 348 | 1、新的传输格式:2.0使用二进制格式,1.0依然使用基于文本格式 349 | 350 | 2、多路复用:连接共享,不同的request可以使用同一个连接传输(最后根据每个request上的id号组合成正常的请求) 351 | 352 | 3、header压缩:由于1.X中header带有大量的信息,并且得重复传输,2.0使用encoder来减少需要传输的hearder大小 353 | 354 | 4、服务端推送:同google的SPDUY(1.0的一种升级)一样 355 | 356 | ## 15、TCP粘包现象原因和解决方法? 357 | 358 | ### 粘包出现原因 359 | 360 | 简单得说,在流传输中出现,UDP不会出现粘包,因为它有消息边界(参考Windows网络编程) 361 | 362 | 1.发送端需要等缓冲区满才发送出去,造成粘包 363 | 364 | 2.接收方不及时接收缓冲区的包,造成多个包接收 365 | 366 | 具体点: 367 | 368 | 369 | (1)发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。 370 | 371 | (2)接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。 372 | 粘包情况有两种,一种是粘在一起的包都是完整的数据包,另一种情况是粘在一起的包有不完整的包。 373 | 374 | 不是所有的粘包现象都需要处理,若传输的数据为不带结构的连续流数据(如文件传输),则不必把粘连的包分开(简称分包)。但在实际工程应用中,传输的数据一般为带结构的数据,这时就需要做分包处理。 375 | 在处理定长结构数据的粘包问题时,分包算法比较简单;在处理不定长结构数据的粘包问题时,分包算法就比较复杂。特别是粘在一起的包有不完整的包的粘包情况,由于一包数据内容被分在了两个连续的接收包中,处理起来难度较大。实际工程应用中应尽量避免出现粘包现象。 376 | 377 | 为了避免粘包采取的措施: 378 | 379 | (1)对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满; 380 | 381 | (2)对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象; 382 | 383 | (3)由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。 384 | 385 | 以上提到的三种措施,都有其不足之处。 386 | 387 | (1)第一种编程设置方法虽然可以避免发送方引起的粘包,但它关闭了优化算法,降低了网络发送效率,影响应用程序的性能,一般不建议使用。 388 | 389 | (2)第二种方法只能减少出现粘包的可能性,但并不能完全避免粘包,当发送频率较高时,或由于网络突发可能使某个时间段数据包到达接收方较快,接收方还是有可能来不及接收,从而导致粘包。 390 | 391 | (3)第三种方法虽然避免了粘包,但应用程序的效率较低,对实时应用的场合不适合。 392 | 393 | 一种比较周全的对策是:接收方创建一预处理线程,对接收到的数据包进行预处理,将粘连的包分开。对这种方法我们进行了实验,证明是高效可行的。 394 | 395 | ## 16、输入url地址按下回车发生了什么? 396 | 397 | ![Image text](https://mubu.com/document_image/f8d3d291-026a-4cbc-832d-9b34b0380abc-4943374.jpg) 398 | 399 | 1.输入url地址后,首先进行DNS解析,将相应的域名解析为IP地址; 400 | 401 | 2.客户端根据IP地址去寻找相应的服务器; 402 | 403 | 404 | 3.与服务器进行TCP的三次握手; 405 | 406 | 4.客户端找到相应的资源库; 407 | 408 | 5.根据资源库返回页面信息; 409 | 410 | 6.浏览器根据自身的执行机制解析页面; 411 | 412 | 浏览器解析页面时,会找到每一个文件夹(css、js、html、img......),每一个文件夹下的资源会重新走到第二步,去找到相应的服务器,然后一步步执行。 413 | 414 | 7.最后服务器将解析信息返回给客户端,进行TCP的四次挥手。 415 | 416 | 8.至此,客户端显示自己请求,即服务端返回的东西 417 | 418 | ## 16、DNS的过程? 419 | 420 | DNS( Domain Name System)是“域名系统”的英文缩写,是一种组织成域层次结构的计算机和网络服务命名系统,它用于TCP/IP网络,它所提供的服务是用来将主机名和域名转换为IP地址的工作。DNS就是这样的一位“翻译官” 421 | 422 | 423 | 1) 浏览器缓存  当用户通过浏览器访问某域名时,浏览器首先会在自己的缓存中查找是否有该域名对应的IP地址(若曾经访问过该域名且没有清空缓存便存在);  424 | 425 | 2) 系统缓存  当浏览器缓存中无域名对应IP则会自动检查用户计算机系统Hosts文件DNS缓存是否有该域名对应IP;   426 | 427 | 3) 路由器缓存  当浏览器及系统缓存中均无域名对应IP则进入路由器缓存中检查,以上三步均为客服端的DNS缓存;   428 | 429 | 4) ISP(互联网服务提供商)DNS缓存  当在用户客服端查找不到域名对应IP地址,则将进入ISP DNS缓存中进行查询。比如你用的是电信的网络,则会进入电信的DNS缓存服务器中进行查找;   430 | 431 | 5) 根域名服务器  当以上均未完成,则进入根服务器进行查询。全球仅有13台根域名服务器,1个主根域名服务器,其余12为辅根域名服务器。根域名收到请求后会查看区域文件记录,若无则将其管辖范围内顶级域名(如.com)服务器IP告诉本地DNS服务器;   432 | 433 | 6) 顶级域名服务器  顶级域名服务器收到请求后查看区域文件记录,若无则将其管辖范围内主域名服务器的IP地址告诉本地DNS服务器;   434 | 435 | 7) 主域名服务器  主域名服务器接受到请求后查询自己的缓存,如果没有则进入下一级域名服务器进行查找,并重复该步骤直至找到正确纪录;   436 | 437 | 8)保存结果至缓存  本地域名服务器把返回的结果保存到缓存,以备下一次使用,同时将该结果反馈给客户端,客户端通过这个IP地址与web服务器建立链接。 438 | 439 | ## 17、TCP协议如何保证可靠传输? 440 | 441 | 应用数据被分割成 TCP 认为最适合发送的数据块。 442 | 443 | TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。 444 | 445 | 校验和: TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。 446 | 447 | 重复丢弃:TCP 的接收端会丢弃重复的数据。 448 | 449 | 流量控制: TCP 连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。 (TCP 利用滑动窗口实现流量控制) 450 | 451 | 拥塞控制: 当网络拥塞时,减少数据的发送。 452 | 453 | ARQ协议: 也是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。 454 | 455 | 超时重传: 当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。 456 | 457 | ## 18、什么是ARQ协议? 458 | 459 | 自动重传请求(Automatic Repeat-reQuest,ARQ)是OSI模型中数据链路层和传输层的错误纠正协议之一。它通过使用确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。如果发送方在发送后一段时间之内没有收到确认帧,它通常会重新发送。ARQ包括停止等待ARQ协议和连续ARQ协议。 460 | 461 | ### 停止等待ARQ协议 462 | 463 | 停止等待协议是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认(回复ACK)。如果过了一段时间(超时时间后),还是没有收到 ACK 确认,说明没有发送成功,需要重新发送,直到收到确认后再发下一个分组; 464 | 在停止等待协议中,若接收方收到重复分组,就丢弃该分组,但同时还要发送确认; 465 | 466 | 优点: 简单 467 | 468 | 缺点: 信道利用率低,等待时间长 469 | 470 | 1) 无差错情况: 471 | 发送方发送分组,接收方在规定时间内收到,并且回复确认.发送方再次发送。 472 | 473 | 2) 出现差错情况(超时重传): 474 | 停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认,就重传前面发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完一个分组需要设置一个超时计时器,其重传时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为 自动重传请求 ARQ 。另外在停止等待协议中若收到重复分组,就丢弃该分组,但同时还要发送确认。连续 ARQ 协议 可提高信道利用率。发送维持一个发送窗口,凡位于发送窗口内的分组可连续发送出去,而不需要等待对方确认。接收方一般采用累积确认,对按序到达的最后一个分组发送确认,表明到这个分组位置的所有分组都已经正确收到了。 475 | 476 | 3) 确认丢失和确认迟到 477 | 478 | 确认丢失 :确认消息在传输过程丢失。当A发送M1消息,B收到后,B向A发送了一个M1确认消息,但却在传输过程中丢失。而A并不知道,在超时计时过后,A重传M1消息,B再次收到该消息后采取以下两点措施:1. 丢弃这个重复的M1消息,不向上层交付。 2. 向A发送确认消息。(不会认为已经发送过了,就不再发送。A能重传,就证明B的确认消息丢失)。 479 | 480 | 确认迟到 :确认消息在传输过程中迟到。A发送M1消息,B收到并发送确认。在超时时间内没有收到确认消息,A重传M1消息,B仍然收到并继续发送确认消息(B收到了2份M1)。此时A收到了B第二次发送的确认消息。接着发送其他数据。过了一会,A收到了B第一次发送的对M1的确认消息(A也收到了2份确认消息)。处理如下:1. A收到重复的确认后,直接丢弃。2. B收到重复的M1后,也直接丢弃重复的M1。 481 | 482 | ### 连续ARQ协议 483 | 484 | 连续 ARQ 协议可提高信道利用率。发送方维持一个发送窗口,凡位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。接收方一般采用累计确认,对按序到达的最后一个分组发送确认,表明到这个分组为止的所有分组都已经正确收到了。 485 | 486 | 优点: 信道利用率高,容易实现,即使确认丢失,也不必重传。 487 | 488 | 缺点: 不能向发送方反映出接收方已经正确收到的所有分组的信息。 比如:发送方发送了 5条 消息,中间第三条丢失(3号),这时接收方只能对前两个发送确认。发送方无法知道后三个分组的下落,而只好把后三个全部重传一次。这也叫 Go-Back-N(回退 N),表示需要退回来重传已经发送过的 N 个消息。 489 | 490 | ## 19、说说http报文头? 491 | 492 | ![Image text](https://mubu.com/document_image/66f61caf-51dc-4959-b15b-f16a1ce28b98-4943374.jpg) 493 | 494 | 一个HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据4个部分组成,下图给出了请求报文的一般格式。 495 | 496 | 1.请求头 497 | 498 | 请求行由请求方法字段、URL字段和HTTP协议版本字段3个字段组成,它们用空格分隔。例如,GET /index.html HTTP/1.1 499 | 500 | HTTP协议的请求方法有GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT。 501 | 502 | 2.请求头部 503 | 504 | 请求头部由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔。请求头部通知服务器有关于客户端请求的信息,典型的请求头有: 505 | User-Agent:产生请求的浏览器类型。 506 | Accept:客户端可识别的内容类型列表。 507 | Host:请求的主机名,允许多个域名同处一个IP地址,即虚拟主机。 508 | Connection: 是否处于连接状态,1.1以后为keep alive,1.0为close 509 | 510 | 3.空行 511 | 512 | 最后一个请求头之后是一个空行,发送回车符和换行符,通知服务器以下不再有请求头。 513 | 514 | 4.请求数据 515 | 516 | 请求数据不在GET方法中使用,而是在POST方法中使用。POST方法适用于需要客户填写表单的场合。与请求数据相关的最常使用的请求头是Content-Type和Content-Length。 517 | 518 | 519 | ## 20、GET和POST的区别? 520 | 521 | 1.GET提交,请求的数据会附在URL之后(就是把数据放置在HTTP协议头<request-line>中),以?分割URL和传输数据,多个参数用&连接;例如:login.action?name=hyddd&password=idontknow&verify=%E4%BD%A0 %E5%A5%BD。如果数据是英文字母/数字,原样发送,如果是空格,转换为+,如果是中文/其他字符,则直接把字符串用BASE64加密,得出如: %E4%BD%A0%E5%A5%BD,其中%XX中的XX为该符号以16进制表示的ASCII。 522 | POST提交:把提交的数据放置在是HTTP包的包体<request-body>中。上文示例中红色字体标明的就是实际的传输数据 523 | 524 | 因此,GET提交的数据会在地址栏中显示出来,而POST提交,地址栏不会改变 525 | 526 | ### 2.传输数据的大小: 527 | 528 | 首先声明,HTTP协议没有对传输的数据大小进行限制,HTTP协议规范也没有对URL长度进行限制。 而在实际开发中存在的限制主要有: 529 | 530 | GET:特定浏览器和服务器对URL长度有限制,例如IE对URL长度的限制是2083字节(2K+35)。对于其他浏览器,如Netscape、FireFox等,理论上没有长度限制,其限制取决于操作系统的支持。 531 | 因此对于GET提交时,传输数据就会受到URL长度的限制。 532 | 533 | POST:由于不是通过URL传值,理论上数据不受限。但实际各个WEB服务器会规定对post提交数据大小进行限制,Apache、IIS6都有各自的配置。 534 | 535 | ### 3.安全性: 536 | 537 | POST的安全性要比GET的安全性高。注意:这里所说的安全性和上面GET提到的“安全”不是同个概念。上面“安全”的含义仅仅是不作数据修改,而这里安全的含义是真正的Security的含义,比如:通过GET提交数据,用户名和密码将明文出现在URL上,因为(1)登录页面有可能被浏览器缓存, (2)其他人查看浏览器的历史纪录,那么别人就可以拿到你的账号和密码了。 538 | 539 | ### 4、幂等性 540 | 541 | get幂等,post不幂等。 542 | 543 | ## 21、重定向和转发的区别? 544 | 545 | ### 一、目标不同 546 | 547 | 转发是服务器行为,重定向是客户端行为。 548 | 549 | ### 二、请求次数不同 550 | 551 | 1、重定向是两次request 552 | 553 | 第一次,客户端request一个网址,服务器响应,并response回来,告诉浏览器,你应该去别一个网址 554 | 555 | 556 | 2、请求转发只有一次请求 557 | 558 | ### 三、网址定位不同 559 | 560 | 重定向的网址可以是任何网址,请求转发只能是指定网址 561 | 562 | ### 四、导致的结果不同 563 | 564 | 不做重定向,则用户收藏夹或搜索引擎数据库中旧地址只能让访问客户得到一个404页面错误信息,访问流量白白丧失;再者某些注册了多个域名的网站,也需要通过重定向让访问这些域名的用户自动跳转到主站点等。 565 | 566 | ## 21、ARP协议是什么? 567 | 568 | ARP协议是“Address Resolution Protocol”(地址解析协议)的缩写。其作用是在以太网环境中,数据的传输所依懒的是MAC地址而非IP地址,而将已知IP地址转换为MAC地址的工作是由ARP协议来完成的。 569 | 570 | 在局域网中,网络中实际传输的是“帧”,帧里面是有目标主机的MAC地址的。在以太网中,一个主机和另一个主机进行直接通信,必须要知道目标主机的MAC地址。但这个目标MAC地址是如何获得的呢?它就是通过地址解析协议获得的。所谓“地址解析”就是主机在发送帧前将目标IP地址转换成目标MAC地址的过程。ARP协议的基本功能就是通过目标设备的IP地址,查询目标设备的MAC地址,以保证通信的顺利进行。 571 | 572 | -------------------------------------------------------------------------------- /Collections/Exceptiom.md: -------------------------------------------------------------------------------- 1 | # Java异常 2 | 3 | ## 1.Error 和 Exception 区别是什么? 4 | 5 | - Error 类型的错误通常为虚拟机相关错误,如系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,JAVA 应用程序也不应对这类错误进行捕获,一旦这类错误发生,通常应用程序会被终止,仅靠应用程序本身无法恢复; 6 | - Exception 类的错误是可以在应用程序中进行捕获并处理的,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行。 7 | 8 | ## 2. 运行时异常和一般异常(受检异常)区别是什么? 9 | 10 | - 运行时异常包括 RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。 Java 编译器不会检查运行时异常。 11 | - 受检异常是Exception 中除 RuntimeException 及其子类之外的异常。 Java 编译器会检查受检异常。 12 | - RuntimeException异常和受检异常之间的区别:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检异常,否则就选择非受检异常(RuntimeException)。一般来讲,如果没有特殊的要求,我们建议使用RuntimeException异常。 13 | 14 | ## 3. JVM 是如何处理异常的? 15 | 16 | - 在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。 17 | - JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。 18 | 19 | ## 4. throw 和 throws 的区别是什么? 20 | 21 | - Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出异常,可以通过 throws 关键字在方法上声明该方法要拋出的异常,或者在方法内部通过 throw 拋出异常对象。 22 | - throws 关键字和 throw 关键字在使用上的几点区别如下: 23 | - throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。 24 | - throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常。 25 | 26 | ## 5. final、finally、finalize 有什么区别? 27 | 28 | - final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。 29 | - finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。 30 | - finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,Java 中允许使用 finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。 31 | 32 | ## 6. NoClassDefFoundError 和 ClassNotFoundException 区别? 33 | 34 | - NoClassDefFoundError 是一个 Error 类型的异常,是由 JVM 引起的,不应该尝试捕获这个异常。 35 | - 引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导致; 36 | - ClassNotFoundException 是一个受查异常,需要显式地使用 try-catch 对其进行捕获和处理,或在方法签名中用 throws 关键字进行声明。当使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。 37 | 38 | ## 7. try-catch-finally 中哪个部分可以省略? 39 | 40 | - 答:catch 可以省略 41 | - 更为严格的说法其实是:try只适合处理运行时异常,try+catch适合处理运行时异常+普通异常。也就是说,如果你只用try去处理普通异常却不加以catch处理,编译是通不过的,因为编译器硬性规定,普通异常如果选择捕获,则必须用catch显示声明以便进一步处理。而运行时异常在编译时没有如此规定,所以catch可以省略,你加上catch编译器也觉得无可厚非。 42 | - 理论上,编译器看任何代码都不顺眼,都觉得可能有潜在的问题,所以你即使对所有代码加上try,代码在运行期时也只不过是在正常运行的基础上加一层皮。但是你一旦对一段代码加上try,就等于显示地承诺编译器,对这段代码可能抛出的异常进行捕获而非向上抛出处理。如果是普通异常,编译器要求必须用catch捕获以便进一步处理;如果运行时异常,捕获然后丢弃并且+finally扫尾处理,或者加上catch捕获以便进一步处理。 43 | - 至于加上finally,则是在不管有没捕获异常,都要进行的“扫尾”处理。 44 | 45 | ## 8. try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗? 46 | 47 | - 答:会执行,在 return 前执行。 48 | 49 | - 注意:在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try中的 return 语句不会立马返回调用者,而是记录下返回值待 finally 代码块执行完毕之后再向调用者返回其值,然后如果在 finally 中修改了返回值,就会返回修改后的值。显然,在 finally 中返回或者修改返回值会对程序造成很大的困扰,C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java 中也可以通过提升编译器的语法检查级别来产生警告或错误。 50 | 51 | - 代码示例1: 52 | 53 | ```java 54 | public static int getInt() { 55 | int a = 10; 56 | try { 57 | System.out.println(a / 0); 58 | a = 20; 59 | } catch (ArithmeticException e) { 60 | a = 30; 61 | return a; 62 | /* 63 | return a 在程序执行到这一步的时候,这里不是return a 而是 return 30;这个返回路径就形成了 64 | 但是呢,它发现后面还有finally,所以继续执行finally的内容,a=40 65 | 再次回到以前的路径,继续走return 30,形成返回路径之后,这里的a就不是a变量了,而是常量30 66 | */ 67 | } finally { 68 | a = 40; 69 | } 70 | return a; 71 | } 72 | ``` 73 | 74 | 75 | 76 | - 执行结果:30 77 | 78 | - 代码示例2: 79 | 80 | ```java 81 | public static int getInt() { 82 | int a = 10; 83 | try { 84 | System.out.println(a / 0); 85 | a = 20; 86 | } catch (ArithmeticException e) { 87 | a = 30; 88 | return a; 89 | } finally { 90 | a = 40; 91 | //如果这样,就又重新形成了一条返回路径,由于只能通过1个return返回,所以这里直接返回40 92 | return a; 93 | } 94 | } 95 | ``` 96 | 97 | 98 | 99 | - 执行结果:40 100 | 101 | ## 9. 类 ExampleA 继承 Exception,类 ExampleB 继承ExampleA。 102 | 103 | - 有如下代码片断: 104 | 105 | ```java 106 | try { 107 | throw new ExampleB("b") 108 | } catch(ExampleA e){ 109 | System.out.println("ExampleA"); 110 | } catch(Exception e){ 111 | System.out.println("Exception"); 112 | } 113 | ``` 114 | 115 | 116 | 117 | - 请问执行此段代码的输出是什么? 118 | 119 | - 答:输出:ExampleA。(根据里氏代换原则[能使用父类型的地方一定能使用子类型],抓取 ExampleA 类型异常的 catch 块能够抓住 try 块中抛出的 ExampleB 类型的异常) 120 | 121 | ## 10.说出下面代码的运行结果。(此题的出处是《Java 编程思想》一书) 122 | 123 | - 代码如下 124 | 125 | ```java 126 | class Annoyance extends Exception { 127 | } 128 | class Sneeze extends Annoyance { 129 | } 130 | class Human { 131 | public static void main(String[] args) 132 | throws Exception { 133 | try { 134 | try { 135 | throw new Sneeze(); 136 | } catch ( Annoyance a ) { 137 | System.out.println("Caught Annoyance"); 138 | throw a; 139 | } 140 | } catch ( Sneeze s ) { 141 | System.out.println("Caught Sneeze"); 142 | return ; 143 | } finally { 144 | System.out.println("Hello World!"); 145 | } 146 | } 147 | } 148 | 149 | 结果 150 | Caught Annoyance 151 | Caught Sneeze 152 | Hello World! 153 | ``` 154 | 155 | 156 | 157 | ## 11. 常见的 RuntimeException 有哪些? 158 | 159 | - ClassCastException(类转换异常) 160 | - IndexOutOfBoundsException(数组越界) 161 | - NullPointerException(空指针) 162 | - ArrayStoreException(数据存储异常,操作数组时类型不一致) 163 | - 还有IO操作的BufferOverflowException异常 164 | - ConcurrentModificationException异常 165 | 166 | ## 12. Java常见异常有哪些 167 | 168 | - java.lang.IllegalAccessError:违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。 169 | - java.lang.InstantiationError:实例化错误。当一个应用试图通过Java的new操作符构造一个抽象类或者接口时抛出该异常. 170 | - java.lang.OutOfMemoryError:内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。 171 | - java.lang.StackOverflowError:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。 172 | - java.lang.ClassCastException:类造型异常。假设有类A和B(A不是B的父类或子类),O是A的实例,那么当强制将O构造为类B的实例时抛出该异常。该异常经常被称为强制类型转换异常。 173 | - java.lang.ClassNotFoundException:找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。 174 | - java.lang.ArithmeticException:算术条件异常。譬如:整数除零等。 175 | - java.lang.ArrayIndexOutOfBoundsException:数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。 176 | - java.lang.IndexOutOfBoundsException:索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。 177 | - java.lang.InstantiationException:实例化异常。当试图通过newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时,抛出该异常。 178 | - java.lang.NoSuchFieldException:属性不存在异常。当访问某个类的不存在的属性时抛出该异常。 179 | - java.lang.NoSuchMethodException:方法不存在异常。当访问某个类的不存在的方法时抛出该异常。 180 | - java.lang.NullPointerException:空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等。 181 | - java.lang.NumberFormatException:数字格式异常。当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。 182 | - java.lang.StringIndexOutOfBoundsException:字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于0或大于等于序列大小时,抛出该异常。 183 | 184 | ## 13、Java异常处理最佳实践 185 | 186 | - 在 Java 中处理异常并不是一个简单的事情。不仅仅初学者很难理解,即使一些有经验的开发者也需要花费很多时间来思考如何处理异常,包括需要处理哪些异常,怎样处理等等。这也是绝大多数开发团队都会制定一些规则来规范进行异常处理的原因。而团队之间的这些规范往往是截然不同的。 187 | 188 | - 本文给出几个被很多团队使用的异常处理最佳实践。 189 | 190 | ### 1. 在 finally 块中清理资源或者使用 try-with-resource 语句 191 | 192 | - 当使用类似InputStream这种需要使用后关闭的资源时,一个常见的错误就是在try块的最后关闭资源。 193 | 194 | ```java 195 | public void doNotCloseResourceInTry() { 196 | FileInputStream inputStream = null; 197 | try { 198 | File file = new File("./tmp.txt"); 199 | inputStream = new FileInputStream(file); 200 | // use the inputStream to read a file 201 | // do NOT do this 202 | inputStream.close(); 203 | } catch (FileNotFoundException e) { 204 | log.error(e); 205 | } catch (IOException e) { 206 | log.error(e); 207 | } 208 | } 209 | ``` 210 | 211 | 212 | 213 | - 问题就是,只有没有异常抛出的时候,这段代码才可以正常工作。try 代码块内代码会正常执行,并且资源可以正常关闭。但是,使用 try 代码块是有原因的,一般调用一个或多个可能抛出异常的方法,而且,你自己也可能会抛出一个异常,这意味着代码可能不会执行到 try 代码块的最后部分。结果就是,你并没有关闭资源。 214 | 215 | - 所以,你应该把清理工作的代码放到 finally 里去,或者使用 try-with-resource 特性。 216 | 217 | ### 1.1 使用 finally 代码块 218 | 219 | - 与前面几行 try 代码块不同,finally 代码块总是会被执行。不管 try 代码块成功执行之后还是你在 catch 代码块中处理完异常后都会执行。因此,你可以确保你清理了所有打开的资源。 220 | 221 | ```java 222 | public void closeResourceInFinally() { 223 | FileInputStream inputStream = null; 224 | try { 225 | File file = new File("./tmp.txt"); 226 | inputStream = new FileInputStream(file); 227 | // use the inputStream to read a file 228 | } catch (FileNotFoundException e) { 229 | log.error(e); 230 | } finally { 231 | if (inputStream != null) { 232 | try { 233 | inputStream.close(); 234 | } catch (IOException e) { 235 | log.error(e); 236 | } 237 | } 238 | } 239 | } 240 | ``` 241 | 242 | 243 | 244 | ### 1.2 Java 7 的 try-with-resource 语法 245 | 246 | - 如果你的资源实现了 AutoCloseable 接口,你可以使用这个语法。大多数的 Java 标准资源都继承了这个接口。当你在 try 子句中打开资源,资源会在 try 代码块执行后或异常处理后自动关闭。 247 | 248 | ```java 249 | public void automaticallyCloseResource() { 250 | File file = new File("./tmp.txt"); 251 | try (FileInputStream inputStream = new FileInputStream(file);) { 252 | // use the inputStream to read a file 253 | } catch (FileNotFoundException e) { 254 | log.error(e); 255 | } catch (IOException e) { 256 | log.error(e); 257 | } 258 | } 259 | ``` 260 | 261 | 262 | 263 | ### 2. 优先明确的异常 264 | 265 | - 你抛出的异常越明确越好,永远记住,你的同事或者几个月之后的你,将会调用你的方法并且处理异常。 266 | - 因此需要保证提供给他们尽可能多的信息。这样你的 API 更容易被理解。你的方法的调用者能够更好的处理异常并且避免额外的检查。 267 | - 因此,总是尝试寻找最适合你的异常事件的类,例如,抛出一个 NumberFormatException 来替换一个 IllegalArgumentException 。避免抛出一个不明确的异常。 268 | ```java 269 | - public void doNotDoThis() throws Exception { 270 | - ... 271 | - } 272 | - public void doThis() throws NumberFormatException { 273 | - ... 274 | - } 275 | ``` 276 | ### 3. 对异常进行文档说明 277 | 278 | - 当在方法上声明抛出异常时,也需要进行文档说明。目的是为了给调用者提供尽可能多的信息,从而可以更好地避免或处理异常。 279 | - 在 Javadoc 添加 @throws 声明,并且描述抛出异常的场景。 280 | ```java 281 | - public void doSomething(String input) throws MyBusinessException { 282 | - ... 283 | - } 284 | ``` 285 | ### 4. 使用描述性消息抛出异常 286 | 287 | - 在抛出异常时,需要尽可能精确地描述问题和相关信息,这样无论是打印到日志中还是在监控工具中,都能够更容易被人阅读,从而可以更好地定位具体错误信息、错误的严重程度等。 288 | - 但这里并不是说要对错误信息长篇大论,因为本来 Exception 的类名就能够反映错误的原因,因此只需要用一到两句话描述即可。 289 | - 如果抛出一个特定的异常,它的类名很可能已经描述了这种错误。所以,你不需要提供很多额外的信息。一个很好的例子是 NumberFormatException 。当你以错误的格式提供 String 时,它将被 java.lang.Long 类的构造函数抛出。 290 | ```java 291 | - try { 292 | - new Long("xyz"); 293 | - } catch (NumberFormatException e) { 294 | - log.error(e); 295 | - } 296 | ``` 297 | ### 5. 优先捕获最具体的异常 298 | 299 | - 大多数 IDE 都可以帮助你实现这个最佳实践。当你尝试首先捕获较不具体的异常时,它们会报告无法访问的代码块。 300 | - 但问题在于,只有匹配异常的第一个 catch 块会被执行。 因此,如果首先捕获 IllegalArgumentException ,则永远不会到达应该处理更具体的 NumberFormatException 的 catch 块,因为它是 IllegalArgumentException 的子类。 301 | - 总是优先捕获最具体的异常类,并将不太具体的 catch 块添加到列表的末尾。 302 | - 你可以在下面的代码片断中看到这样一个 try-catch 语句的例子。 第一个 catch 块处理所有 NumberFormatException 异常,第二个处理所有非 NumberFormatException 异常的IllegalArgumentException 异常。 303 | ```java 304 | - public void catchMostSpecificExceptionFirst() { 305 | - try { 306 | - doSomething("A message"); 307 | - } catch (NumberFormatException e) { 308 | - log.error(e); 309 | - } catch (IllegalArgumentException e) { 310 | - log.error(e) 311 | - 312 | - } 313 | ``` 314 | ### 6. 不要捕获 Throwable 类 315 | 316 | - Throwable 是所有异常和错误的超类。你可以在 catch 子句中使用它,但是你永远不应该这样做! 317 | - 如果在 catch 子句中使用 Throwable ,它不仅会捕获所有异常,也将捕获所有的错误。JVM 抛出错误,指出不应该由应用程序处理的严重问题。 典型的例子是 OutOfMemoryError 或者 StackOverflowError 。两者都是由应用程序控制之外的情况引起的,无法处理。 318 | - 所以,最好不要捕获 Throwable ,除非你确定自己处于一种特殊的情况下能够处理错误。 319 | ```java 320 | - public void doNotCatchThrowable() { 321 | - try { 322 | - // do something 323 | - } catch (Throwable t) { 324 | - // don't do this! 325 | - } 326 | - } 327 | ``` 328 | ### 7. 不要忽略异常 329 | 330 | - 很多时候,开发者很有自信不会抛出异常,因此写了一个catch块,但是没有做任何处理或者记录日志。 331 | ```java 332 | - public void doNotIgnoreExceptions() { 333 | - try { 334 | - // do something 335 | - } catch (NumberFormatException e) { 336 | - // this will never happen 337 | - } 338 | - } 339 | ``` 340 | - 但现实是经常会出现无法预料的异常,或者无法确定这里的代码未来是不是会改动(删除了阻止异常抛出的代码),而此时由于异常被捕获,使得无法拿到足够的错误信息来定位问题。 341 | - 合理的做法是至少要记录异常的信息。 342 | ```java 343 | - public void logAnException() { 344 | - try { 345 | - // do something 346 | - } catch (NumberFormatException e) { 347 | - log.error("This should never happen: " + e); 348 | - } 349 | - } 350 | ``` 351 | ### 8. 不要记录并抛出异常 352 | 353 | - 这可能是本文中最常被忽略的最佳实践。可以发现很多代码甚至类库中都会有捕获异常、记录日志并再次抛出的逻辑。如下: 354 | ```java 355 | - try { 356 | - new Long("xyz"); 357 | - } catch (NumberFormatException e) { 358 | - log.error(e); 359 | - throw e; 360 | - } 361 | ``` 362 | - 这个处理逻辑看着是合理的。但这经常会给同一个异常输出多条日志。如下: 363 | - 17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz" 364 | - Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz" 365 | - at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) 366 | - at java.lang.Long.parseLong(Long.java:589) 367 | - at java.lang.Long.(Long.java:965) 368 | - at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63) 369 | - at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58) 370 | - 如上所示,后面的日志也没有附加更有用的信息。如果想要提供更加有用的信息,那么可以将异常包装为自定义异常。 371 | ```java 372 | - public void wrapException(String input) throws MyBusinessException { 373 | - try { 374 | - // do something 375 | - } catch (NumberFormatException e) { 376 | - throw new MyBusinessException("A message that describes the error.", e); 377 | - } 378 | - } 379 | ``` 380 | 因此,仅仅当想要处理异常时才去捕获,否则只需要在方法签名中声明让调用者去处理。 381 | 382 | ### 9. 包装异常时不要抛弃原始的异常 383 | 384 | - 捕获标准异常并包装为自定义异常是一个很常见的做法。这样可以添加更为具体的异常信息并能够做针对的异常处理。 385 | - 在你这样做时,请确保将原始异常设置为原因(注:参考下方代码 NumberFormatException e 中的原始异常 e )。Exception 类提供了特殊的构造函数方法,它接受一个 Throwable 作为参数。否则,你将会丢失堆栈跟踪和原始异常的消息,这将会使分析导致异常的异常事件变得困难 386 | ```java 387 | - public void wrapException(String input) throws MyBusinessException { 388 | - try { 389 | - // do something 390 | - } catch (NumberFormatException e) { 391 | - throw new MyBusinessException("A message that describes the error.", e); 392 | - } 393 | - } 394 | ``` 395 | ### 10. 不要使用异常控制程序的流程 396 | 397 | - 不应该使用异常控制应用的执行流程,例如,本应该使用if语句进行条件判断的情况下,你却使用异常处理,这是非常不好的习惯,会严重影响应用的性能。 398 | 399 | ### 11. 使用标准异常 400 | 401 | - 如果使用内建的异常可以解决问题,就不要定义自己的异常。Java API 提供了上百种针对不同情况的异常类型,在开发中首先尽可能使用 Java API 提供的异常,如果标准的异常不能满足你的要求,这时候创建自己的定制异常。尽可能得使用标准异常有利于新加入的开发者看懂项目代码。 402 | 403 | ### 12. 异常会影响性能 404 | 405 | - 异常处理的性能成本非常高,每个 Java 程序员在开发时都应牢记这句话。创建一个异常非常慢,抛出一个异常又会消耗1~5ms,当一个异常在应用的多个层级之间传递时,会拖累整个应用的性能。 406 | - 仅在异常情况下使用异常; 407 | - 在可恢复的异常情况下使用异常; 408 | - 尽管使用异常有利于 Java 开发,但是在应用中最好不要捕获太多的调用栈,因为在很多情况下都不需要打印调用栈就知道哪里出错了。因此,异常消息应该提供恰到好处的信息。 409 | 410 | ### 13. 总结 411 | 412 | - 综上所述,当你抛出或捕获异常的时候,有很多不同的情况需要考虑,而且大部分事情都是为了改善代码的可读性或者 API 的可用性。 413 | - 异常不仅仅是一个错误控制机制,也是一个通信媒介。因此,为了和同事更好的合作,一个团队必须要制定出一个最佳实践和规则,只有这样,团队成员才能理解这些通用概念,同时在工作中使用它。 414 | -------------------------------------------------------------------------------- /Collections/JVM.md: -------------------------------------------------------------------------------- 1 | # JVM面试题总结 2 | ## 1、Java如何实现平台无关性? 3 | 4 | 平台无关性就是一种语言在计算机上的运行不受平台的约束,一次编译,到处执行。 5 | 通过javac的编译把.java代码转换成.class代码,即把java代码转换成java字节码组成的class文件。 6 | 通过不同平台的Java虚拟机将Class文件转成对应平台的二进制文件,即机器码文件。然后就可以在不同平台上运行了。 7 | 8 | 9 | ## 2、JVM有哪几个部分组成? 10 | 11 | 1. 类加载器 Class Loader 12 | 类加载器的作用是加载类文件到内存,比如编写一个HelloWorld.java程序,然后通过javac编译生成class文件。由Class Loader将class文件加载到内存中。但是Class Loader加载class文件有格式要求。注意:Class Loader只管加载,只要符合文件结构就加载,至于能不能运行,是由Execution Engine负责。 13 | 2. 执行引擎 Exexution Engine 14 | 执行引擎也叫作解释器,负责解释命令,提交操作系统执行。 15 | 3. 本地接口 Native Interface 16 | 本地接口的作用是为了融合不同的编程语言为Java所用。它的初衷是为了融合C/C++程序,Java诞生的时候是C/C++横行的时候,要想立足,必须要有一个聪明的、睿智的调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载加载native libraries。目前该方法只有在与硬件有关的应用中才会使用,在企业级应用中已经比较少见,因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用WebService等。 17 | 4. 运行数据区 Runtime data area 18 | 运行数据区使整个JVM的重点。我们所写的程序都被加载到这里,之后才开始运行,Java生态系统如此的繁荣,得益于该区域的优良自治。 19 | 20 | 21 | ## 3、你了解反射吗?说说你用到反射的场景 22 | 23 | 反射的定义: 24 | Class类是一个比Object类还抽象的应该东西,在POJO里面,Object表示所有的东西,而Class表示这些所有定义对象的类文件,可以理解为Class的实例对象,表示定义对象的字节码,Class就是Java的类的抽象。 25 | Class是不能用new来创建的,有三种获得Class实例的方法,分别如下 26 | Class a = String.class; 27 | Class b = "abc".getClass(); 28 | Class c = 29 | Class.forName("java.lang.String"); 30 | 类中的方法被抽象为Method类,属性被抽象为Field类,可以获取,私有的成员变量和方法也可以获取 31 | 反射的使用场景: 32 | 在搭建框架的时候,有时候不知道需要什么类,什么方法,这个类有哪些属性。比如查询数据库之后的数据,反射成对象。 33 | 比如: 34 | 现在在配置文件中,定义了要使用的类com.User 35 | 我们在数据库里查询到了一个数据(这里用JSON字符串来代替) 36 | 我们根据配置,把这组数据,转换成配置中的对象 37 | 38 | ## 4、虚拟机类加载机制相关 39 | 40 | 41 | ### 1、简述java类加载机制? 42 | 43 | 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。 44 | 45 | ### 2、描述一下JVM加载Class文件的原理机制? 46 | 47 | Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。 48 | 类装载方式,有两种 : 49 | 1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中, 50 | 2.显式装载, 通过class.forname()等方法,显式加载需要的类 51 | Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。 52 | 53 | ### 3、什么是类加载器,类加载器有哪些? 54 | 55 | Java中的类加载器实质上也是也是类,功能是把类加载入JVM中,值得注意的是JVM的类加载器有四个,原因有:一方面是为了分工明确,各自负责各自的区块,另一方面为了实现委托模型。 56 | 层次结构如下: 57 | BootStrap Loader(引导类加载器) ----- 负责加载系统类 58 | ExtClassLoader(扩展类加载器) ----- 负责加载扩展类 59 | AppClassLoade(应用类加载器) ----- 负责加载应用类 60 | 自定义类加载器,程序员自定义的类加载器 61 | 62 | ### 4、说一下类装载的执行过程? 63 | 64 | 类装载分为以下 5 个步骤: 65 | 加载:根据查找路径找到相应的 class 文件然后导入; 66 | 验证:检查加载的 class 文件的正确性; 67 | 准备:给类中的静态变量分配内存空间; 68 | 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址; 69 | 初始化:对静态变量和静态代码块执行初始化工作。 70 | 71 | ### 5、说说双亲委派机制? 72 | 73 | 双亲委派的意思是如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。这里的双亲其实就指的是父类,没有mother。父类也不是我们平日所说的那种继承关系,只是调用逻辑是这样。 74 | 双亲委派有啥好处呢?它使得类有了层次的划分。样如果有不法分子自己造了个java.lang.Object,里面嵌了不好的代码,如果我们是按照双亲委派模型来实现的话,最终加载到JVM中的只会是我们rt.jar里面的东西,也就是这些核心的基础类代码得到了保护。因为这个机制使得系统中只会出现一个java.lang.Object。不会乱套了。你想想如果我们JVM里面有两个Object,那岂不是天下大乱了。 75 | 76 | 77 | ## 5、Java会存在内存泄漏吗?请简单描述 78 | 79 | 内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。 80 | 但是,即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。 81 | 82 | ## 6、垃圾收集器相关 83 | 84 | ### 1、简述Java垃圾回收机制 85 | 86 | 在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。 87 | 88 | ### 2、GC是什么?为什么要GC 89 | 90 | GC 是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存 91 | 回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动 92 | 回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。 93 | 94 | ### 3、垃圾回收的优点和原理。并考虑2种回收机制 95 | 96 | java语言最显著的特点就是引入了垃圾回收机制,它使java程序员在编写程序时不再考虑内存管理的问题。 97 | 由于有这个垃圾回收机制,java中的对象不再有“作用域”的概念,只有引用的对象才有“作用域”。 98 | 垃圾回收机制有效的防止了内存泄露,可以有效的使用可使用的内存。 99 | 垃圾回收器通常作为一个单独的低级别的线程运行,在不可预知的情况下对内存堆中已经死亡的或很长时间没有用过的对象进行清除和回收。 100 | 程序员不能实时的对某个对象或所有对象调用垃圾回收器进行垃圾回收。 101 | 垃圾回收有分代复制垃圾回收、标记垃圾回收、增量垃圾回收。 102 | 103 | ### 4、垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收? 104 | 105 | 对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。 106 | 通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。 107 | 可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。 108 | ### 5、Java 中都有哪些引用类型? 109 | 110 | 强引用:发生 gc 的时候不会被回收。 111 | 软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。 112 | 弱引用:有用但不是必须的对象,在下一次GC时会被回收。 113 | 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。 114 | 115 | ### 6、怎么判断对象是否可以被回收? 116 | 117 | 垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是「存活」的,是不可以被回收的;哪些对象已经「死掉」了,需要被回收。 118 | 一般有两种方法来判断: 119 | 引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题; 120 | 可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。 121 | 122 | ### 7、在Java中,对象什么时候可以被垃圾回收 123 | 124 | 当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。 125 | 垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。 126 | 127 | ### 8、JVM中的永久代中会发生垃圾回收吗 128 | 129 | 垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。请参考下Java8:从永久代到元空间区 130 | (译者注:Java8中已经移除了永久代,新加了一个叫做元空间区的native内存区) 131 | 132 | ### 9、说一下 JVM 有哪些垃圾回收算法? 133 | 134 | (1)标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。 135 | 标记无用对象,然后进行清除回收。 136 | 标记-清除算法(Mark-Sweep)是一种常见的基础垃圾收集算法,它将垃圾收集分为两个阶段: 137 | 标记阶段:标记出可以回收的对象。 138 | 清除阶段:回收被标记的对象所占用的空间。 139 | 标记-清除算法之所以是基础的,是因为后面讲到的垃圾收集算法都是在此算法的基础上进行改进的。 140 | 优点:实现简单,不需要对象进行移动。 141 | 缺点:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。 142 | 143 | (2)复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。 144 | 为了解决标记-清除算法的效率不高的问题,产生了复制算法。它把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收。 145 | 优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。 146 | 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。 147 | 148 | (3)标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。 149 | 在新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率会较高,这样会有较多的复制操作,导致效率变低。标记-清除算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎片。因此就出现了一种标记-整理算法(Mark-Compact)算法,与标记-整理算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,已用和未用的内存都各自一边。 150 | 优点:解决了标记-清理算法存在的内存碎片问题。 151 | 缺点:仍需要进行局部对象移动,一定程度上降低了效率。 152 | 153 | (4)分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。 154 | 155 | ### 10、说一下 JVM 有哪些垃圾回收器? 156 | 157 | 如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了7种作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用。 158 | 159 | Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效; 160 | 161 | ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现; 162 | 163 | Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景; 164 | 165 | Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本; 166 | 167 | Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本; 168 | 169 | CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。 170 | 171 | G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。 172 | 173 | ### 11、详细介绍一下 CMS 垃圾回收器? 174 | 175 | CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。 176 | CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。 177 | 178 | ### 12、新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别? 179 | 180 | 新生代回收器:Serial、ParNew、Parallel Scavenge 181 | 老年代回收器:Serial Old、Parallel Old、CMS 182 | 整堆回收器:G1 183 | 新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。 184 | ### 13、简述分代垃圾回收器是怎么工作的? 185 | 186 | 分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。 187 | 新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下: 188 | 189 | 把 Eden + From Survivor 存活的对象放入 To Survivor 区; 190 | 191 | 清空 Eden 和 From Survivor 分区; 192 | 193 | From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。 194 | 195 | 每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。 196 | 197 | 老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。 198 | 199 | 200 | ## 7、内存分配策略相关:简述java内存分配与回收策率以及Minor GC和Major GC 201 | 202 | 203 | 对象的内存分配通常是在 Java 堆上分配(某些场景下也会在栈上分配),对象主要分配在新生代的 Eden 区,如果启动了本地线程缓冲,将按照线程优先在 TLAB 上分配。少数情况下也会直接在老年代上分配。总的来说分配规则不是百分百固定的,其细节取决于哪一种垃圾收集器组合以及虚拟机相关参数有关,但是虚拟机对于内存的分配还是会遵循以下几种「普世」规则: 204 | 对象优先在 Eden 区分配 205 | 206 | 多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。 207 | 这里我们提到 Minor GC,如果你仔细观察过 GC 日常,通常我们还能从日志中发现 Major GC/Full GC。 208 | 209 | Minor GC 是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快; 210 | 211 | Major GC/Full GC 是指发生在老年代的 GC,出现了 Major GC 通常会伴随至少一次 Minor GC。Major GC 的速度通常会比 Minor GC 慢 10 倍以上。 212 | 213 | 大对象直接进入老年代 214 | 所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导致在内存还有不少空间的情况下提前触发 GC 以获取足够的连续空间来安置新对象。 215 | 前面我们介绍过新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配。 216 | 长期存活对象将进入老年代 217 | 218 | 虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须判断哪些对象应该放在新生代,哪些对象应该放在老年代。因此虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在 Eden 区出生,并且能够被 Survivor 容纳,将被移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区中每「熬过」一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升到老年代。 219 | 220 | 至于为什么是15,是因为mark word里存放的对象的分代年龄最多是二进制的1111.所以是15. 221 | 222 | 223 | ## 8、说一下 JVM 运行时数据区? 224 | 225 | Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁。Java 虚拟机所管理的内存被划分为如下几个区域: 226 | 227 | 228 | 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成; 229 | 230 | 231 | Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息; 232 | 233 | 234 | 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的; 235 | 236 | 237 | Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存; 238 | 239 | 240 | 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。 241 | 242 | ## 9、说一下深拷贝与浅拷贝(深复制与浅复制) 243 | 244 | 浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址, 245 | 246 | 深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存, 247 | 248 | 使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。 249 | 250 | 浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。 251 | 252 | 深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。 253 | 254 | ## 10、说一下堆栈的区别? 255 | 256 | **物理地址** 257 | 258 | 259 | 堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩) 260 | 261 | 262 | 栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。 263 | 264 | **内存分别** 265 | 266 | 堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。 267 | 268 | 栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。 269 | 270 | **存放的内容** 271 | 272 | 堆存放的是对象的实例和数组。因此该区更关注的是数据的存储 273 | 274 | 栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。 275 | 276 | **程序的可见度** 277 | 278 | 堆对于整个应用程序都是共享、可见的。 279 | 280 | 栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。 281 | 282 | **PS:** 283 | 284 | 静态变量放在方法区 285 | 286 | 静态的对象还是放在堆。 287 | 288 | ## 11、队列和栈是什么?有什么区别? 289 | 290 | 队列和栈都是被用来预存储数据的。 291 | 292 | 操作的名称不同。队列的插入称为入队,队列的删除称为出队。栈的插入称为进栈,栈的删除称为出栈。 293 | 可操作的方式不同。队列是在队尾入队,队头出队,即两边都可操作。而栈的进栈和出栈都是在栈顶进行的,无法对栈底直接进行操作。 294 | 295 | 操作的方法不同。队列是先进先出(FIFO),即队列的修改是依先进先出的原则进行的。新来的成员总是加入队尾(不能从中间插入),每次离开的成员总是队列头上(不允许中途离队)。而栈为后进先出(LIFO),即每次删除(出栈)的总是当前栈中最新的元素,即最后插入(进栈)的元素,而最先插入的被放在栈的底部,要到最后才能删除。 296 | 297 | ## 12、JVM加载.class文件的过程? 298 | 299 | 1. Java中的所有类,必须被装载到JVM中才能运行,这个装载工作是由JVM中的类装载器完成的,类装载器所做的工作实质是把类文件从硬盘读取到内存中,作用就是在运行时加载类。 300 | 301 | Java类加载器基于三个机制:**委托、可见性和单一性。** 302 | 303 | (1)委托机制是指加载一个类的请求交给父类加载器,如果这个父类加载器不能够找到或加载这个类,那么再加载它。 304 | 305 | (2)可见性的原理是子类的加载器可以看见所有的父类加载器加载的类,而父类加载器看不到子类加载器加载的类。 306 | 307 | (3)单一性原理是指一个类仅被加载一次,这是由委托机制确保子类加载器不会再次加载父类加载器加载过的类。 308 | 309 | ### 2. Java中的类大致分为三种: 310 | (1)系统类 311 | 312 | (2)扩展类 313 | 314 | (3)由程序员自定义的类 315 | 316 | ### 3. 类装载有两种方式 317 | 318 | (1)隐式装载:程序在运行过程中当碰到通过new等方式生成类或者子类对象、使用类或者子类的静态域时,隐式调用类加载器加载对应的的类到JVM中。 319 | 320 | (2)显式装载:通过调用Class.forName()或者ClassLoader.loadClass(className)等方法,显式加载需要的类。 321 | 322 | ### 4. 类加载的动态性体现 323 | 324 | 一个应用程序总是由n多个类组成,Java程序启动时,并不是一次把所有的类全部加载再运行,他总是把保证程序运行的基础类一次性加载到JVM中,其他类等到JVM用到的时候再加载,这样是为了节省内存的开销,因为Java最早就是为嵌入式系统而设计的,内存宝贵,而用到时再加载这也是Java动态性的一种体现。 325 | 326 | ### 5. Java类加载器 327 | Java中的类加载器实质上也是也是类,功能是把类加载入JVM中,值得注意的是JVM的类加载器有四个,原因有:一方面是为了分工明确,各自负责各自的区块,另一方面为了实现委托模型。 328 | 层次结构如下: 329 | 330 | BootStrap Loader(引导类加载器) ----- 负责加载系统类 331 | 332 | ExtClassLoader(扩展类加载器) ----- 负责加载扩展类 333 | 334 | AppClassLoade(应用类加载器) ----- 负责加载应用类 335 | 336 | 自定义类加载器,程序员自定义的类加载器 337 | 338 | ### 6.Java类加载器的工作原理: 339 | 340 | 当执行Java的.class文件的时候,java.exe会帮助我们找到jRE,接着找到JRE内部的jvm.dll,这才是真正的Java虚拟机器,最后加载动态库,激活Java虚拟机器。虚拟机激活以后,会先做一些初始化的动作,比如说读取系统参数等。一旦初始化动作完成后,就会产生第一个类加载器-----Bootstrap Loader,Bootstrap Loader是由C++撰写而成,这个Bootstrap所做的初始工作中,除了一些基本的初始化动作之外,最重要的就是加载Launcher.java之中的ExtClassLoader,并设定其parent为null,代表其父加载器为BootstrapLoader。然后Bootstrap loader再要求加载Launcher.java之中的AppClassLoader,并设定其parent为之前产生的ExtClassLoader实体。这两个类加载器都是以静态类的形式存在的。注意:LauncherExtClassLoader.class与LauncherExtClassLoader.class与LauncherAppClassLoader.class都是由Bootstrap Loader所加载,所以Parent和由哪个类加载器加载没有关系。 341 | 342 | ## 13.HotSpot虚拟机对象探秘 343 | 344 | 对象的创建 345 | 346 | 说到对象的创建,首先让我们看看 Java 中提供的几种对象创建方式: 347 | 348 | 使用new关键字: 调用了构造函数 349 | 350 | 使用Class的newInstance方法: 调用了构造函数 351 | 352 | 使用Constructor类的newInstance方法: 调用了构造函数 353 | 354 | 使用clone方法: 没有调用构造函数 355 | 356 | 使用反序列化: 没有调用构造函数 357 | 358 | 下面是对象创建的主要流程: 359 | 360 | 虚拟机遇到一条new指令时,先检查常量池是否已经加载相应的类,如果没有,必须先执行相应的类加载。类加载通过后,接下来分配内存。若Java堆中内存是绝对规整的,使用“指针碰撞“方式分配内存;如果不是规整的,就从空闲列表中分配,叫做”空闲列表“方式。划分内存时还需要考虑一个问题-并发,也有两种方式: CAS同步处理,或者本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。然后内存空间初始化操作,接着是做一些必要的对象设置(元信息、哈希码…),最后执行方法。 361 | 362 | ### 为对象分配内存 363 | 364 | 类加载完成后,接着会在Java堆中划分一块内存分配给对象。内存分配根据Java堆是否规整,有两种方式: 365 | 366 | 指针碰撞:如果Java堆的内存是规整,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。 367 | 368 | 空闲列表:如果Java堆的内存不是规整的,则需要由虚拟机维护一个列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录。 369 | 370 | 选择哪种分配方式是由 Java 堆是否规整来决定的,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。 371 | 372 | ### 处理并发安全问题 373 | 对象的创建在虚拟机中是一个非常频繁的行为,哪怕只是修改一个指针所指向的位置,在并发情况下也是不安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案: 374 | 375 | 对分配内存空间的动作进行同步处理(采用 CAS + 失败重试来保障更新操作的原子性); 376 | 377 | 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。哪个线程要分配内存,就在哪个线程的 TLAB 上分配。只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁。通过 -XX:+/-UserTLAB 参数来设定虚拟机是否使用TLAB。 378 | 379 | ### 对象的访问定位 380 | 381 | Java程序需要通过 JVM 栈上的引用访问堆中的具体对象。对象的访问方式取决于 JVM 虚拟机的实现。目前主流的访问方式有 句柄 和 直接指针 两种方式。 382 | 383 | **指针**: 指向对象,代表一个对象在内存中的起始地址。 384 | 385 | **句柄**: 可以理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,而是指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的真实内存地址。 386 | 387 | **句柄访问** 388 | Java堆中划分出一块内存来作为句柄池,引用中存储对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。 389 | 390 | 优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。 391 | 392 | **直接访问** 393 | 如果使用直接指针访问,引用 中存储的直接就是对象地址,那么Java堆对象内部的布局中就必须考虑如何放置访问类型数据的相关信息。 394 | 395 | 优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot 中采用的就是这种方式。 396 | 397 | ## 14.JVM调优相关 398 | 399 | ### 说一下 JVM 调优的工具? 400 | 401 | JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。 402 | 403 | jconsole:用于对 JVM 中的内存、线程和类等进行监控; 404 | 405 | jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。 406 | ### 常用的 JVM 调优的参数都有哪些? 407 | 408 | -Xms2g:初始化推大小为 2g; 409 | 410 | -Xmx2g:堆最大内存为 2g; 411 | 412 | -XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4; 413 | 414 | -XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2; 415 | 416 | –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合; 417 | 418 | -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合; 419 | 420 | -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合; 421 | 422 | -XX:+PrintGC:开启打印 gc 信息; 423 | 424 | -XX:+PrintGCDetails:打印 gc 详细信息。 425 | 426 | ## 15、如何打破双亲委派机制? 427 | 428 | ### 为什么需要破坏双亲委派? 429 | 430 | 因为在某些情况下父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器无法加载到需要的文件,以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,比如mysql的就写了MySQL Connector,那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载,只能记载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派,这里仅仅是举了破坏双亲委派的其中一个情况。 431 | 432 | ### 如何打破? 433 | 434 | 线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。了有线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。 435 | 436 | 437 | ## TIPS: JVM部分的知识强烈建议阅读《深入了解JAVA虚拟机》第三版,说的非常详细,虽然很多地方不是很深,但毕竟JVM还是要在工作中深入了解熟练运用才是 438 | -------------------------------------------------------------------------------- /Collections/JavaBasis.md: -------------------------------------------------------------------------------- 1 | # Java基础部分 2 | 3 | ## 1、什么是字节码?采用字节码的最大好处是什么 4 | 5 | 字节码:Java源代码经过虚拟机编译器编译后产生的文件(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。 6 | 7 | 采用字节码的好处: 8 | 9 | Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。 10 | 11 | 先看下java中的编译器和解释器: 12 | 13 | Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行,这就是上面提到的Java的特点的编译与解释并存的解释。 14 | 15 | Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中解释器----->机器可执行的二进制机器码---->程序运行。 16 | 17 | ## 2、什么是Java程序的主类?应用程序和小程序的主类有何不同? 18 | 19 | 一个程序中可以有多个类,但只能有一个类是主类。在Java应用程序中,这个主类是指包含main()方法的类。而在Java小程序中,这个主类是一个继承自系统类JApplet或Applet的子类。应用程序的主类不一定要求是public类,但小程序的主类要求必须是public类。主类是Java程序执行的入口点。 20 | 21 | ## 3、Java应用程序与小程序之间有那些差别? 22 | 23 | 简单说应用程序是从主线程启动(也就是main()方法)。applet小程序没有main方法,主要是嵌在浏览器页面上运行(调用init()线程或者run()来启动),嵌入浏览器这点跟flash的小游戏类似。 24 | 25 | ## 4、Java和CPP的区别?? 26 | 27 | 都是面向对象的语言,都支持封装、继承和多态 28 | 29 | Java不提供指针来直接访问内存,程序内存更加安全 30 | 31 | Java的类是单继承的,C++支持多重继承;虽然Java的类不可以多继承,但是接口可以多继承。 32 | 33 | Java有自动内存管理机制,不需要程序员手动释放无用内存 34 | 35 | ## 5、 Java有哪些数据类型? 36 | 37 | ![Image text](https://mubu.com/document_image/c5066aa1-9fae-4357-b4b4-a79495f0112f-4943374.jpg) 38 | 39 | ### 分类 40 | 41 | **基本数据类型** 42 | 43 | 数值型 44 | 45 | 整数类型(byte,short,int,long) 46 | 47 | 浮点类型(float,double) 48 | 49 | 字符型(char) 50 | 51 | 布尔型(boolean) 52 | 53 | **引用数据类型** 54 | 55 | 类(class) 56 | 57 | 接口(interface) 58 | 59 | 数组([]) 60 | 61 | ## 6、访问修饰符public, private, protected和不写的区别? 62 | 63 | ![Image text](https://mubu.com/document_image/0910680a-a2b7-4607-bedd-11e25baab883-4943374.jpg) 64 | 65 | private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类) 66 | 67 | default (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。 68 | 69 | protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。 70 | 71 | public : 对所有类可见。使用对象:类、接口、变量、方法 72 | 73 | ## 7、&和&&的区别 74 | 75 | &运算符有两种用法:(1)按位与;(2)逻辑与。 76 | 77 | &&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true 整个表达式的值才是 true。&&之所以称为短路运算,是因为如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。 78 | 79 | 注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。 80 | 81 | ## 8、final有什么用? 82 | 83 | 用于修饰类、属性和方法; 84 | 85 | 被final修饰的类不可以被继承 86 | 87 | 被final修饰的方法不可以被重写 88 | 89 | 被final修饰的变量不可以被改变,被final修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的 90 | 91 | ## 9、final, finally, finalize的区别? 92 | 93 | inal可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。 94 | 95 | finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。 96 | 97 | finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。 98 | 99 | ## 10、this和super的区别? 100 | 101 | super: 它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名(实参) 102 | 103 | this:它代表当前对象名(在程序中易产生二义性之处,应使用this来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用this来指明成员变量名) 104 | 105 | super()和this()类似,区别是,super()在子类中调用父类的构造方法,this()在本类内调用本类的其它构造方法。 106 | 107 | super()和this()均需放在构造方法内第一行。 108 | 109 | 尽管可以用this调用一个构造器,但却不能调用两个。 110 | 111 | this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。 112 | 113 | this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。 114 | 115 | 从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。 116 | 117 | ## 11、static存在的意义? 118 | 119 | static的主要意义是在于创建独立于具体对象的域变量或者方法。以致于即使没有创建对象,也能使用属性和调用方法! 120 | 121 | static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。 122 | 123 | 为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。 124 | 125 | 同时,使用static时,静态只能访问静态,非静态既可以访问静态的,也可以访问非静态的。 126 | 127 | ### static应用场景: 128 | 129 | 1、修饰成员变量 130 | 131 | 2、修饰成员方法 132 | 133 | 3、静态代码块 134 | 135 | 4、修饰类【只能修饰内部类也就是静态内部类】 136 | 137 | 5、静态导包 138 | 139 | ## 12、break, continue, return的区别和作用? 140 | 141 | break 跳出总上一层循环,不再执行循环(结束当前的循环体) 142 | 143 | continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件) 144 | 145 | return 程序返回,不再执行下面的代码(结束当前的方法 直接返回) 146 | 147 | ## 13、面向对象和面向过程的区别? 148 | 149 | 面向过程: 150 | 151 | 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。 152 | 153 | 缺点:没有面向对象易维护、易复用、易扩展 154 | 155 | 面向对象: 156 | 157 | 158 | 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护 159 | 160 | 缺点:性能比面向过程低 161 | 162 | 面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现。 163 | 164 | 面向对象是模型化的,你只需抽象出一个类,这是一个封闭的盒子,在这里你拥有数据也拥有解决问题的方法。需要什么功能直接使用就可以了,不必去一步一步的实现,至于这个功能是如何实现的,管我们什么事?我们会用就可以了。 165 | 166 | 面向对象的底层其实还是面向过程,把面向过程抽象成类,然后封装,方便我们使用的就是面向对象了。 167 | 168 | ## 14、面向对象三大特征? 169 | 170 | 面向对象的特征主要有以下几个方面: 171 | 172 | **抽象**:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。 173 | 174 | **封装**:封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。 175 | 176 | **继承**:继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。关于继承如下 3 点请记住: 177 | 178 | 子类拥有父类非 private 的属性和方法。 179 | 180 | 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 181 | 182 | 子类可以用自己的方式实现父类的方法。 183 | 184 | **多态**:所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。 185 | 多态性:父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。 186 | 在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。 187 | 方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。 188 | 189 | ## 15、什么是多态机制? Java语言是如何实现多态的? 190 | 191 | 所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。 192 | 193 | 多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。 194 | 195 | ### 多态的实现 196 | 197 | Java实现多态有三个必要条件:继承、重写、向上转型。 198 | 199 | **继承**:在多态中必须存在有继承关系的子类和父类。 200 | 201 | **重写**:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。 202 | 203 | **向上转型**:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。 204 | 205 | 只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。 206 | 207 | 对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。 208 | 209 | ## 16、面向对象五大基本原则? 210 | 211 | 单一职责原则SRP(Single Responsibility Principle):类的功能要单一,不能包罗万象,跟杂货铺似的。 212 | 213 | 开放封闭原则OCP(Open-Close Principle):一个模块对于拓展是开放的,对于修改是封闭的,想要增加功能热烈欢迎,想要修改,不乐意。 214 | 215 | 里式替换原则LSP(the Liskov Substitution Principle LSP):子类可以替换父类出现在父类能够出现的任何地方。 216 | 217 | 依赖倒置原则DIP(the Dependency Inversion Principle DIP):高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。就是你出国要说你是中国人,而不能说你是哪个村子的。比如说中国人是抽象的,下面有具体的xx省,xx市,xx县。你要依赖的抽象是中国人,而不是你是xx村的。 218 | 219 | 接口分离原则ISP(the Interface Segregation Principle ISP):设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。就比如一个手机拥有打电话,看视频,玩游戏等功能,把这几个功能拆分成不同的接口,比在一个接口里要好的多。 220 | 221 | ## 17、抽象类和接口的对比? 222 | 223 | 抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。 224 | 225 | 从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。 226 | 227 | **相同点** 228 | 229 | 接口和抽象类都不能实例化 230 | 231 | 都位于继承的顶端,用于被其他实现或继承 232 | 233 | 都包含抽象方法,其子类都必须覆写这些抽象方法 234 | 235 | **不同点** 236 | ![Image text](https://mubu.com/document_image/eb870ca8-47be-4c4a-aa41-36d2511ac599-4943374.jpg) 237 | 238 | 备注:Java8中接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。 239 | 240 | ## 18、普通类和抽象类有哪些区别? 241 | 242 | 普通类不能包含抽象方法,抽象类可以包含抽象方法。 243 | 244 | 抽象类不能直接实例化,普通类可以直接实例化。 245 | 246 | ## 19、抽象类能使用final修饰吗? 247 | 248 | 不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类 249 | 250 | ## 20、 成员变量与局部变量的区别有哪些 251 | 252 | **作用域** 253 | 254 | 成员变量:针对整个类有效。 255 | 256 | 局部变量:只在某个范围内有效。(一般指的就是方法,语句体内) 257 | 258 | **存储位置** 259 | 260 | 成员变量:随着对象的创建而存在,随着对象的消失而消失,存储在堆内存中。 261 | 262 | 局部变量:在方法被调用,或者语句被执行的时候存在,存储在栈内存中。当方法调用完,或者语句结束后,就自动释放。 263 | 264 | **生命周期** 265 | 266 | 成员变量:随着对象的创建而存在,随着对象的消失而消失 267 | 268 | 局部变量:当方法调用完,或者语句结束后,就自动释放。 269 | 270 | **初始值** 271 | 272 | 成员变量:有默认初始值。 273 | 274 | 局部变量:没有默认初始值,使用前必须赋值。 275 | 276 | **使用原则** 277 | 278 | 在使用变量时需要遵循的原则为:就近原则。首先在局部范围找,有就使用;接着在成员位置找。 279 | 280 | 281 | ## 21、构造方法有哪些特性? 282 | 283 | 名字与类名相同; 284 | 285 | 没有返回值,但不能用void声明构造函数; 286 | 287 | 生成类的对象时自动执行,无需调用。 288 | 289 | ## 22、静态变量和实例变量的区别? 290 | 291 | 静态变量: 静态变量由于不属于任何实例对象,属于类的,所以在内存中只会有一份,在类的加载过程中,JVM只为静态变量分配一次内存空间。 292 | 293 | 实例变量: 每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象的,在内存中,创建几次对象,就有几份成员变量。 294 | 295 | ## 23、静态变量和普通变量的区别? 296 | 297 | static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。 298 | 299 | 还有一点就是static成员变量的初始化顺序按照定义的顺序进行初始化。 300 | 301 | ## 24、静态方法和实例方法有何不同? 302 | 303 | 静态方法和实例方法的区别主要体现在两个方面: 304 | 305 | 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。 306 | 307 | 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制 308 | 309 | ## 25、在一个静态方法内调用一个非静态成员为什么是非法的? 310 | 311 | 由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。 312 | 313 | ## 26、什么是内部类? 314 | 315 | 在Java中,可以将一个类的定义放在另外一个类的定义内部,这就是内部类。内部类本身就是类的一个属性,与其他属性定义方式一致。 316 | 317 | ## 27、内部类的分类有哪些? 318 | 319 | 320 | 内部类可以分为四种:成员内部类、局部内部类、匿名内部类和静态内部类。 321 | 322 | ### 1、静态内部类 323 | 324 | 定义在类内部的静态类,就是静态内部类。 325 | 326 | 静态内部类可以访问外部类所有的静态变量,而不可访问外部类的非静态变量;静态内部类的创建方式: 327 | 328 | new 外部类.静态内部类() 329 | 330 | ### 2、成员内部类 331 | 332 | 定义在类内部,成员位置上的非静态类,就是成员内部类: 333 | 334 | 成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有。成员内部类依赖于外部类的实例,它的创建方式: 335 | 336 | 外部类实例.new 内部类() 337 | 338 | ### 3、局部内部类 339 | 340 | 定义在方法中的内部类,就是局部内部类。 341 | 342 | 定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法。局部内部类的创建方式,在对应方法内,new 内部类() 343 | 344 | ### 4、匿名内部类 345 | 346 | 匿名内部类就是没有名字的内部类,日常开发中使用的比较多。示例如下: 347 | 348 | ![Image text](https://mubu.com/document_image/d26b4341-ac4d-4e38-ad0e-637500ef3d40-4943374.jpg) 349 | 350 | 除了没有名字,匿名内部类还有以下特点: 351 | 352 | 匿名内部类必须继承一个抽象类或者实现一个接口。 353 | 354 | 匿名内部类不能定义任何静态成员和静态方法。 355 | 356 | 当所在的方法的形参需要被匿名内部类使用时,必须声明为 final。 357 | 358 | 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。 359 | 360 | 匿名内部类创建方式: 361 | ```java 362 | new 类/接口{ //匿名内部类实现部分} 363 | ``` 364 | 365 | ## 28、内部类的优点 366 | 367 | 一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据! 368 | 369 | 内部类不为同一包的其他类所见,具有很好的封装性; 370 | 371 | 内部类有效实现了“多重继承”,优化 java 单继承的缺陷。 372 | 373 | 匿名内部类可以很方便的定义回调。 374 | 375 | ## 29、内部类应用场景? 376 | 377 | 一些多算法场合 378 | 379 | 解决一些非面向对象的语句块。 380 | 381 | 适当使用内部类,使得代码更加灵活和富有扩展性。 382 | 383 | 当某个类除了它的外部类,不再被其他的类使用时。 384 | 385 | ## 30、局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final? 386 | 387 | 先看这段代码: 388 | 389 | ![Image text](https://mubu.com/document_image/948e6462-e615-4816-b552-b0816d93a234-4943374.jpg) 390 | 391 | 以上例子,为什么要加final呢?是因为生命周期不一致, 局部变量直接存储在栈中,当方法执行结束后,非final的局部变量就被销毁。而局部内部类对局部变量的引用依然存在,如果局部内部类要调用局部变量时,就会出错。加了final,可以确保局部内部类使用的变量与外层的局部变量区分开,解决了这个问题。 392 | 393 | ## 31、构造器是否可以被重写 394 | 395 | 构造器不能被继承,因此不能被重写,但可以被重载。 396 | 397 | ## 32、重写和重载的区别? 398 | 399 | 方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。同时两者也对应着JVM的动态分派和静态分派,也就是运行时和编译时的区别。 400 | 401 | 重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分 402 | 403 | 重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。 404 | 405 | ## 33、==和equals的区别是什么? 406 | 407 | == : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址) 408 | 409 | equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况: 410 | 411 | 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。 412 | 413 | 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。 414 | 415 | ## 34、hashCode和equals 416 | 417 | ### hashCode()介绍 418 | 419 | hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。 420 | 421 | hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。 422 | 423 | 散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) 424 | 425 | ### 为什么要有 hashCode 426 | 427 | 我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode: 428 | 429 | 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 430 | 431 | ### hashCode()与equals()的相关规定 432 | 433 | 如果两个对象相等,则hashcode一定也是相同的 434 | 435 | 两个对象相等,对两个对象分别调用equals方法都返回true 436 | 437 | 两个对象有相同的hashcode值,它们也不一定是相等的 438 | 439 | 因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖 440 | 441 | hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据) 442 | -------------------------------------------------------------------------------- /Collections/MySQL.md: -------------------------------------------------------------------------------- 1 | # MySQL面试题 2 | 3 | ## 1、数据库三大范式是什么 4 | 5 | 第一范式:每个列都不可以再拆分。 6 | 7 | 第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。 8 | 9 | 第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。 10 | 11 | 在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由。比如性能。事实上我们经常会为了性能而妥协数据库的设计。 12 | 13 | ## 2、mysql有关权限的表都有哪几个 14 | 15 | MySQL服务器通过权限表来控制用户对数据库的访问,权限表存放在mysql数据库里,由mysql_install_db脚本初始化。这些权限表分别user,db,table_priv,columns_priv和host。下面分别介绍一下这些表的结构和内容: 16 | 17 | user权限表:记录允许连接到服务器的用户帐号信息,里面的权限是全局级的。 18 | 19 | db权限表:记录各个帐号在各个数据库上的操作权限。 20 | 21 | table_priv权限表:记录数据表级的操作权限。 22 | 23 | columns_priv权限表:记录数据列级的操作权限。 24 | 25 | host权限表:配合db权限表对给定主机上数据库级操作权限作更细致的控制。这个权限表不受GRANT和REVOKE语句的影响。 26 | 27 | ## 3、mysql有哪些数据类型? 28 | 29 | 1、**整数类型**,包括TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT,分别表示1字节、2字节、3字节、4字节、8字节整数。任何整数类型都可以加上UNSIGNED属性,表示数据是无符号的,即非负整数。 30 | 31 | 长度:整数类型可以被指定长度,例如:INT(11)表示长度为11的INT类型。长度在大多数场景是没有意义的,它不会限制值的合法范围,只会影响显示字符的个数,而且需要和UNSIGNED ZEROFILL属性配合使用才有意义。 32 | 33 | 例子,假定类型设定为INT(5),属性为UNSIGNED ZEROFILL,如果用户插入的数据为12的话,那么数据库实际存储数据为00012。 34 | 35 | 2、**实数类型**,包括FLOAT、DOUBLE、DECIMAL。 36 | 37 | DECIMAL可以用于存储比BIGINT还大的整型,能存储精确的小数。 38 | 39 | 而FLOAT和DOUBLE是有取值范围的,并支持使用标准的浮点进行近似计算。 40 | 41 | 计算时FLOAT和DOUBLE相比DECIMAL效率更高一些,DECIMAL你可以理解成是用字符串进行处理。 42 | 43 | 3、**字符串类型,包括VARCHAR、CHAR、TEXT、BLOB** 44 | 45 | VARCHAR用于存储可变长字符串,它比定长类型更节省空间。 46 | 47 | VARCHAR使用额外1或2个字节存储字符串长度。列长度小于255字节时,使用1字节表示,否则使用2字节表示。 48 | 49 | VARCHAR存储的内容超出设置的长度时,内容会被截断。 50 | 51 | CHAR是定长的,根据定义的字符串长度分配足够的空间。 52 | 53 | CHAR会根据需要使用空格进行填充方便比较。 54 | 55 | CHAR适合存储很短的字符串,或者所有值都接近同一个长度。 56 | 57 | CHAR存储的内容超出设置的长度时,内容同样会被截断。 58 | 59 | **使用策略:** 60 | 61 | 对于经常变更的数据来说,CHAR比VARCHAR更好,因为CHAR不容易产生碎片。 62 | 63 | 对于非常短的列,CHAR比VARCHAR在存储空间上更有效率。 64 | 65 | 使用时要注意只分配需要的空间,更长的列排序时会消耗更多内存。 66 | 67 | 尽量避免使用TEXT/BLOB类型,查询时会使用临时表,导致严重的性能开销。 68 | 69 | **4、枚举类型(ENUM),把不重复的数据存储为一个预定义的集合。** 70 | 71 | 有时可以使用ENUM代替常用的字符串类型。 72 | 73 | ENUM存储非常紧凑,会把列表值压缩到一个或两个字节。 74 | 75 | ENUM在内部存储时,其实存的是整数。 76 | 77 | 尽量避免使用数字作为ENUM枚举的常量,因为容易混乱。 78 | 79 | 排序是按照内部存储的整数 80 | 81 | **5、日期和时间类型,尽量使用timestamp,空间效率高于datetime,** 82 | 83 | 用整数保存时间戳通常不方便处理。 84 | 85 | 如果需要存储微妙,可以使用bigint存储。 86 | 87 | ## 4、MySQL存储引擎有哪些? 88 | 89 | 存储引擎Storage engine:MySQL中的数据、索引以及其他对象是如何存储的,是一套文件系统的实现。 90 | 91 | 常用的存储引擎有以下: 92 | 93 | Innodb引擎:Innodb引擎提供了对数据库ACID事务的支持。并且还提供了行级锁和外键的约束。它的设计的目标就是处理大数据容量的数据库系统。 94 | 95 | MyIASM引擎(原本Mysql的默认引擎):不提供事务的支持,也不支持行级锁和外键。 96 | 97 | MEMORY引擎:所有的数据都在内存中,数据的处理速度快,但是安全性不高。 98 | 99 | ## 5、MyISAM索引与InnoDB索引的区别? 100 | 101 | InnoDB索引是聚簇索引,MyISAM索引是非聚簇索引。 102 | 103 | InnoDB的主键索引的叶子节点存储着行数据,因此主键索引非常高效。 104 | 105 | MyISAM索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据。 106 | 107 | InnoDB非主键索引的叶子节点存储的是主键和其他带索引的列数据,因此查询时做到覆盖索引会非常高效。 108 | 109 | InnoDB支持行锁和表锁,MyISAM只支持表锁 110 | 111 | ## 6、InnoDB引擎的4大特性 112 | 113 | 插入缓冲(insert buffer) 114 | 115 | 二次写(double write) 116 | 117 | 自适应哈希索引(ahi) 118 | 119 | 预读(read ahead) 120 | 121 | ## 7、索引相关面试题 122 | 123 | ### 什么是索引? 124 | 125 | 索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。 126 | 127 | 索引是一种数据结构。数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。 128 | 129 | 更通俗的说,索引就相当于目录。为了方便查找书中的内容,通过对内容建立索引形成目录。索引是一个文件,它是要占据物理空间的。 130 | 131 | ### 索引有哪些优缺点? 132 | 133 | #### 索引的优点 134 | 135 | 可以大大加快数据的检索速度,这也是创建索引的最主要的原因。 136 | 137 | 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。 138 | 139 | #### 索引的缺点 140 | 141 | 时间方面:创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,会降低增/改/删的执行效率; 142 | 143 | 空间方面:索引需要占物理空间。 144 | 145 | ### 索引使用场景? 146 | 147 | 根据id 148 | 149 | 根据id查询记录,若id字段仅建立了主键索引,则此SQL执行可选的索引只有主键索引,如果有多个,最终会选一个较优的作为检索的依据。 150 | 可以尝试在一个字段未建立索引时,根据该字段查询的效率,然后对该字段建立索引(alter table 表名 add index(字段名)),同样的SQL执行的效率,你会发现查询效率会有明显的提升(数据量越大越明显)。 151 | 152 | order by 153 | 154 | 当我们使用order by将查询结果按照某个字段排序时,如果该字段没有建立索引,那么执行计划会将查询出的所有数据使用外部排序(将数据从硬盘分批读取到内存使用内部排序,最后合并排序结果),这个操作是很影响性能的,因为需要将查询涉及到的所有数据从磁盘中读到内存(如果单条数据过大或者数据量过多都会降低效率),更无论读到内存之后的排序了。 155 | 但是如果我们对该字段建立索引alter table 表名 add index(字段名),那么由于索引本身是有序的,因此直接按照索引的顺序和映射关系逐条取出数据即可。而且如果分页的,那么只用取出索引表某个范围内的索引对应的数据,而不用像上述那取出所有数据进行排序再返回某个范围内的数据。(从磁盘取数据是最影响性能的 156 | 157 | join 158 | 159 | 对join语句匹配关系(on)涉及的字段建立索引能够提高效率 160 | 161 | 索引覆盖 162 | 163 | 如果要查询的字段都建立过索引,那么引擎会直接在索引表中查询而不会访问原始数据(否则只要有一个字段没有建立索引就会做全表扫描),这叫索引覆盖。因此我们需要尽可能的在select后只写必要的查询字段,以增加索引覆盖的几率。 164 | 这里值得注意的是不要想着为每个字段建立索引,因为优先使用索引的优势就在于其体积小。 165 | 166 | ## 8、索引的数据结构(b+树,hash) 167 | 168 | 索引的数据结构和具体存储引擎的实现有关,在MySQL中使用较多的索引有Hash索引,B+树索引等,而我们经常使用的InnoDB存储引擎的默认索引实现为:B+树索引。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。(这里的BTree是因为在innodb里显示的就是BTree,但其实是B+树) 169 | 170 | ### 1) B+树索引 171 | 172 | 查询方式: 173 | 174 | 主键索引区:PI(关联保存的时数据的地址)按主键查询 175 | 176 | 普通索引区:si(关联的id的地址,然后再到达上面的地址)。所以按主键查询,速度最快 177 | 178 | B+tree性质: 179 | 180 | 1.)n棵子tree的节点包含n个关键字,不用来保存数据而是保存数据的索引。 181 | 182 | 2.)所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。 183 | 184 | 3.)所有的非终端结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字。 185 | 186 | 4.)B+ 树中,数据对象的插入和删除仅在叶节点上进行。 187 | 188 | 5.)B+树有2个头指针,一个是树的根节点,一个是最小关键码的叶节点。 189 | 190 | ### 2) 哈希索引 191 | 192 | 简要说下,类似于数据结构中简单实现的HASH表(散列表)一样,当我们在mysql中用哈希索引时,主要就是通过Hash算法(常见的Hash算法有直接定址法、平方取中法、折叠法、除数取余法、随机数法),将数据库字段数据转换成定长的Hash值,与这条数据的行指针一并存入Hash表的对应位置;如果发生Hash碰撞(两个不同关键字的Hash值相同),则在对应Hash键下以链表形式存储。 193 | 194 | ### 区别: 195 | 196 | 1)Hash 索引仅仅能满足"=","IN"和"<=>"查询,不能使用范围查询。 197 | 198 | 由于 Hash 索引比较的是进行 Hash 运算之后的 Hash 值,所以它只能用于等值的过滤,不能用于基于范围的过滤,因为经过相应的 Hash 算法处理之后的 Hash 值的大小关系,并不能保证和Hash运算前完全一样。 199 | 200 | (2)Hash 索引无法被用来避免数据的排序操作。 201 | 202 | 由于 Hash 索引中存放的是经过 Hash 计算之后的 Hash 值,而且Hash值的大小关系并不一定和 Hash 运算前的键值完全一样,所以数据库无法利用索引的数据来避免任何排序运算; 203 | 204 | (3)Hash 索引不能利用部分索引键查询。 205 | 206 | 对于组合索引,Hash 索引在计算 Hash 值的时候是组合索引键合并后再一起计算 Hash 值,而不是单独计算 Hash 值,所以通过组合索引的前面一个或几个索引键进行查询的时候,Hash 索引也无法被利用。 207 | 208 | (4)Hash 索引在任何时候都不能避免表扫描。 209 | 210 | 前面已经知道,Hash 索引是将索引键通过 Hash 运算之后,将 Hash运算结果的 Hash 值和所对应的行指针信息存放于一个 Hash 表中,由于不同索引键存在相同 Hash 值,所以即使取满足某个 Hash 键值的数据的记录条数,也无法从 Hash 索引中直接完成查询,还是要通过访问表中的实际数据进行相应的比较,并得到相应的结果。 211 | 212 | (5)Hash 索引遇到大量Hash值相等的情况后性能并不一定就会比B-Tree索引高。 213 | 214 | 对于选择性比较低的索引键,如果创建 Hash 索引,那么将会存在大量记录指针信息存于同一个 Hash 值相关联。这样要定位某一条记录时就会非常麻烦,会浪费多次表数据的访问,而造成整体性能低下。 215 | 216 | ## 9、索引的设计原则? 217 | 218 | 1、适合索引的列是出现在where子句中的列,或者连接子句中指定的列 219 | 220 | 2、基数较小的类,索引效果较差,没有必要在此列建立索引 221 | 222 | 3、使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间 223 | 224 | 4、不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。 225 | 226 | ## 10、创建索引的原则 227 | 228 | 1) 最左前缀匹配原则,组合索引非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。 229 | 230 | 2)较频繁作为查询条件的字段才去创建索引 231 | 232 | 3)更新频繁字段不适合创建索引 233 | 234 | 4)若是不能有效区分数据的列不适合做索引列(如性别,男女未知,最多也就三种,区分度实在太低) 235 | 236 | 5)尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。 237 | 238 | 6)定义有外键的数据列一定要建立索引。 239 | 240 | 7)对于那些查询中很少涉及的列,重复值比较多的列不要建立索引。 241 | 242 | 8)对于定义为text、image和bit的数据类型的列不要建立索引。 243 | 244 | ## 11、创建索引的方式? 如何删除索引? 245 | 246 | ### 第一种方式:在执行CREATE TABLE时创建索引 247 | 248 | ### 第二种方式:使用ALTER TABLE命令去增加索引 249 | 250 | ALTER TABLE table_name ADD INDEX index_name (column_list); 251 | 252 | ALTER TABLE用来创建普通索引、UNIQUE索引或PRIMARY KEY索引。 253 | 254 | 其中table_name是要增加索引的表名,column_list指出对哪些列进行索引,多列时各列之间用逗号分隔。 255 | 256 | 索引名index_name可自己命名,缺省时,MySQL将根据第一个索引列赋一个名称。另外,ALTER TABLE允许在单个语句中更改多个表,因此可以在同时创建多个索引。 257 | 258 | ### 第三种方式:使用CREATE INDEX命令创建 259 | 260 | CREATE INDEX index_name ON table_name (column_list); 261 | 262 | CREATE INDEX可对表增加普通索引或UNIQUE索引。(但是,不能创建PRIMARY KEY索引) 263 | 264 | ### 如何删除索引? 265 | 266 | 根据索引名删除普通索引、唯一索引、全文索引:alter table 表名 drop KEY 索引名 267 | 268 | 删除主键索引:alter table 表名 drop primary key(因为主键只有一个)。这里值得注意的是,如果主键自增长,那么不能直接执行此操作(自增长依赖于主键索引)需要取消自增长再行删除:-- 重新定义字段 MODIFY id int, drop PRIMARY KEY 269 | 270 | 但通常不会删除主键,因为设计主键一定与业务逻辑无关。 271 | 272 | ## 11.创建索引时需要注意什么 273 | 274 | **非空字段**:应该指定列为NOT NULL,除非你想存储NULL。在mysql中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0、一个特殊的值或者一个空串代替空值; 275 | 276 | **取值离散大的字段**:(变量各个取值之间的差异程度)的列放到联合索引的前面,可以通过count()函数查看字段的差异值,返回值越大说明字段的唯一值越多字段的离散程度高; 277 | 278 | **索引字段越小越好**:数据库的数据存储以页为单位一页存储的数据越多一次IO操作获取的数据越大效率越高。 279 | 280 | ## 12、索引一定能提升查询的性能吗? 281 | 282 | 不是这样的,通常,通过索引查询数据比全表扫描要快。但是我们也必须注意到它的代价。 283 | 284 | 索引需要空间来存储,也需要定期维护, 每当有记录在表中增减或索引列被修改时,索引本身也会被修改。 这意味着每条记录的INSERT,DELETE,UPDATE将为此多付出4,5 次的磁盘I/O。 因为索引需要额外的存储空间和处理,那些不必要的索引反而会使查询反应时间变慢。使用索引查询不一定能提高查询性能,索引范围查询(INDEX RANGE SCAN)适用于两种情况: 285 | 286 | 基于一个范围的检索,一般查询返回结果集小于表中记录数的30% 287 | 288 | 基于非唯一性索引的检索 289 | 290 | ## 13、什么是最左前缀匹配? 291 | 292 | 顾名思义,就是最左优先,在创建多列索引时,要根据业务需求,where子句中使用最频繁的一列放在最左边。 293 | 294 | 最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。 295 | 296 | =和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式 297 | 298 | ## 14、B树和B+树的区别? 299 | 300 | 在B树中,你可以将键和值存放在内部节点和叶子节点;但在B+树中,内部节点都是键,没有值,叶子节点同时存放键和值。 301 | 302 | B+树的叶子节点有一条链相连,而B树的叶子节点各自独立。 303 | 304 | **使用B树的好处 305 | 306 | B树可以在内部节点同时存储键和值,因此,把频繁访问的数据放在靠近根节点的地方将会大大提高热点数据的查询效率。这种特性使得B树在特定数据重复多次查询的场景中更加高效。 307 | 308 | **使用B+树的好处 309 | 310 | 由于B+树的内部节点只存放键,不存放值,因此,一次读取,可以在内存页中获取更多的键,有利于更快地缩小查找范围。 B+树的叶节点由一条链相连,因此,当需要进行一次全数据遍历的时候,B+树只需要使用O(logN)时间找到最小的一个节点,然后通过链进行O(N)的顺序遍历即可。而B树则需要对树的每一层进行遍历,这会需要更多的内存置换次数,因此也就需要花费更多的时间 311 | 312 | 一颗m阶的B树定义如下: 313 | 314 | 1)每个结点最多有m-1个关键字。 315 | 316 | 2)根结点最少可以只有1个关键字。 317 | 318 | 3)非根结点至少有Math.ceil(m/2)-1个关键字。 319 | 320 | 4)每个结点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它。 321 | 322 | 5)所有叶子结点都位于同一层,或者说根结点到每个叶子结点的长度都相同。 323 | 324 | 除此之外B+树还有以下的要求。 325 | 326 | 1)B+树包含2种类型的结点:内部结点(也称索引结点)和叶子结点。根结点本身即可以是内部结点,也可以是叶子结点。根结点的关键字个数最少可以只有1个。 327 | 328 | 2)B+树与B树最大的不同是内部结点不保存数据,只用于索引,所有数据(或者说记录)都保存在叶子结点中。 329 | 330 | 3) m阶B+树表示了内部结点最多有m-1个关键字(或者说内部结点最多有m个子树),阶数m同时限制了叶子结点最多存储m-1个记录。 331 | 332 | 4)内部结点中的key都按照从小到大的顺序排列,对于内部结点中的一个key,左树中的所有key都小于它,右子树中的key都大于等于它。叶子结点中的记录也按照key的大小排列。 333 | 334 | 5)每个叶子结点都存有相邻叶子结点的指针,叶子结点本身依关键字的大小自小而大顺序链接。 335 | 336 | ## 15、数据库为什么使用B+树而不是B树 337 | 338 | B树只适合随机检索,而B+树同时支持随机检索和顺序检索; 339 | 340 | B+树空间利用率更高,可减少I/O次数,磁盘读写代价更低。一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗。B+树的内部结点并没有指向关键字具体信息的指针,只是作为索引使用,其内部结点比B树小,盘块能容纳的结点中关键字数量更多,一次性读入内存中可以查找的关键字也就越多,相对的,IO读写次数也就降低了。而IO读写次数是影响索引检索效率的最大因素; 341 | 342 | B+树的查询效率更加稳定。B树搜索有可能会在非叶子结点结束,越靠近根节点的记录查找时间越短,只要找到关键字即可确定记录的存在,其性能等价于在关键字全集内做一次二分查找。而在B+树中,顺序检索比较明显,随机检索时,任何关键字的查找都必须走一条从根节点到叶节点的路,所有关键字的查找路径长度相同,导致每一个关键字的查询效率相当。 343 | 344 | B-树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题。B+树的叶子节点使用指针顺序连接在一起,只要遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作。 345 | 346 | 增删文件(节点)时,效率更高。因为B+树的叶子节点包含所有关键字,并以有序的链表结构存储,这样可很好提高增删效率。 347 | 348 | ## 16、B+树在满足聚簇索引和覆盖索引的时候需不需要回表查询数据? 349 | 350 | 在B+树的索引中,叶子节点可能存储了当前的key值,也可能存储了当前的key值以及整行的数据,这就是聚簇索引和非聚簇索引。 在InnoDB中,只有主键索引是聚簇索引,如果没有主键,则挑选一个唯一键建立聚簇索引。如果没有唯一键,则隐式的生成一个键来建立聚簇索引。当查询使用聚簇索引时,在对应的叶子节点,可以获取到整行数据,因此不用再次进行回表查询。 351 | 352 | ## 17、什么是聚簇索引?何时使用聚簇索引与非聚簇索引 353 | 354 | 聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据 355 | 356 | 非聚簇索引:将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行,myisam通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因 357 | 358 | 澄清一个概念:innodb中,在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引,辅助索引叶子节点存储的不再是行的物理位置,而是主键值 359 | 360 | ## 18、联合索引是什么?为什么需要注意联合索引中的顺序? 361 | 362 | MySQL可以使用多个字段同时建立一个索引,叫做联合索引。在联合索引中,如果想要命中索引,需要按照建立索引时的字段顺序挨个使用,否则无法命中索引。 363 | 364 | 具体原因为: 365 | 366 | MySQL使用索引时需要索引有序,假设现在建立了"name,age,school"的联合索引,那么索引的排序为: 先按照name排序,如果name相同,则按照age排序,如果age的值也相等,则按照school进行排序。 367 | 368 | 当进行查询时,此时索引仅仅按照name严格有序,因此必须首先使用name字段进行等值查询,之后对于匹配到的列而言,其按照age字段严格有序,此时可以使用age字段用做索引查找,以此类推。因此在建立联合索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面。此外可以根据特例的查询或者表结构进行单独的调整。 369 | 370 | ## 19、什么是数据库事务? 371 | 事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。 372 | 373 | 事务最经典也经常被拿出来说例子就是转账了。 374 | 375 | 假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。 376 | 377 | ## 20、 事务的四大特性? 378 | 关系性数据库需要遵循ACID规则,具体内容如下: 379 | 380 | 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; 381 | 382 | 一致性: 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的; 383 | 384 | 隔离性: 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的; 385 | 386 | 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。 387 | 388 | ## 21、什么是脏读幻读不可重复读? 389 | 390 | 脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。 391 | 392 | 不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。 393 | 394 | 幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。 395 | 396 | ## 22、什么是事务的隔离级别? MySQL的默认隔离级别是什么? 397 | 398 | 为了达到事务的四大特性,数据库定义了4种不同的事务隔离级别,由低到高依次为Read uncommitted、Read committed、Repeatable read、Serializable,这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题。 399 | 400 | SQL 标准定义了四个隔离级别: 401 | 402 | READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。 403 | 404 | READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。 405 | 406 | REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。 407 | 408 | SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。 409 | 410 | 这里需要注意的是:Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别 411 | 412 | 事务隔离机制的实现基于锁机制和并发调度。其中并发调度使用的是MVVC(多版本并发控制),通过保存修改的旧版本信息来支持并发一致性读和回滚等特性。 413 | 414 | 因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容):,但是你要知道的是InnoDB 存储引擎默认使用 **REPEATABLE-READ(可重读)**并不会有任何性能损失。 415 | 416 | InnoDB 存储引擎在分布式事务的情况下一般会用到**SERIALIZABLE(可串行化)**隔离级别。 417 | 418 | ## 23、MySQL锁的问题 419 | 420 | ### 1、对MySQL的锁了解吗 421 | 422 | 当数据库有并发事务的时候,可能会产生数据的不一致,这时候需要一些机制来保证访问的次序,锁机制就是这样的一个机制。 423 | 424 | 就像酒店的房间,如果大家随意进出,就会出现多人抢夺同一个房间的情况,而在房间上装上锁,申请到钥匙的人才可以入住并且将房间锁起来,其他人只有等他使用完毕才可以再次使用。 425 | 426 | ### 2、隔离级别与锁的关系 427 | 428 | 在Read Uncommitted级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突 429 | 430 | 在Read Committed级别下,读操作需要加共享锁,但是在语句执行完以后释放共享锁; 431 | 432 | 在Repeatable Read级别下,读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁。 433 | 434 | SERIALIZABLE 是限制性最强的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成。 435 | 436 | ### 3、按照锁的粒度分数据库锁有哪些?锁机制与InnoDB锁算法 437 | 438 | 在关系型数据库中,可以按照锁的粒度把数据库锁分为行级锁(INNODB引擎)、表级锁(MYISAM引擎)和页级锁(BDB引擎 )。 439 | 440 | MyISAM和InnoDB存储引擎使用的锁: 441 | 442 | MyISAM采用表级锁(table-level locking)。 443 | 444 | InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁 445 | 446 | 行级锁,表级锁和页级锁对比 447 | 448 | **行级锁** 行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。 449 | 450 | 特点:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。 451 | 452 | **表级锁** 表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。 453 | 454 | 特点:开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。 455 | 456 | **页级锁** 页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。 457 | 458 | 特点:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般 459 | 460 | ### 4、从锁的类别上分MySQL都有哪些锁呢?像上面那样子进行锁定岂不是有点阻碍并发效率了 461 | 462 | 从锁的类别上来讲,有共享锁和排他锁。 463 | 464 | **共享锁**: 又叫做读锁。 当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。 465 | 466 | **排他锁**: 又叫做写锁。 当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。 467 | 468 | 用上面的例子来说就是用户的行为有两种,一种是来看房,多个用户一起看房是可以接受的。 一种是真正的入住一晚,在这期间,无论是想入住的还是想看房的都不可以。 469 | 470 | 锁的粒度取决于具体的存储引擎,InnoDB实现了行级锁,页级锁,表级锁。 471 | 472 | 他们的加锁开销从大到小,并发能力也是从大到小。 473 | 474 | ### 5、MySQL中InnoDB引擎的行锁是怎么实现的? 475 | 476 | 答:InnoDB是基于索引来完成行锁 477 | 478 | 例: select * from tab_with_index where id = 1 for update; 479 | 480 | for update 可以根据条件来完成行锁锁定,并且 id 是有索引键的列,如果 id 不是索引键那么InnoDB将完成表锁,并发将无从谈起 481 | 482 | InnoDB存储引擎的锁的算法有三种 483 | 484 | **Record lock:单个行记录上的锁 485 | 486 | **Gap lock:间隙锁,锁定一个范围,不包括记录本身 487 | 488 | **Next-key lock:record+gap 锁定一个范围,包含记录本身** 489 | 490 | 相关知识点: 491 | 492 | innodb对于行的查询使用next-key lock 493 | 494 | Next-locking keying为了解决Phantom Problem幻读问题 495 | 496 | 当查询的索引含有唯一属性时,将next-key lock降级为record key 497 | 498 | Gap锁设计的目的是为了阻止多个事务将记录插入到同一范围内,而这会导致幻读问题的产生 499 | 500 | 有两种方式显式关闭gap锁:(除了外键约束和唯一性检查外,其余情况仅使用record lock) 501 | 502 | A. 将事务隔离级别设置为RC 503 | 504 | B. 将参数innodb_locks_unsafe_for_binlog设置为1 505 | 506 | ### 6、什么是死锁?怎么解决? 507 | 508 | 死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。 509 | 510 | 常见的解决死锁的方法 511 | 512 | 1、如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。 513 | 514 | 2、在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率; 515 | 516 | 3、对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率; 517 | 518 | 如果业务处理不好可以用分布式事务锁或者使用乐观锁 519 | 520 | ### 7、数据库的乐观锁和悲观锁是什么?怎么实现的? 521 | 522 | 数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。 523 | 524 | 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制 525 | 526 | 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:一般会使用版本号机制或CAS算法实现。 527 | 528 | 两种锁的使用场景 529 | 530 | 从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。 531 | 532 | 但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。 533 | 534 | ## 24、超键、候选键、主键、外键分别是什么? 535 | 536 | **超键**:在关系中能唯一标识元组的属性集称为关系模式的超键。一个属性可以为作为一个超键,多个属性组合在一起也可以作为一个超键。超键包含候选键和主键。 537 | 538 | **候选键**:是最小超键,即没有冗余元素的超键。 539 | 540 | **主键**:数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。 541 | 542 | **外键**:在一个表中存在的另一个表的主键称此表的外键。 543 | 544 | ## 25、五种关联查询 545 | 546 | **交叉连接(CROSS JOIN)**笛卡尔积 547 | 548 | **内连接(INNER JOIN)**内连接分为三类 549 | 550 | 等值连接:ON A.id=B.id 551 | 552 | 不等值连接:ON A.id > B.id 553 | 554 | 自连接:SELECT * FROM A T1 INNER JOIN A T2 ON T1.id=T2.pid 555 | 556 | **外连接**(LEFT JOIN/RIGHT JOIN) 557 | 558 | 左外连接:LEFT OUTER JOIN, 以左表为主,先查询出左表,按照ON后的关联条件匹配右表,没有匹配到的用NULL填充,可以简写成LEFT JOIN 559 | 560 | 右外连接:RIGHT OUTER JOIN, 以右表为主,先查询出右表,按照ON后的关联条件匹配左表,没有匹配到的用NULL填充,可以简写成RIGHT JOIN 561 | 562 | **联合查询(UNION与UNION ALL)SELECT * FROM A UNION SELECT * FROM B UNION ...** 563 | 564 | 就是把多个结果集集中在一起,UNION前的结果为基准,需要注意的是联合查询的列数要相等,相同的记录行会合并 565 | 如果使用UNION ALL,不会合并重复的记录行 566 | 效率 UNION 高于 UNION ALL 567 | 568 | **全连接(FULL JOIN)** 569 | MySQL不支持全连接 570 | 可以使用LEFT JOIN 和UNION和RIGHT JOIN联合使用 571 | 572 | ## 26、varchar和char的区别? 573 | 574 | ### char的特点 575 | 576 | char表示定长字符串,长度是固定的; 577 | 578 | 如果插入数据的长度小于char的固定长度时,则用空格填充; 579 | 580 | 因为长度固定,所以存取速度要比varchar快很多,甚至能快50%,但正因为其长度固定,所以会占据多余的空间,是空间换时间的做法; 581 | 582 | 对于char来说,最多能存放的字符个数为255,和编码无关 583 | 584 | 585 | ### varchar的特点 586 | 587 | varchar表示可变长字符串,长度是可变的; 588 | 589 | 插入的数据是多长,就按照多长来存储; 590 | 591 | varchar在存取方面与char相反,它存取慢,因为长度不固定,但正因如此,不占据多余的空间,是时间换空间的做法; 592 | 593 | 对于varchar来说,最多能存放的字符个数为65532 594 | 595 | 总之,结合性能角度(char更快)和节省磁盘空间角度(varchar更小),具体情况还需具体来设计数据库才是妥当的做法。 596 | 597 | ## 27、如何定位及优化SQL语句的性能问题?创建的索引有没有被使用到?或者说怎么才可以知道这条语句运行很慢的原因? 598 | 599 | 对于低性能的SQL语句的定位,最重要也是最有效的方法就是使用执行计划,MySQL提供了explain命令来查看语句的执行计划。 我们知道,不管是哪种数据库,或者是哪种数据库引擎,在对一条SQL语句进行执行的过程中都会做很多相关的优化,对于查询语句,最重要的优化方式就是使用索引。 而执行计划,就是显示数据库引擎对于SQL语句的执行的详细情况,其中包含了是否使用索引,使用什么索引,使用的索引的相关信息等。 600 | 601 | 执行计划包含的信息 id 有一组数字组成。表示一个查询中各个子查询的执行顺序; 602 | 603 | id相同执行顺序由上至下。 604 | 605 | id不同,id值越大优先级越高,越先被执行。 606 | 607 | id为null时表示一个结果集,不需要使用它查询,常出现在包含union等查询语句中。 608 | 609 | select_type 每个子查询的查询类型,一些常见的查询类型。 610 | 611 | table 查询的数据表,当从衍生表中查数据时会显示 x 表示对应的执行计划id partitions 表分区、表创建的时候可以指定通过那个列进行表分区。 612 | 613 | + type(非常重要,可以看到有没有走索引) 访问类型 614 | + ALL 扫描全表数据 615 | + index 遍历索引 616 | + range 索引范围查找 617 | + index_subquery 在子查询中使用 ref 618 | + unique_subquery 在子查询中使用 eq_ref 619 | + ref_or_null 对Null进行索引的优化的 ref 620 | + fulltext 使用全文索引 621 | + ref 使用非唯一索引查找数据 622 | + eq_ref 在join查询中使用PRIMARY KEYorUNIQUE NOT NULL索引关联。 623 | 624 | possible_keys 可能使用的索引,注意不一定会使用。查询涉及到的字段上若存在索引,则该索引将被列出来。当该列为 NULL时就要考虑当前的SQL是否需要优化了。 625 | 626 | key 显示MySQL在查询中实际使用的索引,若没有使用索引,显示为NULL。 627 | 628 | TIPS:查询中若使用了覆盖索引(覆盖索引:索引的数据覆盖了需要查询的所有数据),则该索引仅出现在key列表中 629 | 630 | key_length 索引长度 631 | 632 | ref 表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值 633 | 634 | rows 返回估算的结果集数目,并不是一个准确的值。 635 | 636 | extra 的信息非常丰富,常见的有: 637 | + Using index 使用覆盖索引 638 | + Using where 使用了用where子句来过滤结果集 639 | + Using filesort 使用文件排序,使用非索引列进行排序时出现,非常消耗性能,尽量优化。 640 | + Using temporary 使用了临时表 sql优化的目标可以参考阿里开发手册 641 | 642 | ## 28、数据库优化问题 643 | 644 | ### 为什么要优化 645 | 646 | 系统的吞吐量瓶颈往往出现在数据库的访问速度上 647 | 648 | 随着应用程序的运行,数据库的中的数据会越来越多,处理时间会相应变慢 649 | 650 | 数据是存放在磁盘上的,读写速度无法和内存相比 651 | 652 | 优化原则:减少系统瓶颈,减少资源占用,增加系统的反应速度。 653 | 654 | ### 数据库结构优化 655 | 656 | 一个好的数据库设计方案对于数据库的性能往往会起到事半功倍的效果。 657 | 658 | 需要考虑数据冗余、查询和更新的速度、字段的数据类型是否合理等多方面的内容。 659 | 660 | 将字段很多的表分解成多个表 661 | 662 | 对于字段较多的表,如果有些字段的使用频率很低,可以将这些字段分离出来形成新表。 663 | 664 | 因为当一个表的数据量很大时,会由于使用频率低的字段的存在而变慢。 665 | 666 | ### 增加中间表 667 | 668 | 对于需要经常联合查询的表,可以建立中间表以提高查询效率。 669 | 670 | 通过建立中间表,将需要通过联合查询的数据插入到中间表中,然后将原来的联合查询改为对中间表的查询。 671 | 672 | ### 增加冗余字段 673 | 674 | 设计数据表时应尽量遵循范式理论的规约,尽可能的减少冗余字段,让数据库设计看起来精致、优雅。但是,合理的加入冗余字段可以提高查询速度。 675 | 表的规范化程度越高,表和表之间的关系越多,需要连接查询的情况也就越多,性能也就越差。 676 | 677 | 注意: 678 | 679 | 冗余字段的值在一个表中修改了,就要想办法在其他表中更新,否则就会导致数据不一致的问题。 680 | 681 | ### 29、MySQL的复制原理以及流程? 682 | 683 | **主从复制**:将主数据库中的DDL和DML操作通过二进制日志(BINLOG)传输到从数据库上,然后将这些日志重新执行(重做);从而使得从数据库的数据与主数据库保持一致。 684 | 685 | ### 主从复制的作用 686 | 687 | 主数据库出现问题,可以切换到从数据库。 688 | 689 | 可以进行数据库层面的读写分离。 690 | 691 | 可以在从数据库上进行日常备份。 692 | 693 | 694 | ### MySQL主从复制解决的问题 695 | 696 | 数据分布:随意开始或停止复制,并在不同地理位置分布数据备份 697 | 698 | 负载均衡:降低单个服务器的压力 699 | 700 | 高可用和故障切换:帮助应用程序避免单点失败 701 | 702 | 升级测试:可以用更高版本的MySQL作为从库 703 | 704 | ### MySQL主从复制工作原理 705 | 706 | 在主库上把数据更高记录到二进制日志 707 | 708 | 从库将主库的日志复制到自己的中继日志 709 | 710 | 从库读取中继日志的事件,将其重放到从库数据中 711 | 712 | 基本原理流程,3个线程以及之间的关联 713 | 714 | 主:binlog线程——记录下所有改变了数据库数据的语句,放进master上的binlog中; 715 | 716 | 从:io线程——在使用start slave 之后,负责从master上拉取 binlog 内容,放进自己的relay log中; 717 | 718 | 从:sql执行线程——执行relay log中的语句; 719 | 720 | 复制过程: 721 | 722 | ![Image text](https://mubu.com/document_image/9859ece3-5a81-4b79-a82e-04fb5266a46e-4943374.jpg) 723 | 724 | Binary log:主数据库的二进制日志 725 | 726 | Relay log:从服务器的中继日志 727 | 728 | 第一步:master在每个事务更新数据完成之前,将该操作记录串行地写入到binlog文件中。 729 | 730 | 第二步:salve开启一个I/O Thread,该线程在master打开一个普通连接,主要工作是binlog dump process。如果读取的进度已经跟上了master,就进入睡眠状态并等待master产生新的事件。I/O线程最终的目的是将这些事件写入到中继日志中。 731 | 732 | 第三步:SQL Thread会读取中继日志,并顺序执行该日志中的SQL事件,从而与主数据库中的数据保持一致。 733 | 734 | ## 30、读写分离有哪些解决方案? 735 | 736 | 读写分离是依赖于主从复制,而主从复制又是为读写分离服务的。因为主从复制要求slave不能写只能读(如果对slave执行写操作,那么show slave status将会呈现Slave_SQL_Running=NO,此时你需要按照前面提到的手动同步一下slave)。 737 | 738 | 方案一 739 | 740 | 使用mysql-proxy代理 741 | 742 | 优点:直接实现读写分离和负载均衡,不用修改代码,master和slave用一样的帐号,mysql官方不建议实际生产中使用 743 | 744 | 缺点:降低性能, 不支持事务 745 | 746 | 方案二 747 | 748 | 使用AbstractRoutingDataSource+aop+annotation在dao层决定数据源。 749 | 750 | 如果采用了mybatis, 可以将读写分离放在ORM层,比如mybatis可以通过mybatis plugin拦截sql语句,所有的insert/update/delete都访问master库,所有的select 都访问salve库,这样对于dao层都是透明。 plugin实现时可以通过注解或者分析语句是读写方法来选定主从库。不过这样依然有一个问题, 也就是不支持事务, 所以我们还需要重写一下DataSourceTransactionManager, 将read-only的事务扔进读库, 其余的有读有写的扔进写库。 751 | 752 | 方案三 753 | 754 | 使用AbstractRoutingDataSource+aop+annotation在service层决定数据源,可以支持事务. 755 | 756 | 缺点:类内部方法通过this.xx()方式相互调用时,aop不会进行拦截,需进行特殊处理。 757 | 758 | ## 31、MySQL的事务实现原理? 759 | 760 | 实现事务功能的三种技术主要是依靠**redo log**和**undo log**,锁技术和**MVCC**实现。 761 | 762 | ### redo log 763 | 764 | redo log叫做重做日志,是用来实现事务的持久性。该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都会存到该日志中。 765 | 766 | mysql 为了提升性能不会把每次的修改都实时同步到磁盘,而是会先存到Buffer Pool(缓冲池)里头,把这个当作缓存来用。然后使用后台线程去做缓冲池和磁盘之间的同步。 767 | 768 | 那么问题来了,如果还没来的同步的时候宕机或断电了怎么办?还没来得及执行同步的操作。这样会导致丢部分已提交事务的修改信息! 769 | 770 | 所以引入了redo log来记录已成功提交事务的修改信息,并且会把redo log持久化到磁盘,系统重启之后在读取redo log恢复最新数据。 771 | 772 | ### undo log 773 | 774 | undo log 叫做回滚日志,用于记录数据被修改前的信息。他正好跟前面所说的重做日志所记录的相反,重做日志记录数据被修改后的信息。undo log主要记录的是数据的逻辑变化,为了在发生错误时回滚之前的操作,需要将之前的操作都记录下来,然后在发生错误时才可以回滚。 775 | 776 | undo log 记录事务修改之前版本的数据信息,因此假如由于系统错误或者rollback操作而回滚的话可以根据undo log的信息来进行回滚到没被修改前的状态。 777 | 778 | 所以,引入undo log是用来回滚数据用于保障未提交事务的原子性。 779 | 780 | ### MVCC 781 | 782 | MVCC (MultiVersion Concurrency Control) 叫做多版本并发控制。 783 | 784 | InnoDB的 MVCC ,是通过在每行记录的后面保存两个隐藏的列来实现的。这两个列, 一个保存了行的创建时间,一个保存了行的过期时间, 当然存储的并不是实际的时间值,而是系统版本号。 785 | 786 | MVCC在mysql中的实现依赖的是undo log与read view 787 | 788 | + undo log :undo log 中记录某行数据的多个版本的数据。 789 | 790 | + read view :用来判断当前版本数据的可见性 791 | 792 | ### 引申问题:什么是read view? 793 | 794 | 涉及到了MySQL版本链,对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列( row_id并不是必要的,我们 795 | 创建的表中有主键或者非NULL唯一键时都不会包含row_id列): 796 | 797 | **trx_id**:每次对某条记录进行改动时,都会把对应的事务id赋值给trx_id隐藏列。 798 | 799 | **roll_pointer**:每次对某条记录进行改动时,这个隐藏列会存一个指针,可以通过这个指针找到该记 800 | 录修改前的信息  801 | 802 | 这里大家可以看一下[Read View原理](https://blog.csdn.net/nmjhehe/article/details/98470570) 803 | -------------------------------------------------------------------------------- /Collections/bingfa.md: -------------------------------------------------------------------------------- 1 | # 多线程与并发 2 | 3 | ## 1、Thread中start()和run()的区别 4 | 5 | 调用start方法会创建一个新的线程并启动 6 | 7 | 调用run方法只是一个普通方法的调用,在原线程中启动 8 | 9 | 只有调用start()方法才是多线程的启用,start()方法内会调用run()方法,如果只是单纯的调用run()方法,并不能达到多线程的效果,依然是单线程的。 10 | 11 | ## 2、线程的返回值有几种方式? 12 | 13 | 14 | 1、主线程等待法 15 | 16 | 主线程循环等待,直到目标子线程返回为止 17 | 18 | 缺点:第一、是需要自己实现循环等待的逻辑,等待的线程或者成员变量多了,代码就会显得异常臃肿。第二、需要等待多久是不确定的,无法精准控制 19 | 20 | 2、使用Thread类的join()方法阻塞当前线程以等待子线程处理完毕 21 | 缺点:不能精准控制每个线程的进度 22 | 23 | 3、通过Callable接口实现:通过FutureTask或者线程池获取 24 | 25 | ## 3、线程有几种状态? 26 | 27 | 1、新建(new)创建后未启动 28 | 29 | 2、运行(Runnable):包含Running和Ready 30 | 31 | 3、无限期等待(waiting):不会被分配cpu执行时间,需要显式被唤醒。使用未设置参数的Object.wait()、使用未设置参数的Thread.join()、LockSupport.park()会使线程进入无限期等待 32 | 33 | 4、限期等待:一定时间后会自动唤醒 34 | 35 | Thread.sleep()、设置了参数的Object.wait()、设置了参数的Thread.join()方法、LockSupport.parkNanos()方法、LockSupport.parkUntil()方法 36 | 37 | 5、阻塞状态(Blocked):等待获取排他锁 38 | 39 | 6、结束状态 40 | 41 | ## 4、锁池和等待池是什么? 42 | 43 | **锁池**:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。 44 | 45 | **等待池**:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中。 46 | 47 | 假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池. 48 | 49 | ## 5、Java中多线程的实现方式? 50 | 51 | 1:继承Thread并重写run方法,并调用start方法 52 | 53 | 2:实现Runnable接口,并用其初始化Thread,然后创建Thread实例,并调用start方法 54 | 55 | 3:实现Callable接口,并用其初始化Thread,然后创建Thread实例,并调用start方法 56 | 57 | 4:使用线程池创建 58 | 59 | ## 6、 notify和notifyall的区别? 60 | 61 | 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。 62 | 63 | 当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争 64 | 65 | 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。 66 | 67 | ## 7、什么是线程ThreadLocal?作用是什么? 68 | 69 | 定义:线程局部变量是局限于线程内的变量,属于线程自身所有,不在多个线程间共享。java提供ThreadLocal类来支持线程局部变量,是一个实现线程安全的方式。任何线程局部变量一旦在工作完成后没有释放,java应用就存在内存泄露的风险 70 | 71 | 作用:ThreadLocal是一种以空间换时间的做法,在每一个Thread里面维护了一个ThreadLocal.ThreadLocalMap把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。 72 | 73 | 但同时由于ThreadLocal内部是依赖ThreadLocalMap实现,但key是弱引用而value是强引用,所以存在内存泄漏的问题。 74 | 75 | 76 | ## 8、synchronized相关面试题(这部分建议阅读《Java并发编程的艺术》) 77 | 78 | 79 | ### 1、作用? 80 | 81 | 用来控制线程同步,即多线程环境下,控制synchronized代码段不被多个线程同时执行。可以用于修饰类,方法,变量。对于普通同步方法,锁住的是实例对象。对于静态同步方法,锁住的是类的class对象。对于代码块,锁住的是括号里的代码 82 | 83 | ### 2、synchronized锁升级过程?对象头里变化? 84 | 85 | 在JDK1.5以后,为了优化synchronized锁,所以新增了synchronized锁的升级过程,避免一上来就是重量级锁。 86 | 87 | 升级过程为: 88 | 89 | 自旋锁(CAS):让不满足条件的线程等待一会看能不能获得锁,通过占用处理器的时间来避免线程切换带来的开销。自旋等待的时间或次数是有一个限度的,如果自旋超过了定义的时间仍然没有获取到锁,则该线程应该被挂起。在 JDK1.6 之后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。 90 | 91 | 偏向锁:大多数情况下,锁总是由同一个线程多次获得。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,偏向锁是一个可重入的锁。如果锁对象头的Mark Word里存储着指向当前线程的偏向锁,无需重新进行CAS操作来加锁和解锁。当有其他线程尝试竞争偏向锁时,持有偏向锁的线程(不处于活动状态)才会释放锁。偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定进而升级为轻量级锁。 92 | 93 | 轻量级锁:减少无实际竞争情况下,使用重量级锁产生的性能消耗。JVM会现在当前线程的栈桢中创建用于存储锁记录的空间 LockRecord,将对象头中的 Mark Word 复制到 LockRecord 中并将 LockRecord 中的 Owner 指针指向锁对象。然后线程会尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针,成功则当前线程获取到锁,失败则表示其他线程竞争锁当前线程则尝试使用自旋的方式获取锁。自旋获取锁失败则锁膨胀升级为重量级锁。 94 | 95 | 重量级锁:通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实 现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。线程竞争不使用自旋,不会消耗CPU。但是线程会进入阻塞等待被其他线程被唤醒,响应时间缓慢。 96 | 97 | 这幅图可以比较详解的概括锁的体系 98 | 99 | ![Image text](https://img-blog.csdn.net/20180908110545722?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2JhaWR1XzM4MDgzNjE5/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) 100 | 101 | ### 3、怎么使用syn的? 102 | 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 103 | 104 | 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。 105 | 106 | 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 107 | 108 | ### 4、syn实现原理? 109 | 通过JDK反编译指令javap -c可以看到在执行同步代码块之前之后都有一个monitor字样,其中前面的是monitorenter,后面的是离开monitorexit,不难想象一个线程也执行同步代码块,首先要获取锁,而获取锁的过程就是monitorenter ,在执行完代码块之后,要释放锁,释放锁就是执行monitorexit指令。 110 | 111 | ### 5、为什么会有两个monitorexit呢? 112 | 113 | 这个主要是防止在同步代码块中线程因异常退出,而锁没有得到释放,这必然会造成死锁(等待的线程永远获取不到锁)。因此最后一个monitorexit是保证在异常情况下,锁也可以得到释放,避免死锁。仅有ACC_SYNCHRONIZED这么一个标志,该标记表明线程进入该方法时,需要monitorenter,退出该方法时需要monitorexit。 114 | 115 | ### 6、synchronized可重入的原理 116 | 117 | 重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。 118 | 119 | ### 7、syn和lock()有什么区别? 120 | 121 | 1.synchronized是Java内置关键字,在JVM层面,Lock是一个Java类 122 | 123 | 2.synchronized可以给类,方法,代码块加锁,而lock只能给代码块加锁 124 | 125 | 3.synchronized不需要手动获取锁和释放锁,而lock需要手动加锁和释放锁 126 | 127 | 4.通过lock可以知道有没有成功获取锁,但synchronized不可以。 128 | 129 | ### 8、synchronized和reentrantlock的区别? 130 | 131 | 相同之处是都是可重入锁。synchronized是一个关键字,而后者是一个类,类比关键字有更高的灵活性,因为可以被继承,可以有方法。主要区别如下: 132 | ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁; 133 | ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。 134 | 135 | 二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark wordJava中每一个对象都可以作为锁,这是synchronized实现同步的基础: 136 | 普通同步方法,锁是当前实例对象静态同步方法,锁是当前类的class对象同步方法块,锁是括号里面的对象 137 | 138 | ### 9、什么是不可变对象?对于并发的帮助? 139 | 140 | 不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。 141 | 142 | 不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类,如 String、基本类型的包装类、BigInteger 和 BigDecimal 等。 143 | 144 | 只有满足如下状态,一个对象才是不可变的; 145 | 146 | 它的状态不能在创建后再被修改; 147 | 148 | 所有域都是 final 类型;并且,它被正确创建(创建期间没有发生 this 引用的逸出)。 149 | 150 | 不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率 151 | 152 | ## 9、volatile相关 153 | 154 | ### 1、内存屏障 155 | 156 | 为了实现volatile的内存语义,编译器在生成字节码的时候,会在指令序列中插入内存屏障来禁止特定的处理器重排序,在volitale写之前和之后都增加StoreStore内存屏障,禁止上面的普通写和下面的volitale写重排序,用于保证程序执行的正确性,再追求效率。 157 | 158 | ### 2、volatile如何保证可见性的? 159 | 160 | 当volatile进行写操作时,进入汇编代码可以看到,有volatile变量修饰的共享变量进行写操作的时候会多出一行带有lock前缀指令的汇编代码。这行指令做了如下两件事:1.将当前处理器的缓存行刷回到系统内存中2.这个刷回内存的操作会使在其他CPU里缓存了该地址的数据无效为了提高处理速度,处理器不直接和内存通信,而是先将系统内存的数据读到内部缓存后再操作,但操作完不知道何时才会写到内存。JVM就会发送lock前缀指令,将数据刷到系统内存中。但是,就算处理器缓存行前的内容刷回到内存中,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会执行缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来判断自己的缓存的数据是不是过期了,如果发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读取到处理器缓存中。 161 | 162 | ### 3、volatile关键字作用? 163 | 164 | 对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。 165 | 从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。 166 | volatile 常用于多线程环境下的单次操作(单次读或者单次写)。 167 | 168 | ### 4、synchronized 和 volatile 的区别是什么? 169 | 170 | synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。 171 | 172 | volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。 173 | 174 | **区别** 175 | 176 | volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。 177 | 178 | volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。 179 | 180 | volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。 181 | 182 | volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。 183 | 184 | volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些 185 | 186 | ## 10、Java内存模型相关问题 187 | 188 | ### 1、Java代码为什么会重排序? 189 | 190 | 在执行程序的时候。JVM为了更好的性能保证,有时会对指令进行重排序,但重排序是简历在单线程环境下运行结果和原运行结果一直,并且存在数据依赖关系的指令不能重排序。但这样在会破坏多线程中的执行语义。重排序分为编译器重排序和处理器重排序,重排序会导致多线程程序出现内存可见性问题。对于编译器重排序,JMM的编译器重排序规则会禁止特定类型的编译器重排序。对于处理器重排序,JMM的处理器重排序规则会要求编译器在生成指令序列时,插入特定类型的内存屏障来禁止特定类型的处理器重排序 191 | 192 | ### 2、as-if-serial规则和happens-before规则的区别 193 | 194 | as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。 195 | 196 | 197 | as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。 198 | 199 | as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度 200 | 201 | ### 3、happens-before原则 202 | 203 | Happens-Before规则Happens-Before的八个规则(摘自《深入理解Java虚拟机》12.3.6章节): 204 | 205 | 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作; 206 | 207 | 管程锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;(此处后面指时间的先后) 208 | 209 | volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;(此处后面指时间的先后) 210 | 211 | 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作; 212 | 213 | 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行; 214 | 215 | 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生; 216 | 217 | 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始; 218 | 219 | 传递性:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C; 220 | 221 | ## 11、并发容器相关问题 222 | 223 | ### 1、ConcurrentHashMap(1.8) 224 | 225 | ConcurrentHashMap是Java中的一个线程安全且高效的HashMap实现。平时涉及高并发如果要用map结构,那第一时间想到的就是它。相对于hashmap来说,ConcurrentHashMap就是线程安全的map,其中利用了锁分段的思想提高了并发度。 226 | 那么它到底是如何实现线程安全的? 227 | 228 | JDK 1.6版本关键要素: 229 | 230 | segment继承了ReentrantLock充当锁的角色,为每一个segment提供了线程安全的保障; 231 | 232 | segment维护了哈希散列表的若干个桶,每个桶由HashEntry构成的链表。 233 | 234 | JDK1.8后,ConcurrentHashMap抛弃了原有的Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。 235 | 236 | ### 2、什么是并发容器? 237 | 238 | 何为同步容器:可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如 Vector,Hashtable,以及 Collections.synchronizedSet,synchronizedList 等方法返回的容器。可以通过查看 Vector,Hashtable 等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字 synchronized。 239 | 240 | 并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问 map,同时允许一定数量的写操作线程并发地修改 map,所以它可以在并发环境下实现更高的吞吐量。 241 | 242 | ### 3、什么是CopyOnWriteArrayList? 243 | 244 | 是一个并发容器,在非复合场景下是线程安全的。在CopyOnWriteArrayList中,写的操作会额外复制底层数组进行操作,也就不影响了原数组的读操作,但这样有明显的弊端,如果写操作增多,需要拷贝数组,会消耗内存,情况严重时会导致gc, 虽然可以进行读操作,但是不能保证读操作的实时性,因为新增删除元素都需要时间,可能读取的是旧的数据,虽然结果会保持一致,但无法保证时效性。同时,每次写都要复制的代价,如果数组很长,则代价非常高。 245 | 246 | ## 12、阻塞队列相关 247 | 248 | 阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。 249 | 250 | 这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。 251 | 252 | 阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。 253 | 254 | ## 13、并行和并发相关 255 | 256 | ### 1、并发三要素 257 | 258 | 原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。 259 | 260 | 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile) 261 | 262 | 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序 263 | 264 | ### 2、并发和并行的区别 265 | 266 | 并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。 267 | 268 | 并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。 269 | 270 | ### 3、Java中怎么保证多线程的安全? 271 | 272 | Automic类解决原子性问题,scnchronized解决可见性问题,Happens-Before决定有序性 273 | 274 | ## 14、多线程相关 275 | 276 | ### 1、进程和线程的区别和概念 277 | 278 | 进程 279 | 280 | 一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。 281 | 282 | 线程 283 | 284 | 285 | 进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位 286 | 287 | 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。 288 | 289 | 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。 290 | 291 | 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的 292 | 293 | 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。 294 | 295 | 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行 296 | 297 | ### 2、什么是上下文切换 298 | 299 | 当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。 300 | 301 | ### 3、普通线程和守护线程区别 302 | 303 | 用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程 304 | 305 | 守护 (Daemon) 线程:运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作 306 | 307 | ### 4、什么是死锁,死锁形成条件?如何避免? 308 | 309 | 死锁:指两个或者多个线程在执行过程中,由于竞争资源而产生的一个如果没有外力作用就会一直阻塞下去的现象。 310 | 311 | 四个必要条件: 312 | 313 | 互斥条件:线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程) 314 | 315 | 释放请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放。 316 | 317 | 不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 318 | 319 | 循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞 320 | 321 | 如何避免死锁:破坏其中之一就可以 322 | 323 | ### 5、创建线程四种方式 324 | 325 | 1.继承Thread类 326 | 327 | 2.继承runnable接口 328 | 329 | 3.继承callable接口 330 | 331 | 4.通过Executors工具类来创建线程池 332 | 333 | ### 6、runnable和callable的区别 334 | 335 | 相同点: 336 | 337 | 1.都是接口 338 | 339 | 2.都可以编写多线程程序 340 | 341 | 3.都通过Thread.start()方法来启动线程 342 | 343 | 不同点: 344 | 345 | 1.runnable接口是没有返回值的,但Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。 346 | 347 | 2.runnable接口run方法只能抛出运行时异常,而且无法捕获。callable接口call方法允许抛出异常,可以获取异常信息。 348 | 349 | run和start的区别 350 | 351 | 每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。start()方法用于启动线程,run()方法用于执行线程的运行时代码,run()方法可以重复调用,start()方法只可以调用一次。如果直接调用run方法,run方法将只会作为线程里的一个普通函数,必须等待run方法执行完毕后,才能执行后续的代码,不符合多线程的特征,而start方法就不需要等待run执行完毕,就可以继续执行其他的代码。同时start只是将线程的状态改为就绪状态,并且会由此Thread类调用方法run来完成其运行状态 352 | 353 | ### 7、callable和futureTask,future 354 | 355 | Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。 356 | Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说 Callable用于产生结果,Future 用于获取结果。 357 | FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。 358 | 359 | ### 8、线程生命周期及状态 360 | 361 | 362 | 新建(new):新创建了一个线程对象。 363 | 364 | 可运行(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。 365 | 366 | 运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中; 367 | 368 | 阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。 369 | 370 | 阻塞的情况分三种:(一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;(二). 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;(三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。 371 | 372 | 死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。 373 | 374 | ### 9、线程同步以及线程调度相关方法 375 | 376 | (1) wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁; 377 | 378 | (2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常; 379 | 380 | (3)notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关; 381 | 382 | (4)notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态; 383 | 384 | ### 10、sleep和wait有什么区别? 385 | 386 | sleep是Thread线程类的静态方法, wait是Object类的方法。 387 | 388 | sleep是不释放锁的, wait会释放锁 389 | 390 | sleep可以在任何地方使用, wait只能在同步方法或者同步代码块中使用 391 | 392 | wait() 被调用后,需要别的线程调用同一个对象上的notify或者NotifyAll方法来唤醒, sleep在睡眠时间到了之后会自动苏醒。 393 | 394 | ### 11、为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里 395 | 396 | Java中,任何对象都可以作为锁,并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。 397 | 398 | ### 12、为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用? 399 | 400 | 当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。 401 | 402 | ### 13、sleep和yield方法的区别? 403 | 404 | (1) sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会; 405 | 406 | (2) 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态; 407 | 408 | (3)sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常; 409 | 410 | (4)sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。 411 | 412 | ### 14、如何终止一个线程? 413 | 414 | 1.(不推荐)使用stop()或者suspend()方法强行停止,现已过时,因为很可能会导致死锁现象。 415 | 416 | 2、使用退出标志,也就是当run方法完成线程后终止。 417 | 418 | 3、使用interrupt方法捕获异常来中断线程。 419 | 420 | ### 15、Java 中 interrupted 和 isInterrupted 方法的区别? 421 | 422 | 1.interrupt用于中断线程,调用该方法的线程的状态将被置于中断状态此时并不会直接中断线程,只是置线程的中断状态位。 423 | 424 | 2.interrupted 是静态方法,查看当前中断信号是true还是false并清除中断信号,如果一个线程被中断了,第一次调用interrupted会返回true,第二次会返回false。 425 | 426 | 3.isInterrupted 查看当前中断信号是true还是false 427 | 428 | ### 16、notify和notifyAll的区别 429 | 430 | 前者是随机唤醒一个线程,后者是唤醒所有线程。notifyAll( )唤醒后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等到锁被释放后再次参与竞争。 431 | 而notify只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。 432 | 433 | ### 17、Java如何实现多线程之间的通信和协作 434 | 435 | 可以通过终端和共享变量的方式实现线程间的通信。Java中最常见的线程通信协作的方式有两种: 436 | synchronized加锁的线程的Object类的wait()方法 437 | reentrantLock类加锁的线程的Condition类的await()方法线程之间的直接数据交换1.通过管道进行线程间的通信: 字节流和字符流 438 | 439 | ### 18、什么是线程同步和线程互斥,有哪几种实现方式? 440 | 441 | 当一个线程对共享的数据进行操作时,应使之成为一个”原子操作“,即在没有完成相关操作之前,不允许其他线程打断它,否则,就会破坏数据的完整性,必然会得到错误的处理结果,这就是线程的同步。 442 | 在多线程应用中,考虑不同线程之间的数据同步和防止死锁。当两个或多个线程之间同时等待对方释放资源的时候就会形成线程之间的死锁。为了防止死锁的发生,需要通过同步来实现线程安全。 443 | 线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。 444 | 线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。 445 | 446 | ### 19、实现线程同步的方法 447 | 448 | 同步代码方法:sychronized 关键字修饰的方法 449 | 450 | 同步代码块:sychronized 关键字修饰的代码块 451 | 452 | 使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制 453 | 454 | 使用重入锁实现线程同步:reentrantlock类是可冲入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义 455 | 456 | ### 20、什么叫线程安全? 457 | 458 | 线程安全是编程中的术语,指某个方法在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。通俗的说如果程序在单线程和多线程的情况下运行结果总是一致的,则该程序是线程安全的。 459 | 460 | ### 21、手写单例模式 461 | 462 | 这个可以参照我的博客 一般情况下有五种单例模式 463 | https://blog.csdn.net/Vince_Wang1/article/details/104264534 464 | 465 | ### 22、线程数过多会产生什么异常? 466 | 467 | 1.线程的生命周期开销非常高 468 | 469 | 2.消耗过多的CPU资源 470 | 471 | 3.降低JVM的稳定性,可能会抛出OOM异常 472 | 473 | ## 15、Lock相关 474 | 475 | ### 1、乐观锁悲观锁?实现方式? 476 | 477 | 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。 478 | 479 | 乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。 480 | 481 | ### 2、乐观锁的实现方式: 482 | 483 | 1、使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。 484 | 485 | 2、java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置值更新为新值 B。否则处理器不做任何操作。 486 | 487 | ## 16、什么是CAS 488 | 489 | CAS 是 compare and swap 的缩写,即我们所说的比较交换。 490 | 491 | CAS 是一种基于锁的操作,而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加 version 来获取数据,性能较悲观锁有很大的提高。 492 | 493 | CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和 A 的值是一样的,那么就将内存里面的值更新成 B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。 494 | 495 | CAS 主要分三步,读取-比较-修改。其中比较是在检测是否有冲突,如果检测到没有冲突后,其他线程还能修改这个值,那么 CAS 还是无法保证正确性。所以最关键的是要保证比较-修改这两步操作的原子性。 496 | 497 | CAS 底层是靠调用 CPU 指令集的 cmpxchg 完成的,它是 x86 和 Intel 架构中的 compare and exchange 指令。在多核的情况下,这个指令也不能保证原子性,需要在前面加上 lock 指令。lock 指令可以保证一个 CPU 核心在操作期间独占一片内存区域。那么 这又是如何实现的呢? 498 | 在处理器中,一般有两种方式来实现上述效果:总线锁和缓存锁。在多核处理器的结构中,CPU 核心并不能直接访问内存,而是统一通过一条总线访问。总线锁就是锁住这条总线,使其他核心无法访问内存。这种方式代价太大了,会导致其他核心停止工作。而缓存锁并不锁定总线,只是锁定某部分内存区域。当一个 CPU 核心将内存区域的数据读取到自己的缓存区后,它会锁定缓存对应的内存区域。锁住期间,其他核心无法操作这块内存区域。 499 | 500 | ABA问题 501 | CAS 保证了比较和交换的原子性。但是从读取到开始比较这段期间,其他核心仍然是可以修改这个值的。如果核心将 A 修改为 B,CAS 可以判断出来。但是如果核心将 A 修改为 B 再修改回 A。那么 CAS 会认为这个值并没有被改变,从而继续操作。这是和实际情况不符的。解决方案是加一个版本号。从 Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。 502 | 503 | ## 17、死锁与活锁的区别,死锁与饥饿的区别? 504 | 505 | 死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。 506 | 507 | 活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。 508 | 509 | 活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。 510 | 511 | 饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。 512 | 513 | ## 18、Java中导致饥饿的原因 514 | 515 | 1、高优先级线程吞噬所有的低优先级线程的 CPU 时间。 516 | 517 | 2、线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。 518 | 519 | 3、线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。 520 | 521 | ## 19、AQS相关 522 | 523 | ### 1、什么是AQS 524 | 525 | AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。 526 | 527 | ### 2、AQS原理? 528 | 529 | 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。 530 | 531 | AQS对资源有独占和共享两种资源共享方式,同时AQS采用了模板方法模式,自定义同步器的时候需要重写以下方法:tryAcquire(int) 独占方式。尝试获取资源,成功则返回true,失败则返回false。tryRelease(int) 独占方式。尝试释放资源,成功则返回true,失败则返回false。tryAcquireShared(int) 共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。tryReleaseShared(int) 共享方式。尝试释放资源,成功则返回true,失败则返回false。 532 | 533 | 以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。这和synchronized中的monitor计数器类似。所以都能保证锁的可重入性。 534 | 535 | ## 20、什么是可重入锁? 536 | 537 | ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。 538 | 539 | 在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。 540 | 要想支持重入性,就要解决两个问题: 541 | 542 | 1. 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功; 543 | 544 | 2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。 545 | 546 | ## ReadWriteLock是什么 547 | 548 | ReadWriteLock 是一个读写锁接口,读写锁是用来提升并发程序性能的锁分离技术,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。 549 | 550 | 而读写锁有以下三个重要的特性: 551 | 552 | (1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。 553 | 554 | (2)重进入:读锁和写锁都支持线程重进入。 555 | 556 | (3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。 557 | 558 | ## 22、线程池相关 559 | 560 | ### 1、常见的四种线程池 561 | 562 | (1)newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。 563 | 564 | (2)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能。 565 | 566 | (3) newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。 567 | 568 | (4)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。 569 | 570 | ### 2、线程池优点 571 | 572 | 降低资源消耗:重用存在的线程,减少对象创建销毁的开销。 573 | 574 | 提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 575 | 576 | 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 577 | 578 | ### 3、线程池状态 579 | 580 | RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。 581 | 582 | SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。 583 | 584 | STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。 585 | 586 | TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。 587 | 588 | TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。 589 | 590 | ### 4、线程池中 submit() 和 execute() 方法有什么区别? 591 | 592 | 接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。 593 | 594 | 返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有 595 | 596 | 异常处理:submit()方便Exception处理 597 | 598 | ### 5、Executors和ThreaPoolExecutor创建线程池的区别 599 | 600 | Executors 各个方法的弊端: 601 | 602 | newFixedThreadPool 和 newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。 603 | 604 | newCachedThreadPool 和 newScheduledThreadPool:主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。 605 | 606 | ### 6、线程池七大参数 607 | 608 | corePoolSize :核心线程数,线程数定义了最小可以同时运行的线程数量。 609 | 610 | maximumPoolSize :线程池中允许存在的工作线程的最大数量 611 | 612 | workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中。 613 | 614 | keepAliveTime:线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁; 615 | 616 | unit :keepAliveTime 参数的时间单位。 617 | 618 | threadFactory:为线程池提供创建新线程的线程工厂 619 | 620 | handler :线程池任务队列超过 maxinumPoolSize 之后的拒绝策略 621 | 622 | ### 7、四大拒绝策略 623 | 624 | ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。 625 | 626 | ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 627 | 628 | ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。 629 | 630 | ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。 631 | 632 | ## 23、并发工具相关 633 | 634 | ### 1、在 Java 中 CycliBarriar 和 CountdownLatch 有什么区别? 635 | 636 | CountDownLatch与CyclicBarrier都是用于控制并发的工具类,都可以理解成维护的就是一个计数器,但是这两者还是各有不同侧重点的: 637 | 638 | CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行; 639 | 640 | 而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行; 641 | 642 | CountDownLatch强调一个线程等多个线程完成某件事情。 643 | 644 | CyclicBarrier是多个线程互等,等大家都完成,再携手共进。 645 | 646 | 调用CountDownLatch的countDown方法后,当前线程并不会阻塞,会继续往下执行; 647 | 648 | 而调用CyclicBarrier的await方法,会阻塞当前线程,直到CyclicBarrier指定的线程全部都到达了指定点的时候,才能继续往下执行; 649 | 650 | CountDownLatch方法比较少,操作比较简单,而CyclicBarrier提供的方法更多,比如能够通过getNumberWaiting(),isBroken()这些方法获取当前多个线程的状态,并且CyclicBarrier的构造方法可以传入barrierAction,指定当所有线程都到达时执行的业务功能; 651 | 652 | CountDownLatch是不能复用的,而CyclicLatch是可以复用的。 653 | 654 | ### 2、Semaphore 有什么作用 655 | 656 | Semaphore 就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个 int 型整数 n,表示某段代码最多只有 n 个线程可以访问,如果超出了 n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果 Semaphore 构造函数中传入的 int 型整数 n=1,相当于变成了一个 synchronized 了。 657 | 658 | ### 3、什么是线程间交换数据的工具Exchanger 659 | 660 | Exchanger是一个用于线程间协作的工具类,用于两个线程间交换数据。它提供了一个交换的同步点,在这个同步点两个线程能够交换数据。交换数据是通过exchange方法来实现的,如果一个线程先执行exchange方法,那么它会同步等待另一个线程也执行exchange方法,这个时候两个线程就都达到了同步点,两个线程就可以交换数据。 661 | 662 | ### 4、常用的并发工具类? 663 | 664 | 665 | Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。 666 | 667 | CountDownLatch(倒计时器): CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。 668 | 669 | CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。 670 | 671 | ## 24、synchronized和lock的区别 672 | 673 | 1.首先synchronized是java内置关键字,在jvm层面,Lock是个java类; 674 | 675 | 2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁; 676 | 677 | 3.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁; 678 | 679 | 4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了; 680 | 681 | 5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可) 682 | 683 | 6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。 684 | -------------------------------------------------------------------------------- /Collections/collections.md: -------------------------------------------------------------------------------- 1 | # 集合容器 2 | 3 | ## 1、什么是集合 4 | 5 | 用于存储数据的容器。集合框架是为表示和操作集合而规定的一种统一的标准的体系结构。 6 | 7 | ## 2、集合的特点 8 | 9 | 集合的特点主要有如下两点: 10 | 11 | 对象封装数据,对象多了也需要存储。集合用于存储对象。 12 | 13 | 对象的个数确定可以使用数组,对象的个数不确定的可以用集合。因为集合是可变长度的。 14 | 15 | ## 3、集合和数组的区别? 16 | 17 | 数组是固定长度的;集合可变长度的。 18 | 19 | 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。 20 | 21 | 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。 22 | 23 | 数据结构:就是容器中存储数据的方式。 24 | 25 | 对于集合容器,有很多种。因为每一个容器的自身特点不同,其实原理在于每个容器的内部数据结构不同。 26 | 27 | 集合容器在不断向上抽取过程中,出现了集合体系。在使用一个体系的原则:参阅顶层内容。建立底层对象。 28 | 29 | ## 4、使用集合框架的好处 30 | 31 | 容量自增长; 32 | 33 | 提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量; 34 | 35 | 允许不同 API 之间的互操作,API之间可以来回传递集合; 36 | 37 | 可以方便地扩展或改写集合,提高代码复用性和可操作性。 38 | 39 | 通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。 40 | 41 | ## 5、常用的集合类有哪些? 42 | 43 | Map接口和Collection接口是所有集合框架的父接口: 44 | 45 | Collection接口的子接口包括:Set接口和List接口 46 | 47 | Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等 48 | 49 | Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等 50 | 51 | List接口的实现类主要有:ArrayList、LinkedList以及Vector等 52 | 53 | ## 6、List,Set,Map三者的区别?List、Set、Map 是否继承自 Collection 接口?List、Map、Set 三个接口存取元素时,各有什么特点? 54 | 55 | Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口。 56 | 57 | Collection集合主要有List和Set两大接口 58 | 59 | **List**:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。 60 | 61 | **Set**:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。 62 | 63 | Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。 64 | 65 | Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap 66 | 67 | ## 7、集合框架底层数据结构 68 | 69 | ### Collection 70 | 71 | #### List 72 | 73 | Arraylist: Object数组 74 | 75 | Vector: Object数组 76 | 77 | LinkedList: 双向循环链表 78 | 79 | #### Set 80 | 81 | HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素 82 | 83 | LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。 84 | 85 | TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。) 86 | 87 | #### Map 88 | 89 | HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间 90 | 91 | LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。 92 | 93 | HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的 94 | 95 | TreeMap: 红黑树(自平衡的排序二叉树) 96 | 97 | ## 8、哪些集合类是线程安全的? 98 | 99 | vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。 100 | 101 | stack:堆栈类,先进后出。 102 | 103 | hashtable:就比hashmap多了个线程安全。 104 | 105 | concurrentHashMap:Jdk1.8以后通过cas + synchronized的方式保证线程安全 106 | 107 | enumeration:枚举,相当于迭代器。 108 | 109 | ## 9、什么是fastfail机制? 110 | 111 | 是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。 112 | 113 | 例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。 114 | 115 | 原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hasNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。 116 | 117 | ## 10、什么是Iterator迭代器? 118 | 119 | Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。 120 | 121 | ## 11、迭代器如何使用?有什么特点? 122 | 123 | ```java 124 | List list = new ArrayList<>(); 125 | Iterator it = list. iterator(); 126 | while(it. hasNext()){ 127 | String obj = it. next(); 128 | System. out. println(obj); 129 | } 130 | ``` 131 | iterator的特点是只能单向遍历,它可以保证在集合结构被修改的时候抛出ConcurrentModificationException异常,更加安全 132 | 133 | ## 12、如何边遍历边移除 Collection 中的元素? 134 | 135 | 可以调用iterator.remove()方法 136 | 137 | ## 13、说一下 ArrayList 的优缺点?适用场景? 138 | 139 | ArrayList的优点如下: 140 | 141 | ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。 142 | 143 | ArrayList 在顺序添加一个元素的时候非常方便。 144 | 145 | ArrayList 的缺点如下: 146 | 147 | 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。 148 | 149 | 插入元素的时候,也需要做一次元素复制操作,缺点同上。 150 | 151 | ArrayList 比较适合顺序添加、随机访问的场景。 152 | 153 | ## 14、遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么? 154 | 155 | 遍历方式有以下几种: 156 | 157 | for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。 158 | 159 | 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。 160 | 161 | foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。 162 | 163 | 最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。 164 | 165 | 如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。 166 | 167 | 如果没有实现该接口,表示不支持 Random Access,如LinkedList。 168 | 169 | 推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历。 170 | 171 | ## 15、如何实现数组和List之间的切换 172 | 173 | 数组转 List:使用 Arrays. asList(array) 进行转换。 174 | List 转数组:使用 List 自带的 toArray() 方法。 175 | 176 | ## 16、ArrayList 和 LinkedList 的区别是什么? 177 | 178 | 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。 179 | 180 | 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。 181 | 182 | 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。 183 | 184 | 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。 185 | 186 | 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全; 187 | 188 | 同时数组内存一定是连续的内存区域,链表可能是不连续的内存,这点要涉及到CPU层面 189 | 190 | 从CPU的角度 191 | 192 | CPU 寄存器 – immediate access (0-1个CPU时钟周期)  193 | 194 | CPU L1 缓存 – fast access (3个CPU时钟周期)  195 | 196 | CPU L2 缓存 – slightly slower access (10个CPU时钟周期)  197 | 198 | 内存 (RAM) – slow access (100个CPU时钟周期)  199 | 200 | 硬盘 (file system) – very slow (10,000,000个CPU时钟周期) 201 | 202 | CPU的缓存会把一片连续的内存空间写入,因为数组是连续的内存地址,所以数组全部或者部分会被直接写入CPU的缓存中,平均读取时间只要3个CPU时钟周期,而链表的节点分布在堆里,则只能去读内存,速度要慢很多,所以数组的访问速度比链表快很多。 203 | 204 | 综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。 205 | 206 | ## 17、ArrayList 和 Vector 的区别是什么? 207 | 208 | 这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合 209 | 210 | 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。 211 | 212 | 性能:ArrayList 在性能方面要优于 Vector。 213 | 214 | 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。 215 | 216 | Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。 217 | 218 | Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。 219 | 220 | ## 18、为什么 ArrayList 的 elementData 加上 transient 修饰? 221 | 222 | ArrayList 中的数组定义如下: 223 | 224 | private transient Object[] elementData; 225 | 226 | 再看一下 ArrayList 的定义: 227 | 228 | public class ArrayList extends AbstractList 229 | 230 | implements List, RandomAccess, Cloneable, java.io.Serializable 231 | 232 | 233 | 可以看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化,重写了 writeObject 实现: 234 | 235 | ```java 236 | private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ 237 | // Write out element count, and any hidden stuff* 238 | int expectedModCount = modCount; 239 | s.defaultWriteObject(); 240 | *// Write out array length* 241 | s.writeInt(elementData.length); 242 | *// Write out all elements in the proper order.* 243 | for (int i=0; i>>16进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标index = (table.length - 1) & hash,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。 322 | 323 | 流程图 324 | ![Image text](https://mubu.com/document_image/b0cc966d-66f1-4d1c-9474-c3e5bf7b7eff-4943374.jpg) 325 | ①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容; 326 | 327 | ②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③; 328 | 329 | ③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals; 330 | 331 | ④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤; 332 | 333 | ⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可; 334 | 335 | ⑥.插入成功后,判断实际存在的键值对数量size是否超过了最大容量threshold,如果超过,进行扩容。 336 | 337 | **TIPS:hashmap强烈建议读源码,算是比较简单的源码了,读懂一部分的话应付面试也够了** 338 | 常规的面试题就不贴在这了,大家想必都烂熟于心 339 | 340 | ## 28、为什么HashMap中String、Integer这样的包装类适合作为Key? 341 | 342 | String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率 343 | 344 | 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况 345 | 346 | 内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况; 347 | 348 | ## 29、 HashMap的长度为什么是2的幂次方 349 | 350 | 为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。 351 | 352 | ![Image text](https://mubu.com/document_image/6d1e7c81-b384-44ea-ba68-d7a2383217cc-4943374.jpg) 353 | 354 | ## 30、HashMap 与 HashTable 有什么区别? 355 | 356 | 1、线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!); 357 | 358 | 2、效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它; 359 | 360 | 3、对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。 361 | 362 | 4、初始容量大小和每次扩充容量大小的不同 : ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小。 363 | 364 | 5、底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。 365 | 366 | 6、推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。 367 | 368 | ## 31、HashMap 和 ConcurrentHashMap 的区别 369 | 370 | ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。) 371 | 372 | HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。 373 | 374 | ## 32、ConcurrentHashMap 和 Hashtable 的区别? 375 | 376 | ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。 377 | 378 | 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 的底层数据结构都是采用 数组+链表 的形式,数组是主体,链表则是主要为了解决哈希冲突而存在的; 379 | 实现线程安全的方式(重要): 380 | 381 | ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本; 382 | 383 | ② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 384 | 385 | 答:ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题。但是 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。 386 | 387 | ## 33、ConcurrentHashMap 底层具体实现知道吗?实现原理是什么? 388 | 389 | ### JDK1.7 390 | 391 | 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 392 | 393 | 在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下: 394 | 395 | 一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。 396 | 397 | ![Image text](https://mubu.com/document_image/7e86fd77-b255-4af3-a476-d8fd9344cb1d-4943374.jpg) 398 | 399 | 该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色; 400 | 401 | Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。 402 | 403 | ### JDK1.8 404 | 405 | ![Image text](https://mubu.com/document_image/889ec623-0e2a-45e3-9d1f-4b0670179aea-4943374.jpg) 406 | 407 | 这部分可以看源码具体concurrenthashmap是如何保证线程安全的 408 | 409 | -------------------------------------------------------------------------------- /Collections/cs.md: -------------------------------------------------------------------------------- 1 | # 操作系统 2 | 3 | ## 1.进程的常见状态?以及各种状态之间的转换条件? 4 | 5 | - 就绪:进程已处于准备好运行的状态,即进程已分配到除CPU外的所有必要资源后,只要再获得CPU,便可立即执行。 6 | - 执行:进程已经获得CPU,程序正在执行状态。 7 | - 阻塞:正在执行的进程由于发生某事件(如I/O请求、申请缓冲区失败等)暂时无法继续执行的状态。 8 | 9 | ## 2.进程同步 10 | 11 | - 进程同步的主要任务:是对多个相关进程在执行次序上进行协调,以使并发执行的诸进程之间能有效地共享资源和相互合作,从而使程序的执行具有可再现性。 12 | - 同步机制遵循的原则: 13 | - (1)空闲让进; 14 | - (2)忙则等待(保证对临界区的互斥访问); 15 | - (3)有限等待(有限代表有限的时间,避免死等); 16 | - (4)让权等待,(当进程不能进入自己的临界区时,应该释放处理机,以免陷入忙等状态)。 17 | 18 | ## 3.进程的通信方式有哪些? 19 | 20 | - 进程通信,是指进程之间的信息交换(信息量少则一个状态或数值,多者则是成千上万个字节)。因此,对于用信号量进行的进程间的互斥和同步,由于其所交换的信息量少而被归结为低级通信。 21 | 22 | - 所谓高级进程通信指:用户可以利用操作系统所提供的一组通信命令传送大量数据的一种通信方式。操作系统隐藏了进程通信的实现细节。或者说,通信过程对用户是透明的。 23 | 24 | ### 高级通信机制可归结为三大类: 25 | 26 | - (1)共享存储器系统(存储器中划分的共享存储区);实际操作中对应的是“剪贴板”(剪贴板实际上是系统维护管理的一块内存区域)的通信方式,比如举例如下:word进程按下ctrl+c,在ppt进程按下ctrl+v,即完成了word进程和ppt进程之间的通信,复制时将数据放入到剪贴板,粘贴时从剪贴板中取出数据,然后显示在ppt窗口上。 27 | 28 | - (2)消息传递系统(进程间的数据交换以消息(message)为单位,当今最流行的微内核操作系统中,微内核与服务器之间的通信,无一例外地都采用了消息传递机制。应用举例:邮槽(MailSlot)是基于广播通信体系设计出来的,它采用无连接的不可靠的数据传输。邮槽是一种单向通信机制,创建邮槽的服务器进程读取数据,打开邮槽的客户机进程写入数据。 29 | 30 | - (3)管道通信系统(管道即:连接读写进程以实现他们之间通信的共享文件(pipe文件,类似先进先出的队列,由一个进程写,另一进程读))。实际操作中,管道分为:匿名管道、命名管道。匿名管道是一个未命名的、单向管道,通过父进程和一个子进程之间传输数据。匿名管道只能实现本地机器上两个进程之间的通信,而不能实现跨网络的通信。命名管道不仅可以在本机上实现两个进程间的通信,还可以跨网络实现两个进程间的通信。 31 | 32 | - 管道:管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。写进程在管道的尾端写入数据,读进程在管道的道端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数据。管道提供了简单的流控制机制。进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样地,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。 33 | 34 | - 注1:无名管道只能实现父子或者兄弟进程之间的通信,有名管道(FIFO)可以实现互不相关的两个进程之间的通信。 35 | - 注2:用FIFO让一个服务器和多个客户端进行交流时候,每个客户在向服务器发送信息前建立自己的读管道,或者让服务器在得到数据后再建立管道。使用客户的进程号(pid)作为管道名是一种常用的方法。客户可以先把自己的进程号告诉服务器,然后再到那个以自己进程号命名的管道中读取回复。 36 | 37 | - 信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其它进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 38 | 39 | - 消息队列:是一个在系统内核中用来保存消 息的队列,它在系统内核中是以消息链表的形式出现的。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 40 | 41 | - 共享内存:共享内存允许两个或多个进程访问同一个逻辑内存。这一段内存可以被两个或两个以上的进程映射至自身的地址空间中,一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取读出,从而实现了进程间的通信。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。共享内存是最快的IPC方式,它是针对其它进程间通信方式运行效率低而专门设计的。它往往与其它通信机制(如信号量)配合使用,来实现进程间的同步和通信。 42 | 43 | - 套接字:套接字也是一种进程间通信机制,与其它通信机制不同的是,它可用于不同机器间的进程通信。 44 | 45 | ## 4.上下文切换 46 | 47 | - 对于单核单线程CPU而言,在某一时刻只能执行一条CPU指令。上下文切换(Context Switch)是一种将CPU资源从一个进程分配给另一个进程的机制。从用户角度看,计算机能够并行运行多个进程,这恰恰是操作系统通过快速上下文切换造成的结果。在切换的过程中,操作系统需要先存储当前进程的状态(包括内存空间的指针,当前执行完的指令等等),再读入下一个进程的状态,然后执行此进程。 48 | 49 | ## 5.进程与线程的区别和联系? 50 | 51 | - 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。 52 | - 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。 53 | - 进程和线程的关系 54 | - (1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。 55 | - (2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。 56 | - (3)处理机分给线程,即真正在处理机上运行的是线程。 57 | - (4)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。 58 | - 进程与线程的区别? 59 | - (1)进程有自己的独立地址空间,线程没有 60 | - (2)进程是资源分配的最小单位,线程是CPU调度的最小单位 61 | - (3)进程和线程通信方式不同(线程之间的通信比较方便。同一进程下的线程共享数据(比如全局变量,静态变量),通过这些数据来通信不仅快捷而且方便,当然如何处理好这些访问的同步与互斥正是编写多线程程序的难点。而进程之间的通信只能通过进程通信的方式进行。) 62 | - (4)进程上下文切换开销大,线程开销小 63 | - (5)一个进程挂掉了不会影响其他进程,而线程挂掉了会影响其他线程 64 | - (6)对进程进程操作一般开销都比较大,对线程开销就小了 65 | 66 | ## 6、为什么进程上下文切换比线程上下文切换代价高? 67 | 68 | ### 进程切换分两步: 69 | 70 | - 1.切换页目录以使用新的地址空间 71 | 72 | - 2.切换内核栈和硬件上下文 73 | 74 | - 对于linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。 75 | 76 | ### 切换的性能消耗: 77 | 78 | - 1、线程上下文切换和进程上下问切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。 79 | 80 | - 2、另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor's Translation Lookaside Buffer (TLB))或者相当的神马东西会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。 81 | 82 | - 首先来一句概括的总论:进程和线程都是一个时间段的描述,是CPU工作时间段的描述。 83 | 84 | - 下面细说背景: 85 | 86 | - CPU+RAM+各种资源(比如显卡,光驱,键盘,GPS, 等等外设)构成我们的电脑,但是电脑的运行,实际就是CPU和相关寄存器以及RAM之间的事情。 87 | 88 | - 一个最最基础的事实:CPU太快,太快,太快了,寄存器仅仅能够追的上他的脚步,RAM和别的挂在各总线上的设备完全是望其项背。那当多个任务要执行的时候怎么办呢?轮流着来?或者谁优先级高谁来?不管怎么样的策略,一句话就是在CPU看来就是轮流着来。 89 | 90 | - 一个必须知道的事实:执行一段程序代码,实现一个功能的过程介绍 ,当得到CPU的时候,相关的资源必须也已经就位,就是显卡啊,GPS啊什么的必须就位,然后CPU开始执行。这里除了CPU以外所有的就构成了这个程序的执行环境,也就是我们所定义的程序上下文。当这个程序执行完了,或者分配给他的CPU执行时间用完了,那它就要被切换出去,等待下一次CPU的临幸。在被切换出去的最后一步工作就是保存程序上下文,因为这个是下次他被CPU临幸的运行环境,必须保存。 91 | 92 | - 串联起来的事实:前面讲过在CPU看来所有的任务都是一个一个的轮流执行的,具体的轮流方法就是:先加载程序A的上下文,然后开始执行A,保存程序A的上下文,调入下一个要执行的程序B的程序上下文,然后开始执行B,保存程序B的上下文。。。。========= 重要的东西出现了========进程和线程就是这样的背景出来的,两个名词不过是对应的CPU时间段的描述,名词就是这样的功能。 93 | 94 | - 进程就是包换上下文切换的程序执行时间总和 = CPU加载上下文+CPU执行+CPU保存上下文 95 | 96 | - 线程是什么呢?进程的颗粒度太大,每次都要有上下的调入,保存,调出。如果我们把进程比喻为一个运行在电脑上的软件,那么一个软件的执行不可能是一条逻辑执行的,必定有多个分支和多个程序段,就好比要实现程序A,实际分成 a,b,c等多个块组合而成。那么这里具体的执行就可能变成: 97 | 98 | - 程序A得到CPU =》CPU加载上下文,开始执行程序A的a小段,然后执行A的b小段,然后再执行A的c小段,最后CPU保存A的上下文。 99 | 100 | - 这里a,b,c的执行是共享了A的上下文,CPU在执行的时候没有进行上下文切换的。这里的a,b,c就是线程,也就是说线程是共享了进程的上下文环境,的更为细小的CPU时间段。到此全文结束,再一个总结: 101 | 102 | - 进程和线程都是一个时间段的描述,是CPU工作时间段的描述,不过是颗粒大小不同。 103 | 104 | - 进程(process)与线程(thread)最大的区别是进程拥有自己的地址空间,某进程内的线程对于其他进程不可见,即进程A不能通过传地址的方式直接读写进程B的存储区域。进程之间的通信需要通过进程间通信(Inter-process communication,IPC)。与之相对的,同一进程的各线程间之间可以直接通过传递地址或全局变量的方式传递信息。 105 | 106 | - 进程作为操作系统中拥有资源和独立调度的基本单位,可以拥有多个线程。通常操作系统中运行的一个程序就对应一个进程。在同一进程中,线程的切换不会引起进程切换。在不同进程中进行线程切换,如从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换。相比进程切换,线程切换的开销要小很多。线程于进程相互结合能够提高系统的运行效率。 107 | 108 | ### 线程可以分为两类: 109 | 110 | - 用户级线程(user level thread):对于这类线程,有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在。在应用程序启动后,操作系统分配给该程序一个进程号,以及其对应的内存空间等资源。应用程序通常先在一个线程中运行,该线程被成为主线程。在其运行的某个时刻,可以通过调用线程库中的函数创建一个在相同进程中运行的新线程。用户级线程的好处是非常高效,不需要进入内核空间,但并发效率不高。 111 | 112 | - 内核级线程(kernel level thread):对于这类线程,有关线程管理的所有工作由内核完成,应用程序没有进行线程管理的代码,只能调用内核线程的接口。内核维护进程及其内部的每个线程,调度也由内核基于线程架构完成。内核级线程的好处是,内核可以将不同线程更好地分配到不同的CPU,以实现真正的并行计算。 113 | 114 | - 事实上,在现代操作系统中,往往使用组合方式实现多线程,即线程创建完全在用户空间中完成,并且一个应用程序中的多个用户级线程被映射到一些内核级线程上,相当于是一种折中方案。 115 | 116 | ## 6.进程调度 117 | 118 | ### 调度种类 119 | 120 | - 高级调度:(High-Level Scheduling)又称为作业调度,它决定把后备作业调入内存运行; 121 | - 低级调度:(Low-Level Scheduling)又称为进程调度,它决定把就绪队列的某进程获得CPU; 122 | - 中级调度:(Intermediate-Level Scheduling)又称为在虚拟存储器中引入,在内、外存对换区进行进程对换。 123 | - 调度策略的设计 124 | - 响应时间: 从用户输入到产生反应的时间 125 | - 周转时间: 从任务开始到任务结束的时间 126 | - CPU任务可以分为交互式任务和批处理任务,调度最终的目标是合理的使用CPU,使得交互式任务的响应时间尽可能短,用户不至于感到延迟,同时使得批处理任务的周转时间尽可能短,减少用户等待的时间。 127 | 128 | - 非抢占式调度与抢占式调度 129 | 130 | - 非抢占式:分派程序一旦把处理机分配给某进程后便让它一直运行下去,直到进程完成或发生进程调度进程调度某事件而阻塞时,才把处理机分配给另一个进程。 131 | - 抢占式:操作系统将正在运行的进程强行暂停,由调度程序将CPU分配给其他就绪进程的调度方式。 132 | 133 | ### 调度算法: 134 | 135 | - Shortest Job First (SJF) 136 | 137 | - 最短的作业(CPU区间长度最小)最先调度。 138 | - SJF可以保证最小的平均等待时间。 139 | 140 | - FIFO或First Come, First Served (FCFS)先来先服务 141 | 142 | - 调度的顺序就是任务到达就绪队列的顺序。 143 | - 公平、简单(FIFO队列)、非抢占、不适合交互式。 144 | - 未考虑任务特性,平均等待时间可以缩短。 145 | 146 | - Shortest Remaining Job First (SRJF) 147 | 148 | - SJF的可抢占版本,比SJF更有优势。 149 | - SJF(SRJF): 如何知道下一CPU区间大小?根据历史进行预测: 指数平均法。 150 | 151 | - 优先权调度 152 | 153 | - 每个任务关联一个优先权,调度优先权最高的任务。 154 | - 注意:优先权太低的任务一直就绪,得不到运行,出现“饥饿”现象。 155 | 156 | - Round-Robin(RR)轮转调度算法 157 | 158 | - 设置一个时间片,按时间片来轮转调度(“轮叫”算法) 159 | - 优点: 定时有响应,等待时间较短;缺点: 上下文切换次数较多; 160 | - 时间片太大,响应时间太长;吞吐量变小,周转时间变长;当时间片过长时,退化为FCFS。 161 | 162 | - 多级队列调度 163 | 164 | - 按照一定的规则建立多个进程队列 165 | - 不同的队列有固定的优先级(高优先级有抢占权) 166 | - 不同的队列可以给不同的时间片和采用不同的调度方法 167 | - 存在问题1:没法区分I/O bound和CPU bound; 168 | - 存在问题2:也存在一定程度的“饥饿”现象; 169 | 170 | - 多级反馈队列 171 | 172 | - 在多级队列的基础上,任务可以在队列之间移动,更细致的区分任务。 173 | - 可以根据“享用”CPU时间多少来移动队列,阻止“饥饿”。 174 | - 最通用的调度算法,多数OS都使用该方法或其变形,如UNIX、Windows等。 175 | 176 | - 多级反馈队列调度算法描述: 177 | 178 | ![img](https://img.mubu.com/document_image/30fe8344-f678-4fe8-b4fe-cdb15590f491-4943374.jpg) 179 | 180 | - 进程在进入待调度的队列等待时,首先进入优先级最高的Q1等待。 181 | 182 | - 首先调度优先级高的队列中的进程。若高优先级中队列中已没有调度的进程,则调度次优先级队列中的进程。例如:Q1,Q2,Q3三个队列,只有在Q1中没有进程等待时才去调度Q2,同理,只有Q1,Q2都为空时才会去调度Q3。 183 | 184 | - 对于同一个队列中的各个进程,按照时间片轮转法调度。比如Q1队列的时间片为N,那么Q1中的作业在经历了N个时间片后若还没有完成,则进入Q2队列等待,若Q2的时间片用完后作业还不能完成,一直进入下一级队列,直至完成。 185 | 186 | - 在低优先级的队列中的进程在运行时,又有新到达的作业,那么在运行完这个时间片后,CPU马上分配给新到达的作业(抢占式)。 187 | 188 | - 一个简单的例子 189 | 190 | - 假设系统中有3个反馈队列Q1,Q2,Q3,时间片分别为2,4,8。现在有3个作业J1,J2,J3分别在时间 0 ,1,3时刻到达。而它们所需要的CPU时间分别是3,2,1个时间片。 191 | 192 | - 时刻0 J1到达。 于是进入到队列1 ,运行1个时间片 ,时间片还未到,此时J2到达。 193 | 194 | - 时刻1 J2到达。 由于时间片仍然由J1掌控,于是等待。J1在运行了1个时间片后,已经完成了在Q1中的2个时间片的限制,于是J1置于Q2等待被调度。现在处理机分配给J2。 195 | 196 | - 时刻2 J1进入Q2等待调度,J2获得CPU开始运行。 197 | 198 | - 时刻3 J3到达,由于J2的时间片未到,故J3在Q1等待调度,J1也在Q2等待调度。 199 | 200 | - 时刻4 J2处理完成,由于J3,J1都在等待调度,但是J3所在的队列比J1所在的队列的优先级要高,于是J3被调度,J1继续在Q2等待。 201 | 202 | - 时刻5 J3经过1个时间片,完成。 203 | 204 | - 时刻6 由于Q1已经空闲,于是开始调度Q2中的作业,则J1得到处理器开始运行。 J1再经过一个时间片,完成了任务。于是整个调度过程结束。 205 | 206 | ## 7.死锁的条件?以及如何处理死锁问题? 207 | 208 | - 定义:如果一组进程中的每一个进程都在等待仅由该组进程中的其他进程才能引发的事件,那么该组进程就是死锁的。或者在两个或多个并发进程中,如果每个进程持有某种资源而又都等待别的进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗地讲,就是两个或多个进程被无限期地阻塞、相互等待的一种状态。 209 | 210 | ### 产生死锁的必要条件: 211 | 212 | - 互斥条件(Mutual exclusion):资源不能被共享,只能由一个进程使用。 213 | - 请求与保持条件(Hold and wait):已经得到资源的进程可以再次申请新的资源。 214 | - 非抢占条件(No pre-emption):已经分配的资源不能从相应的进程中被强制地剥夺。 215 | - 循环等待条件(Circular wait):系统中若干进程组成环路,该环路中每个进程都在等待相邻进程正占用的资源。 216 | 217 | ### 如何处理死锁问题: 218 | 219 | - 忽略该问题。例如鸵鸟算法,该算法可以应用在极少发生死锁的的情况下。为什么叫鸵鸟算法呢,因为传说中鸵鸟看到危险就把头埋在地底下,可能鸵鸟觉得看不到危险也就没危险了吧。跟掩耳盗铃有点像。 220 | - 检测死锁并且恢复。 221 | - 仔细地对资源进行动态分配,使系统始终处于安全状态以避免死锁。 222 | - 通过破除死锁四个必要条件之一,来防止死锁产生。 223 | 224 | ## 8.临界资源 225 | 226 | - 在操作系统中,进程是占有资源的最小单位(线程可以访问其所在进程内的所有资源,但线程本身并不占有资源或仅仅占有一点必须资源)。但对于某些资源来说,其在同一时间只能被一个进程所占用。这些一次只能被一个进程所占用的资源就是所谓的临界资源。典型的临界资源比如物理上的打印机,或是存在硬盘或内存中被多个进程所共享的一些变量和数据等(如果这类资源不被看成临界资源加以保护,那么很有可能造成丢数据的问题)。 227 | - 对于临界资源的访问,必须是互斥进行。也就是当临界资源被占用时,另一个申请临界资源的进程会被阻塞,直到其所申请的临界资源被释放。而进程内访问临界资源的代码被成为临界区。 228 | 229 | ## 9.一个程序从开始运行到结束的完整过程(四个过程) 230 | 231 | - 1、预处理:条件编译,头文件包含,宏替换的处理,生成.i文件。 232 | - 2、编译:将预处理后的文件转换成汇编语言,生成.s文件 233 | - 3、汇编:汇编变为目标代码(机器代码)生成.o的文件 234 | - 4、链接:连接目标代码,生成可执行程序 235 | 236 | ## 10.内存池、进程池、线程池。 237 | 238 | - 首先介绍一个概念“池化技术 ”。池化技术就是:提前保存大量的资源,以备不时之需以及重复使用。池化技术应用广泛,如内存池,线程池,连接池等等。内存池相关的内容,建议看看Apache、Nginx等开源web服务器的内存池实现。 239 | - 由于在实际应用当做,分配内存、创建进程、线程都会设计到一些系统调用,系统调用需要导致程序从用户态切换到内核态,是非常耗时的操作。因此,当程序中需要频繁的进行内存申请释放,进程、线程创建销毁等操作时,通常会使用内存池、进程池、线程池技术来提升程序的性能。 240 | - 线程池:线程池的原理很简单,类似于操作系统中的缓冲区的概念,它的流程如下:先启动若干数量的线程,并让这些线程都处于睡眠状态,当需要一个开辟一个线程去做具体的工作时,就会唤醒线程池中的某一个睡眠线程,让它去做具体工作,当工作完成后,线程又处于睡眠状态,而不是将线程销毁。 241 | - 进程池与线程池同理。 242 | - 内存池:内存池是指程序预先从操作系统申请一块足够大内存,此后,当程序中需要申请内存的时候,不是直接向操作系统申请,而是直接从内存池中获取;同理,当程序释放内存的时候,并不真正将内存返回给操作系统,而是返回内存池。当程序退出(或者特定时间)时,内存池才将之前申请的内存真正释放。 243 | 244 | ## 11.动态链接库与静态链接库的区别 245 | 246 | - 静态库 247 | 248 | - 静态库是一个外部函数与变量的集合体。静态库的文件内容,通常包含一堆程序员自定的变量与函数,其内容不像动态链接库那么复杂,在编译期间由编译器与链接器将它集成至应用程序内,并制作成目标文件以及可以独立运作的可执行文件。而这个可执行文件与编译可执行文件的程序,都是一种程序的静态创建(static build)。 249 | 250 | - 251 | 252 | ![img](https://img.mubu.com/document_image/aefbbdfe-7b5b-462d-82e0-7845e8926da2-4943374.jpg) 253 | 254 | - 动态库 255 | 256 | - 静态库很方便,但是如果我们只是想用库中的某一个函数,却仍然得把所有的内容都链接进去。一个更现代的方法则是使用共享库,避免了在文件中静态库的大量重复。 257 | 258 | - 动态链接可以在首次载入的时候执行(load-time linking),这是 Linux 的标准做法,会由动态链接器ld-linux.so 完成,比方标准 C 库(libc.so) 通常就是动态链接的,这样所有的程序可以共享同一个库,而不用分别进行封装。 259 | 260 | - 动态链接也可以在程序开始执行的时候完成(run-time linking),在 Linux 中使用 dlopen()接口来完成(会使用函数指针),通常用于分布式软件,高性能服务器上。而且共享库也可以在多个进程间共享。 261 | 262 | - 链接使得我们可以用多个对象文件构造我们的程序。可以在程序的不同阶段进行(编译、载入、运行期间均可),理解链接可以帮助我们避免遇到奇怪的错误。 263 | 264 | - ![img](https://img.mubu.com/document_image/6584bb0a-d249-4e95-8c2f-dad35bef84b3-4943374.jpg) 265 | 266 | ### 区别: 267 | 268 | - 使用静态库的时候,静态链接库要参与编译,在生成执行文件之前的链接过程中,要将静态链接库的全部指令直接链接入可执行文件中。而动态库提供了一种方法,使进程可以调用不属于其可执行代码的函数。函数的可执行代码位于一个.dll文件中,该dll包含一个或多个已被编译,链接并与使用它们的进程分开储存的函数。 269 | 270 | - 静态库中不能再包含其他动态库或静态库,而在动态库中还可以再包含其他动态或者静态库。 271 | 272 | - 静态库在编译的时候,就将库函数装在到程序中去了,而动态库函数必须在运行的时候才被装载,所以使用静态库速度快一些。 273 | 274 | ## 12.虚拟内存?优缺点? 275 | 276 | - 定义:具有请求调入功能和置换功能,能从逻辑上对内存容量加以扩充得一种存储器系统。其逻辑容量由内存之和和外存之和决定。 277 | - 与传统存储器比较虚拟存储器有以下三个主要特征: 278 | - 多次性,是指无需在作业运行时一次性地全部装入内存,而是允许被分成多次调入内存运行。 279 | - 对换性,是指无需在作业运行时一直常驻内存,而是允许在作业的运行过程中,进行换进和换出。 280 | - 虚拟性,是指从逻辑上扩充内存的容量,使用户所看到的内存容量,远大于实际的内存容量。 281 | - 虚拟内存的实现有以下两种方式: 282 | - 请求分页存储管理。 283 | - 请求分段存储管理。 284 | 285 | ## 13.页面置换算法 286 | 287 | ### 一、最优页面置换算法 288 | 289 | - 最理想的状态下,我们给页面做个标记,挑选一个最远才会被再次用到的页面调出。当然,这样的算法不可能实现,因为不确定一个页面在何时会被用到。 290 | 291 | ### 二、先进先出页面置换算法(FIFO) 292 | 293 | - 这种算法的思想和队列是一样的,该算法总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面予淘汰。实现:把一个进程已调入内存的页面按先后次序链接成一个队列,并且设置一个指针总是指向最老的页面。缺点:对于有些经常被访问的页面如含有全局变量、常用函数、例程等的页面,不能保证这些不被淘汰。 294 | 295 | ### 三、最近最少使用页面置换算法LRU(Least Recently Used) 296 | 297 | - 根据页面调入内存后的使用情况做出决策。LRU置换算法是选择最近最久未使用的页面进行淘汰。 298 | 299 | - 1.为每个在内存中的页面配置一个移位寄存器。(P165)定时信号将每隔一段时间将寄存器右移一位。最小数值的寄存器对应页面就是最久未使用页面。 300 | 301 | - 2.利用一个特殊的栈保存当前使用的各个页面的页面号。每当进程访问某页面时,便将该页面的页面号从栈中移出,将它压入栈顶。因此,栈顶永远是最新被访问的页面号,栈底是最近最久未被访问的页面号。 302 | 303 | ### 四、NRU(Not Recent Used, 最近未使用)算法 304 | 305 | - 前面提到修改位和使用位,NRU算法利用这两个标志位将所有页帧分为4组: 306 | 307 | - 第0组:修改位和使用位都为0; 308 | 309 | - 第1组:修改位为0,使用位为1; 310 | 311 | - 第2组:修改位为1,使用位为0; 312 | 313 | - 第3组:修改位和使用位都为1。 314 | 315 | - NRU算法从组数最小的一组中随机选择一个页面将其移出内存。可能有人会发现第2组这种情况根本不会出现,如果一个页帧被修改,其修改位会被置1,同时它也被使用了,其使用位也会被置1;即不会出现被修改但是没有被使用的情况。真实情况是,页帧的使用位会被定时清零,这样第3组经过一次清零就会变成第2组。这也符合“最近”未使用,即很久以前被使用的页帧被清零了,不在统计范围内,只要“最近”没有被使用,就很有可能被移出。 316 | 317 | ### 五、Second Chance(第二次机会)算法 318 | 319 | - 为了避免FIFO算法将重要的页换出内存,Second Chance算法提供了一些改进。Second Chance算法在将页面换出内存前检查其使用位(使用位前文有介绍),如果其使用位为1,证明此页最近有被使用,猜测它还可能被使用,于是不把它置换出内存,但是把其使用位置为0,随后检查下一个页面,直到发现某页的使用位为0,将此页置换出内存。 320 | 321 | ### 六、Clock算法(时钟轮转法) 322 | 323 | - 为了节约Second Chance算法一个接着一个检查使用位的开销,时钟轮转法又提出了改进。时钟轮转法将所有的页组成一个圆,圆心的指针指向下一个要被置换的页面,置换前同样检查使用位,如果使用位为1,同样将其使用位置为0,随后将顺指针旋转,检查下一个页面,直到发现某页的使用位为0,将此页置换出内存。很容易理解此算法为什么叫“时钟”轮转法。 324 | 325 | 326 | ## 14.中断与系统调用 327 | 328 | - 所谓的中断就是在计算机执行程序的过程中,由于出现了某些特殊事情,使得CPU暂停对程序的执行,转而去执行处理这一事件的程序。等这些特殊事情处理完之后再回去执行之前的程序。中断一般分为三类: 329 | - 由计算机硬件异常或故障引起的中断,称为内部异常中断; 330 | - 由程序中执行了引起中断的指令而造成的中断,称为软中断(这也是和我们将要说明的系统调用相关的中断); 331 | - 由外部设备请求引起的中断,称为外部中断。简单来说,对中断的理解就是对一些特殊事情的处理。 332 | - 与中断紧密相连的一个概念就是中断处理程序了。当中断发生的时候,系统需要去对中断进行处理,对这些中断的处理是由操作系统内核中的特定函数进行的,这些处理中断的特定的函数就是我们所说的中断处理程序了。 333 | - 另一个与中断紧密相连的概念就是中断的优先级。中断的优先级说明的是当一个中断正在被处理的时候,处理器能接受的中断的级别。中断的优先级也表明了中断需要被处理的紧急程度。每个中断都有一个对应的优先级,当处理器在处理某一中断的时候,只有比这个中断优先级高的中断可以被处理器接受并且被处理。优先级比这个当前正在被处理的中断优先级要低的中断将会被忽略。 334 | - 典型的中断优先级如下所示: 335 | - 机器错误 > 时钟 > 磁盘 > 网络设备 > 终端 > 软件中断 336 | - 在讲系统调用之前,先说下进程的执行在系统上的两个级别:用户级和核心级,也称为用户态和系统态(user mode and kernel mode)。 337 | - 用户空间就是用户进程所在的内存区域,相对的,系统空间就是操作系统占据的内存区域。用户进程和系统进程的所有数据都在内存中。处于用户态的程序只能访问用户空间,而处于内核态的程序可以访问用户空间和内核空间。 338 | 339 | ### 用户态切换到内核态的方式如下: 340 | 341 | - 系统调用:程序的执行一般是在用户态下执行的,但当程序需要使用操作系统提供的服务时,比如说打开某一设备、创建文件、读写文件(这些均属于系统调用)等,就需要向操作系统发出调用服务的请求,这就是系统调用。 342 | - 异常:当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。 343 | - 外围设备的中断:当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。 344 | 345 | ### 用户态和核心态(内核态)之间的区别是什么呢? 346 | 347 | - 权限不一样。 348 | - 用户态的进程能存取它们自己的指令和数据,但不能存取内核指令和数据(或其他进程的指令和数据)。 349 | - 核心态下的进程能够存取内核和用户地址某些机器指令是特权指令,在用户态下执行特权指令会引起错误。在系统中内核并不是作为一个与用户进程平行的估计的进程的集合。 350 | 351 | ## 15.多线程,互斥,同步 352 | 353 | ### 同步和互斥 354 | 355 | - 当有多个线程的时候,经常需要去同步(注:同步不是同时刻)这些线程以访问同一个数据或资源。例如,假设有一个程序,其中一个线程用于把文件读到内存,而另一个线程用于统计文件中的字符数。当然,在把整个文件调入内存之前,统计它的计数是没有意义的。但是,由于每个操作都有自己的线程,操作系统会把两个线程当作是互不相干的任务分别执行,这样就可能在没有把整个文件装入内存时统计字数。为解决此问题,你必须使两个线程同步工作。 356 | - 所谓同步,是指在不同进程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。如果用对资源的访问来定义的话,同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。 357 | - 所谓互斥,是指散布在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。如果用对资源的访问来定义的话,互斥某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。 358 | 359 | ### 多线程同步和互斥有几种实现方法 360 | 361 | - 线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。 362 | - 用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。 363 | - 内核模式下的方法有:事件,信号量,互斥量。 364 | - 1、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。 365 | - 2、互斥量:为协调共同对一个共享资源的单独访问而设计的。 366 | - 3、信号量:为控制一个具有有限数量用户资源而设计。 367 | - 4、事 件:用来通知线程有一些事件已发生,从而启动后继任务的开始。 368 | 369 | ## 16.逻辑地址 Vs 物理地址 Vs 虚拟内存 370 | 371 | - 所谓的逻辑地址,是指计算机用户(例如程序开发者),看到的地址。例如,当创建一个长度为100的整型数组时,操作系统返回一个逻辑上的连续空间:指针指向数组第一个元素的内存地址。由于整型元素的大小为4个字节,故第二个元素的地址时起始地址加4,以此类推。事实上,逻辑地址并不一定是元素存储的真实地址,即数组元素的物理地址(在内存条中所处的位置),并非是连续的,只是操作系统通过地址映射,将逻辑地址映射成连续的,这样更符合人们的直观思维。 372 | - 另一个重要概念是虚拟内存。操作系统读写内存的速度可以比读写磁盘的速度快几个量级。但是,内存价格也相对较高,不能大规模扩展。于是,操作系统可以通过将部分不太常用的数据移出内存,“存放到价格相对较低的磁盘缓存,以实现内存扩展。操作系统还可以通过算法预测哪部分存储到磁盘缓存的数据需要进行读写,提前把这部分数据读回内存。虚拟内存空间相对磁盘而言要小很多,因此,即使搜索虚拟内存空间也比直接搜索磁盘要快。唯一慢于磁盘的可能是,内存、虚拟内存中都没有所需要的数据,最终还需要从硬盘中直接读取。这就是为什么内存和虚拟内存中需要存储会被重复读写的数据,否则就失去了缓存的意义。现代计算机中有一个专门的转译缓冲区(Translation Lookaside Buffer,TLB),用来实现虚拟地址到物理地址的快速转换。 373 | 374 | ### 与内存/虚拟内存相关的还有如下两个概念: 375 | 376 | - 1) Resident Set 377 | - 当一个进程在运行的时候,操作系统不会一次性加载进程的所有数据到内存,只会加载一部分正在用,以及预期要用的数据。其他数据可能存储在虚拟内存,交换区和硬盘文件系统上。被加载到内存的部分就是resident set。 378 | - 2) Thrashing 379 | - 由于resident set包含预期要用的数据,理想情况下,进程运行过程中用到的数据都会逐步加载进resident set。但事实往往并非如此:每当需要的内存页面(page)不在resident set中时,操作系统必须从虚拟内存或硬盘中读数据,这个过程被称为内存页面错误(page faults)。当操作系统需要花费大量时间去处理页面错误的情况就是thrashing。 380 | - 参考链接:https://blog.csdn.net/newcong0123/article/details/52792070 381 | 382 | ## 17.内部碎片与外部碎片 383 | 384 | - 在内存管理中,内部碎片是已经被分配出去的的内存空间大于请求所需的内存空间。 385 | - 外部碎片是指还没有分配出去,但是由于大小太小而无法分配给申请空间的新进程的内存空间空闲块。 386 | - 固定分区存在内部碎片,可变式分区分配会存在外部碎片; 387 | - 页式虚拟存储系统存在内部碎片;段式虚拟存储系统,存在外部碎片 388 | - 为了有效的利用内存,使内存产生更少的碎片,要对内存分页,内存以页为单位来使用,最后一页往往装不满,于是形成了内部碎片。 389 | - 为了共享要分段,在段的换入换出时形成外部碎片,比如5K的段换出后,有一个4k的段进来放到原来5k的地方,于是形成1k的外部碎片。 390 | 391 | ## 18.同步和互斥的区别 392 | 393 | - 当有多个线程的时候,经常需要去同步这些线程以访问同一个数据或资源。例如,假设有一个程序,其中一个线程用于把文件读到内存,而另一个线程用于统计文件中的字符数。当然,在把整个文件调入内存之前,统计它的计数是没有意义的。但是,由于每个操作都有自己的线程,操作系统会把两个线程当作是互不相干的任务分别执行,这样就可能在没有把整个文件装入内存时统计字数。为解决此问题,你必须使两个线程同步工作。 394 | - 所谓同步,是指散步在不同进程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。如果用对资源的访问来定义的话,同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。 395 | - 所谓互斥,是指散布在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。如果用对资源的访问来定义的话,互斥某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。 396 | 397 | ## 19.什么是线程安全 398 | 399 | 如果多线程的程序运行结果是可预期的,而且与单线程的程序运行结果一样,那么说明是“线程安全”的。 400 | 401 | ## 20.同步与异步 402 | 403 | - 同步:同步的定义:是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么,这个进程将会一直等待下去,直到收到返回信息才继续执行下去。 404 | - 特点:同步是阻塞模式; 405 | - 同步是按顺序执行,执行完一个再执行下一个,需要等待,协调运行; 406 | - 异步:是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。 407 | - 特点:异步是非阻塞模式,无需等待; 408 | - 异步是彼此独立,在等待某事件的过程中,继续做自己的事,不需要等待这一事件完成后再工作。线程是异步实现的一个方式。 409 | - 同步与异步的优缺点: 410 | - 同步可以避免出现死锁,读脏数据的发生。一般共享某一资源的时候,如果每个人都有修改权限,同时修改一个文件,有可能使一个读取另一个人已经删除了内容,就会出错,同步就不会出错。但,同步需要等待资源访问结束,浪费时间,效率低。 411 | - 异步可以提高效率,但,安全性较低。 412 | 413 | ## 21.系统调用与库函数的区别 414 | 415 | - 系统调用(System call)是程序向系统内核请求服务的方式。可以包括硬件相关的服务(例如,访问硬盘等),或者创建新进程,调度其他进程等。系统调用是程序和操作系统之间的重要接口。 416 | - 库函数:把一些常用的函数编写完放到一个文件里,编写应用程序时调用,这是由第三方提供的,发生在用户地址空间。 417 | - 在移植性方面,不同操作系统的系统调用一般是不同的,移植性差;而在所有的ANSI C编译器版本中,C库函数是相同的。 418 | - 在调用开销方面,系统调用需要在用户空间和内核环境间切换,开销较大;而库函数调用属于“过程调用”,开销较小。 419 | 420 | ## 22.守护、僵尸、孤儿进程的概念 421 | 422 | - 守护进程:运行在后台的一种特殊进程,独立于控制终端并周期性地执行某些任务。 423 | - 僵尸进程:一个进程 fork 子进程,子进程退出,而父进程没有wait/waitpid子进程,那么子进程的进程描述符仍保存在系统中,这样的进程称为僵尸进程。 424 | - 孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,这些子进程称为孤儿进程。(孤儿进程将由 init 进程收养并对它们完成状态收集工作) 425 | 426 | ## 23.Semaphore(信号量) Vs Mutex(互斥锁) 427 | 428 | - 当用户创立多个线程/进程时,如果不同线程/进程同时读写相同的内容,则可能造成读写错误,或者数据不一致。此时,需要通过加锁的方式,控制临界区(critical section)的访问权限。对于semaphore而言,在初始化变量的时候可以控制允许多少个线程/进程同时访问一个临界区,其他的线程/进程会被堵塞,直到有人解锁。 429 | - Mutex相当于只允许一个线程/进程访问的semaphore。此外,根据实际需要,人们还实现了一种读写锁(read-write lock),它允许同时存在多个阅读者(reader),但任何时候至多只有一个写者(writer),且不能于读者共存。 430 | 431 | ## 24.IO多路复用 432 | 433 | - IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合: 434 | - 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。 435 | - 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。 436 | - 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。 437 | - 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。 438 | - 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。 439 | - 与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。 440 | 441 | ## 25.线程安全 442 | 443 | - 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。 444 | - 线程安全问题都是由全局变量及静态变量引起的。 445 | - 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。 446 | 447 | ## 26.线程共享资源和独占资源问题 448 | 449 | - 一个进程中的所有线程共享该进程的地址空间,但它们有各自独立的(/私有的)栈(stack),Windows线程的缺省堆栈大小为1M。堆(heap)的分配与栈有所不同,一般是一个进程有一个C运行时堆,这个堆为本进程中所有线程共享,windows进程还有所谓进程默认堆,用户也可以创建自己的堆。 450 | 451 | - 用操作系统术语,线程切换的时候实际上切换的是一个可以称之为线程控制块的结构(TCB),里面保存所有将来用于恢复线程环境必须的信息,包括所有必须保存的寄存器集,线程的状态等。 452 | 453 | - 堆: 是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。 454 | 455 | - 栈:是个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是 thread safe的。操作系统在切换线程的时候会自动的切换栈,就是切换 SS/ESP寄存器。栈空间不需要在高级语言里面显式的分配和释放。 456 | ![img](https://img.mubu.com/document_image/b9b63c5c-d03f-458d-9670-c60d84d75fb4-4943374.jpg) 457 | 458 | ## 27.什么是分页 459 | 460 | - https://www.cnblogs.com/edisonchou/p/5094066.html 461 | 462 | ## 28.什么是分段 463 | 464 | - https://www.cnblogs.com/edisonchou/p/5115242.html 465 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | 本项目主要从以下几个方向着手,几乎网罗了常见的面试题,可作为面试宝典巩固复习,冲就完事了!在此送一段视频给大家,也是支持我一直学习下去的动力,同时也希望大家给个⭐⭐谢谢! 4 | 5 | **There is something they would lose if you were not there,** 6 | 7 | **there is something they would miss if you were not there,** 8 | 9 | **YOU DO MAKE A DIFFERENCE!** 10 | 11 | **YOU DO MAKE A DIFFERENCE!** 12 | 13 | **YOU DO MAKE A DIFFERENCE!!!!!!** 14 | 15 | https://www.youtube.com/watch?v=CGXL8C1_WlI 16 | 17 | 18 | | JVM💻 | 网络✉️ | 并发和多线程♨| 数据库💾 | Java ☕️ | 集合容器🍸 | 异常❌| 操作系统🖥 | Spring💦| 19 | | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | 20 | 21 | ## JVM💻部分 22 | 23 | > [JVM](https://github.com/whw19970927/-a-Java-book-/blob/master/JVM.md) 24 | 25 | ## 计算机网络✉️ 26 | 27 | > [计算机网络](https://github.com/whw19970927/JavaSecret/blob/master/Collections/CSnet.md) 28 | 29 | ## 并发和多线程♨ 30 | 31 | > [并发和多线程](https://github.com/whw19970927/JavaSecret/blob/master/Collections/CSnet.md) 32 | 33 | ## 数据库💾 34 | 35 | > [数据库](https://github.com/whw19970927/JavaSecret/blob/master/Collections/MySQL.md) 36 | 37 | ## Java基础☕️ 38 | 39 | > [Java](https://github.com/whw19970927/JavaSecret/blob/master/Collections/JavaBasis.md) 40 | 41 | ## 集合容器🍸 42 | 43 | > [集合容器](https://github.com/whw19970927/JavaSecret/blob/master/Collections/collections.md) 44 | 45 | ## 异常❌ 46 | 47 | > [异常](https://github.com/whw19970927/JavaSecret/blob/master/Collections/Exceptiom.md) 48 | 49 | ## 操作系统🖥 50 | 51 | > [操作系统](https://github.com/whw19970927/JavaSecret/blob/master/Collections/cs.md) 52 | 53 | ## Spring💦 54 | 55 | > [Spring]() 56 | 57 | 58 | # 致谢 59 | 60 | 感谢一位网友在学习路上的支持和同行,这个项目也有他的贡献,同时也恭喜他字节上岸,做低端的iOS。 61 | 我本人来好未来拿了后端sp,爽的批爆! 62 | --------------------------------------------------------------------------------