Java
2022-01-12 16:35:07 4 举报
AI智能生成
Java面试总结
作者其他创作
大纲/内容
网络
OSI七层模型和对应的协议
七层模型
- 应用层:允许访问OSI环境的手段(应用协议数据单元APDU)
- 表示层:对数据进行翻译、加密和压缩(表示协议数据单元PPDU)
- 会话层:建立、管理和终止会话(会话协议数据单元SPDU)
- 传输层:提供端到端的可靠报文传递和错误恢复(段 Segment)
- 网络层:负责数据包从哪个源到宿的传递和网际互连(包 Packet)
- 数据链路层:将比特组装成帧和点到点的传递(帧 Frame)
- 物理层:通过媒介传输比特,确定机械及电器规范(比特 Bit)
协议
- 应用层:FTP、HTTP、WWW、DNS、SMTP、Telnet、NFS
- 表示层:JPEG、MPEG、ASII
- 会话层:NFS、SQL、NETBIOS、RPC
- 传输层:TCP、UDP、SPX
- 网络层:IP、ICMP、ARP、RARP、OSPF、IPX、RIP、IGRP(路由器)
- 数据链路层:PPP、FR、HDLC、VLAN、MAC(网桥、交换机)
- 物理层:RJ45、CLOCK、IEEE802.3(中继器,集线器,网关)
五层协议的体系架构
- 应用层:应用层 (application-layer)的任务是通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程间的通信和交互规则。对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多,如域名系统DNS,支持万维网应用的HTTP协议,支持电子邮件的SMTP协议等等。我们把应用层交互的数据单元称为报文。
- 运输层:运输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务。运输层主要使用两种协议:1.传输控制协议TCP(Transmission Control Protocol),提供面向连接的,可靠的数据传输服务。2.用户数据协议UDP(User Datagram Protocol),提供无连接的,尽最大努力的数据传输服务(不保证数据传输的可靠性)
- 网络层:在计算机网络中进行通信的两个计算机之间可能会经过很多个数据链路,也可能还要经过很多通信子网。网络层的任务就是选择合适的网间路由和交换结点,确保数据及时传送。主要使用IP协议,因此分组也叫IP数据报,简称数据报。互联网是由大量的异构(heterogeneous)网络通过路由器相互连接起来的。互联网使用的网络层协议是无连接的网际协议(Intert Protocol)和许多路由选择协议,因此互联网的网络层也叫做网际层或IP层。
- 数据链路层:数据链路层(data link layer)通常简称链路层。两台主机之间的数据传输总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。在两个相邻节点之间传送数据时,数据链路层将网络层交下来的IP数据报组装成帧,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息(如同步信息,地址信息,差错控制等)。
- 物理层:物理层上所传送的数据单位是比特。物理层(physical layer)的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。
HTTP
什么是HTTP协议
HTTP是HyperText Transfer Protocol的缩写,译为超文本传输协议。是一种应用于OSI七层模型中的应用层的协议,是我们平常互联网网络通信传输的基础。它的作用就是规定了服务器和客户端之间的建立连接,请求数据,响应数据,关闭连接(HTTP的连接和断开时基于TCP的三次握手,四次挥手)
HTTP 1.0和HTTP 1.1的区别,HTTP2.0
- 缓存处理:在HTTP1.0中主要使用header里的if-Modified-Since,Expires来作为缓存判断的标准;HTTP1.1则引入了更多的缓存控制策略,例如Entity tag,if-Unmodified-Since,if-Match,if-None-Match等更多可供选择的缓存头来控制缓存策略。
- 带宽优化及网络连接的使用:在HTTP1.0中,存在了一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却把整个对象送过来了,并且不支持断点续传功能,HTTP1.1则在请求头中引入了range头域,它允许只请求资源的某个部分,即返回码是206(PartialContent),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
- 错误处理的管理:在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。
- Host头处理:在HTTP1.0中认为每一台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机,并且他们共享一个IP地址。HTTP1.1的请求消息和响应消息都支持Host头域,且请求消息中如果没有头域会报告一个错误(400 Bad Request)。
- 长连接:HTTP1.1支持长连接和请求的流水线处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭的消耗和延迟,在HTTP1.1中默认开启Connection:keep-alive(连接重用),一定程度上弥补了HTTP1.0每次请求都要连接的缺点。
Keep-Alive模式:
我们知道 HTTP 协议采用 “请求 - 应答” 模式,当使用普通模式,即非 KeepAlive 模式时,每个请求 / 应答客户和服务器都要新建一个连接,完成 之后立即断开连接(HTTP 协议为无连接的协议);当使用 Keep-Alive 模式(又称持久连接、连接重用)时,K eep-Alive 功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive 功能避免了建立或者重新建立连接。启用 Keep-Alive 模式肯定更高效,性能更高。因为避免了建立 / 释放连接的开销。因此最好能维持一个长连接,可以用一个长连接来发多个请求。
我们知道 HTTP 协议采用 “请求 - 应答” 模式,当使用普通模式,即非 KeepAlive 模式时,每个请求 / 应答客户和服务器都要新建一个连接,完成 之后立即断开连接(HTTP 协议为无连接的协议);当使用 Keep-Alive 模式(又称持久连接、连接重用)时,K eep-Alive 功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive 功能避免了建立或者重新建立连接。启用 Keep-Alive 模式肯定更高效,性能更高。因为避免了建立 / 释放连接的开销。因此最好能维持一个长连接,可以用一个长连接来发多个请求。
HTTP2.0
HTTP2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比 HTTP1.1 大了好几个数量级。
当然 HTTP1.1 也可以多建立几个 TCP 连接,来支持处理更多并发的请求,但是创建 TCP 连接本身也是有开销的。TCP 连接有一个预热和保护的过程,先检查数据是否传送成功,一旦成功过,则慢慢加大传输速度。因此对应瞬时并发的连接,服务器的响应就会变慢。所以最好能使用一个建立好的连接,并且这个连接可以支持瞬时并发的请求。
HTTP2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比 HTTP1.1 大了好几个数量级。
当然 HTTP1.1 也可以多建立几个 TCP 连接,来支持处理更多并发的请求,但是创建 TCP 连接本身也是有开销的。TCP 连接有一个预热和保护的过程,先检查数据是否传送成功,一旦成功过,则慢慢加大传输速度。因此对应瞬时并发的连接,服务器的响应就会变慢。所以最好能使用一个建立好的连接,并且这个连接可以支持瞬时并发的请求。
幂等性
HTTP方法的幂等性:
- GET方法:用于获取资源,不应该有副作用,所以是幂等的。
- DELETE方法:用于删除资源,有副作用,但它应该满足幂等性。例如,删除id = 1的数据,调用1次和N次的副作用相同。
- PUT方法:用于创建或更新操作,有副作用,但它也应该满足幂等性。例如,创建或更新一个id = 1的数据,调用一次和N次的副作用相同。
- POST方法:POST方法与PUT方法的区别在于幂等性,POST不具有幂等性。因为POST请求每次都会创建一个文件,而PUT方法会在服务器验证是否有ENTITY,若有则更新该ENTITY而不是创建。例如:POST的URL创建一个帖子,若请求2次则会创建2个帖子。
POST不具有幂等性,如何防范POST重复提交:
- 1.对应的后端一定要做到幂等性。
- 2.服务器收到POST请求,在操作成功后必须返回状态码302重定向到另一个页面,这样即使用户刷新页面,url已经变更,不需要进行POST请求,也不会重复提交表单。
幂等性:
HTTP 方法的幂等性是指一次和多次请求某一个资源应该具有同样的副作用。说白了就是,同一个请求,发送一次和发送 N 次效果是一样的!
幂等性是分布式系统设计中十分重要的概念,而 HTTP 的分布式本质也决定了它在 HTTP 中具有重要地位。而我们不能轻易假设分布式环境的可靠性。
HTTP 方法的幂等性是指一次和多次请求某一个资源应该具有同样的副作用。说白了就是,同一个请求,发送一次和发送 N 次效果是一样的!
幂等性是分布式系统设计中十分重要的概念,而 HTTP 的分布式本质也决定了它在 HTTP 中具有重要地位。而我们不能轻易假设分布式环境的可靠性。
post方法和get方法的区别
针对浏览器使用规范来说:
但是本质上,GET和POST没有区别:
HTTP的底层是TCP/IP。所以GET和POST的底层也是TCP/IP,也就是说,GET/POST都是TCP链接。GET和POST能做的事情是一样一样的。你要给GET加上request body,给POST带上url参数,技术上是完全行的通的。由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。
- GET方法从服务器获取资源,POST是向服务器发送数据
- GET产生的URL地址可以被书签收藏,也可以被浏览器缓存,而POST不能书签收藏也不行缓存,
- GET参数通过URL传递,并且长度有限,而POST参数是放在request body里面并且长度没有限制,相比GET更加安全。
- 两者还有一个显著的区别:GET 产生一个 TCP 数据包;POST 产生两个 TCP 数据包。
- 对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);
- 而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。
但是本质上,GET和POST没有区别:
HTTP的底层是TCP/IP。所以GET和POST的底层也是TCP/IP,也就是说,GET/POST都是TCP链接。GET和POST能做的事情是一样一样的。你要给GET加上request body,给POST带上url参数,技术上是完全行的通的。由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。
HTTP与HTTPS的区别
HTTP协议运行在TCP之上,明文传输,客户端与服务器都无法验证对方的身份;HTTPS是运行在SSL/TLS(SSL“安全套接层”协议,TLS“安全传输层”协议,都属于是加密协议,)之上的HTTP协议,SSL/TLS运行在TCP之上。HTTPS是添加了加密认证机制的HTTP。
区别:
HTTPS 并非绝对安全,掌握根证书的机构、掌握加密算法的组织同样可以进行中间人形式的攻击,但 HTTPS 仍是现行架构下最安全的解决方案
区别:
- 端口不同:HTTP是80端口,HTTPS是443
- 资源消耗:与HTTP通信相比,HTTPS通信会由于加减密处理消耗更多的CPU和内存资源
- 开销:HTTPS通信需要证书,而证书一般需要想认证机构购买。
HTTPS 并非绝对安全,掌握根证书的机构、掌握加密算法的组织同样可以进行中间人形式的攻击,但 HTTPS 仍是现行架构下最安全的解决方案
HTTPS加密过程
HTTPS采用 对称加密 和 非对称加密 结合的方式来保护浏览器和服务端之间的通信安全。
原理:服务端和客户端都有两把钥匙:公钥和私钥,公钥用来加密数据,同时只能用自己的私钥去解开,这样及时公钥被截获,密文内容也破解不了。
- 对称加密:加密和解密都是同一个密钥。
- 非对称加密:密钥成对出现,分为公钥和私钥,公钥和私钥之间不能互相推导,公钥加密需要私钥解密,私钥加密需要公钥解密。
原理:服务端和客户端都有两把钥匙:公钥和私钥,公钥用来加密数据,同时只能用自己的私钥去解开,这样及时公钥被截获,密文内容也破解不了。
加密流程:
- 浏览器使用HTTPS的URL访问服务器,建立SSL链接、
- 服务器接收到SSL链接后,发送非对称加密的公钥A给浏览器。
- 浏览器生成随机数,作为对称加密的密钥B、
- 浏览器使用服务器返回的公钥,对自己生成的对称加密密钥B进行加密,得到密钥C。
- 浏览器将密钥C发送给服务器。
- 服务器使用自己的非对称加密私钥D对接收的密钥进行解密。得到对称加密密钥B。
- 浏览器和服务器之间使用密钥B作为对称加密密钥进行通信。
优点:
非对称加密只使用了一次,后续所有的通信消息都是用对称加密,效率比非对称加密高。对称加密速度快,非对称加密速度慢 (相对慢 100 倍)。
缺点:
当服务器发送公钥给客户端, 中间人截获公钥 ,将 中间人自己的公钥 冒充服务器的公钥发送给客户端。之后客户端会用 中间人的的公钥 来加密自己生成的 对称密钥 。然后把加密的密钥发送给服务器,这时中间人又把密钥截取,中间人用自己的私钥把加密的密钥进行解密,解密后中间人就能获取 对称加密的密钥 。
注意:非对称加密之所以不安全,因为客户端不知道这把公钥是不是属于服务器的。
非对称加密只使用了一次,后续所有的通信消息都是用对称加密,效率比非对称加密高。对称加密速度快,非对称加密速度慢 (相对慢 100 倍)。
缺点:
当服务器发送公钥给客户端, 中间人截获公钥 ,将 中间人自己的公钥 冒充服务器的公钥发送给客户端。之后客户端会用 中间人的的公钥 来加密自己生成的 对称密钥 。然后把加密的密钥发送给服务器,这时中间人又把密钥截取,中间人用自己的私钥把加密的密钥进行解密,解密后中间人就能获取 对称加密的密钥 。
注意:非对称加密之所以不安全,因为客户端不知道这把公钥是不是属于服务器的。
Cookie和Session的区别?
HTTP是一种不保存状态,即无状态协议。也就是说HTTP协议自身不对请求和响应之间的通信状态进行保存,那么我们怎样保存用户状态呢?Session和Cookie的存在就是为了解决这个问题。
Cookie 和 Session都是用来跟踪浏览器用户身份的会话方式,但是两者的应用场景不太一样。
Cookie 通过在客户端记录信息确定用户身份,Session 通过在服务器端记录信息确定用户身份。
Cookie 和 Session都是用来跟踪浏览器用户身份的会话方式,但是两者的应用场景不太一样。
Cookie 通过在客户端记录信息确定用户身份,Session 通过在服务器端记录信息确定用户身份。
- 实现机制:Session的实现常常依赖与Cookie机制,通过Cookie机制回传的SessionID。
- 大小限制:Cookie有大小限制并且浏览器对每一个站点也有cookie的个数限制,Session没有大小限制,理论上只与服务器的内存大小有关。
- 安全性:Cookie存在安全隐患,通过拦截或本地文件找到cookie后可以进行攻击,而Session由于保存在服务器端,相对更加安全。
- 服务器资源消耗:Session是保存在服务端上会存在一段时间后才会消失,如果session过多会增加服务器压力,cookie是保存在客户端的,不会增加服务器压力。
Cookie
Cookie技术是客户端的解决方案,Cookie就是由服务器发给客户端的特殊信息,而这些信息以文本文件的方式存放在客户端,然后客户端每次向服务器发送请求的时候都会带上这些特殊的信息。
Session
Session是一个存在服务器上的类似一个散列表格的文件。里面存有我们需要的信息,在我们需要用的时候可以从里面取出来。类似于一个大号的map,里面的建存的是用户的sessionid,用户向服务器发送请求的时候会带上这个sessionid,这时就可以从中取出对应的值了。
Session的实现依赖于Cookie机制,如果客户端禁止cookie,Session还能用吗?
一般默认的情况下,在会话中,服务器存储session的sessionID是通过cookie存到浏览器里,如果浏览器禁用了cookie,浏览器请求服务器就无法携带sessionID,服务器无法识别请求中的用户身份,session失效。
通过其他方法可以在禁用cookie的情况下使用session:
一般默认的情况下,在会话中,服务器存储session的sessionID是通过cookie存到浏览器里,如果浏览器禁用了cookie,浏览器请求服务器就无法携带sessionID,服务器无法识别请求中的用户身份,session失效。
通过其他方法可以在禁用cookie的情况下使用session:
- 重写URL:把sessionID作为参数追加到原URL中,后续的浏览器与服务器交互中就携带sessionID参数。
- 服务器的返回数据包含sessionID:浏览器发送请求时,携带sessionID参数。
- 通过HTTP协议其他header字段:服务器每次返回时设置该header字段信息,浏览其中js读取该header字段,请求服务器时,js在设置携带该header字段。
URL和URI的区别?
URL 一般有四部分组成:<协议>://< 主机 >:< 端口 >/< 路径 >
URI (Uniform Resource Identifier) 是统一资源标志符,可以唯一标识一个资源。
URL (Uniform Resource Location) 是统一资源定位符,可以提供该资源的路径。它是一种具体的 URI,即 URL 可以用来标识一个资源,而且还指明了如何 locate 这个资源。
URI 的作用像身份证号一样,URL 的作用更像家庭住址一样。URL 是一种具体的 URI,它不仅唯一标识资源,而且还提供了定位该资源的信息。
URL (Uniform Resource Location) 是统一资源定位符,可以提供该资源的路径。它是一种具体的 URI,即 URL 可以用来标识一个资源,而且还指明了如何 locate 这个资源。
URI 的作用像身份证号一样,URL 的作用更像家庭住址一样。URL 是一种具体的 URI,它不仅唯一标识资源,而且还提供了定位该资源的信息。
在浏览器中输入URL地址到显示页面的过程
总结:DNS解析获得ip——tcp——http——定位资源,服务器处理请求并返回HTTP报文——渲染页面展示页面
- 1.浏览器查询 DNS,获取域名对应的 IP 地址:具体过程包括浏览器搜索自身的 DNS 缓存、搜索操作系统的 DNS 缓存、读取本地的 Host 文件和向本地 DNS 服务器进行查询等。
- 2.浏览器获得域名对应的 IP 地址以后,浏览器向服务器请求建立链接,发起TCP三次握手;
- 3.TCP/IP 链接建立起来后,浏览器向服务器发送 HTTP 请求;
- 4.服务器接收到这个请求,并根据路径参数映射到特定的请求处理器进行处理,并将处理结果及相应的视图返回给浏览器;
- 5.浏览器解析并渲染视图,若遇到对 js 文件、css 文件及图片等静态资源的引用,则重复上述步骤并向服务器请求这些资源;浏览器根据其请求到的资源、数据渲染页面,最终向用户呈现一个完整的页面。
总结:DNS解析获得ip——tcp——http——定位资源,服务器处理请求并返回HTTP报文——渲染页面展示页面
首先明确:建立一个完整的socket连接需要的5个参数,分别是:(本机ip,本机端口号,使用的网络协议,要访问的机器的ip,要访问机器的端口号)
- 假设访问百度,输入www.baidu.com,然后回车,此时要确定的是百度的ip地址,使用dns协议,向dns服务器发送数据包(dns服务器开启的是53端口),DNS服务器返回给我们百度的ip地址。利用子网掩码判断要访问的ip是否和本地主机是同一个网段,假设要访问的ip跟我们不是同一个网段,那么向百度发送数据包必须通过网关转发。
- 接下来通过应用层,浏览器访问使用的是http协议,构造一个http数据包。假定其长度为4960个字节,他会被嵌在tcp数据包之中。
- 然后传输层,Tcp数据包需要设置端口,接收方的默认端口是80,本机的端口是一个随机生成的1024到65535之间的整数。假定为8888。Tcp数据包的包头长度为20字节,加上http数据包,为4980字节。
- 然后经过网络层,tcp数据包再嵌入ip数据包,ip数据包需设置双方ip【已知】。Ip数据包的头长度为20字节,总共是5000字节。IP数据包经过网关转发,进入以太网。
- 接下来到数据链路层。Ip数据包嵌入以太网数据包,以太网数据包需设置双方mac地址【已知】,接收方mac地址即网关mac地址【通过arp协议得到】。以太网数据包的数据部分最大为1500字节,因此ip数据包必须分包,因为每个包都有自己的ip标头,因此四个包的ip数据包的长度分别是1500,1500,1500,560。
- 然后是物理层。物理线路则只负责该数据以bit为单位从主机传输到下一个目的地。下一个目的地接受到数据后,从物理层得到数据然后经过逐层的解包到链路层到网络层,然后开始上述的处理,在经网络层、链路层、物理层将数据封装好继续传往下一个地址。
TCP和UDP
TCP和UDP的区别?
- UDP 在传送数据之前不需要先建立连接,远地主机在收到 UDP 报文后,不需要给出任何确认。虽然 UDP 不提供可靠交付,但在某些情况下 UDP 确是一种最有效的工作方式(一般用于即时通信),比如: QQ 语音、 QQ 视频 、直播等等
- TCP 提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。 TCP 不提供广播或多播服务。由于 TCP 要提供可靠的,面向连接的传输服务(TCP的可靠体现在TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源),这一难以避免增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的首部增大很多,还要占用许多处理机资源。TCP 一般用于文件传输、发送和接收邮件、远程登录等场景。
TCP保证可靠的机制
- 应用数据被分割成TCP认为最合适发送的数据块。
- TCP给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。
- 校验和:TCP将保持它首部和数据的校验和。这是一个端到端的校验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP将丢弃这个报文段不确定收到此报文段。
- TCP的接收端会丢弃重复的数据。
- 流量控制:TCP 连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。 (TCP 利用滑动窗口实现流量控制)
- 拥塞控制:当网络拥塞时,减少数据的发送。
- ARQ协议(自动重传请求):也是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确定,在确定后再发送下一个分组。
- 超时重传:当TCP发出一个段后,它启动一个定时器,等待目的端确定收到这个报文段。如果不能及时收到一个确定,将重新发这个报文段。
TCP的流量控制,拥塞控制、ARQ协议、滑动窗口
流量控制与滑动窗口:
TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。 接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。
本质上是描述接收方的 TCP 数据报缓冲区大小的数据,发送方根据这个数据来计算自己最多能发送多长的数据
TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。 接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。
本质上是描述接收方的 TCP 数据报缓冲区大小的数据,发送方根据这个数据来计算自己最多能发送多长的数据
拥塞控制:
资源:计算机网络中的带宽、交换结点中的缓存及处理机等都是网络的资源。
拥塞:在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就会变坏,这种情况就叫做拥塞。
拥塞控制:防止过多的数据注入网络中,这样可以使网络中的路由器或链路不致过载。拥塞控制和流量控制不同,前者是一个全局性的过程,而后者是点对点通信量的控制。
目的:找到违反原则的地方并进行修改。
拥塞控制的方法主要有四种:
资源:计算机网络中的带宽、交换结点中的缓存及处理机等都是网络的资源。
拥塞:在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就会变坏,这种情况就叫做拥塞。
拥塞控制:防止过多的数据注入网络中,这样可以使网络中的路由器或链路不致过载。拥塞控制和流量控制不同,前者是一个全局性的过程,而后者是点对点通信量的控制。
目的:找到违反原则的地方并进行修改。
拥塞控制的方法主要有四种:
- 慢启动:不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小。
- 拥塞避免:拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间PTT就把发送方的拥塞窗口cwnd加1,而不是加倍,这样拥塞窗口按线性规律缓慢增长。
- 快重传:快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方尽早知道有报文没有到达对方)而不是等到自己发送数据时在捎带确定。快重算法规定,发送方只要一连收到三个重复确定就应当立即重传对方尚未收到的报文段,而不是继续等待设置的重传计时器时间到期。
- 快恢复:快重传配合使用的还有快恢复算法,当发送发连续收到三个重复确定时,就会执行“乘法减少”算法,把ssthresh(阈值)门限减半,但是接下去并不是执行慢开始算法:因为如果网络出现拥塞的话就不会收到好几个重复的确定,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将cwnd设置为ssthresh的大小,然后执行拥塞避免算法。
ARQ协议:
自动重传请求(Automatic Repeat-reQuest,ARQ)是 OSI 模型中网络层和传输层的错误纠正协议之一。它通过使用确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。如果发送方在发送后一段时间之内没有收到确认帧,它通常会重新发送。ARQ 包括停止等待 ARQ 协议和连续 ARQ 协议。
自动重传请求(Automatic Repeat-reQuest,ARQ)是 OSI 模型中网络层和传输层的错误纠正协议之一。它通过使用确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。如果发送方在发送后一段时间之内没有收到确认帧,它通常会重新发送。ARQ 包括停止等待 ARQ 协议和连续 ARQ 协议。
- 停止等待ARQ协议: 停止等待协议是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认(回复 ACK)。如果过了一段时间(超时时间后),还是没有收到 ACK 确认,说明没有发送成功,需要重新发送,直到收到确认后再发下一个分组;在停止等待协议中,若接收方收到重复分组,就丢弃该分组,但同时还要发送确认;
- 连续ARQ协议:连续 ARQ 协议可提高信道利用率。发送方维持一个发送窗口,凡位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。接收方一般采用累计确认,对按序到达的最后一个分组发送确认,表明到这个分组为止的所有分组都已经正确收到了。
TCP的三次握手和四次挥手
三次握手:(我要和你建立链接,你真的要和我建立链接么,我真的要和你建立链接,成功)
三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。经过三次握手之后就可以确定自己与对方的发送与接收是正常的,第四次就没有必要了。
三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。经过三次握手之后就可以确定自己与对方的发送与接收是正常的,第四次就没有必要了。
- 一次握手:客户端 - 发送带有SYN标志的数据包 - 服务端。(Client什么都不能确定,Server端确定了对方发送正常,自己接受正常)。
- 二次握手:服务端 - 发送带有SYN/ACK标志的数据包 - 客户端。(Client确定了自己发送,接受正常,对方发送,接受正常;Server确定了对方发送正常,自己接收正常)
- 三次握手: 客户端 – 发送带有带有 ACK 标志的数据包 – 服务端。(Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常)
四次挥手:(我要和你断开链接;好的,断吧。我也要和你断开链接;好的,断吧)
- 第一次挥手:客户端主动关闭方发送一个 FIN,用来关闭客户端到服务端的数据传送,也就是客户端告诉服务端:我已经不会再给你发数据了 (当然,在 fin 包之前发送出去的数据,如果没有收到对应的 ack 确认报文,客户端依然会重发这些数据),但是,此时客户端还可以接受数据。
- 第二次挥手:服务端收到 FIN 包后,发送一个 ACK 给客户端,确认序号为收到序号 + 1(与 SYN 相同,一个 FIN 占用一个序号)。
- 第三次挥手:服务端发送一个 FIN,用来关闭服务端到客户端的数据传送,也就是告诉客户端,我的数据也发送完了,不会再给你发数据了。
- 第四次挥手:客户端收到 FIN 后,发送一个 ACK 给服务端,确认序号为收到序号 + 1
IP
路由器和交换机
交换机拥有一条高带宽的背部总线和内部交换矩阵。交换机的所有的端口都挂接在这条背部总线上,当控制电路收到数据包以后,处理端口会查找内存中的地址对照表以确定目的 MAC(网卡的硬件地址)的 NIC(网卡)挂接在哪个端口上,通过内部 交换矩阵迅速将数据包传送到目的端口。目的 MAC 若不存在,交换机才广播到所有的端口,接收端口回应后交换机会 “学习” 新的地址,并把它添加入内部地址表 中。
交换机工作于 OSI 参考模型的第二层,即数据链路层。交换机内部的 CPU 会在每个端口成功连接时,通过 ARP 协议学习它的 MAC 地址,保存成一张 ARP 表。在今后的通讯中,发往该 MAC 地址的数据包将仅送往其对应的端口,而不是所有的端口。
交换机工作于 OSI 参考模型的第二层,即数据链路层。交换机内部的 CPU 会在每个端口成功连接时,通过 ARP 协议学习它的 MAC 地址,保存成一张 ARP 表。在今后的通讯中,发往该 MAC 地址的数据包将仅送往其对应的端口,而不是所有的端口。
路由器(Router)是一种计算机网络设备,提供了路由与转送两种重要机制,可以决定数据包从来源端到目的端所经过 的路由路径(host 到 host 之间的传输路径),这个过程称为路由;将路由器输入端的数据包移送至适当的路由器输出端 (在路由器内部进行),这称为转送。路由工作在 OSI 模型的第三层 —— 即网络层,例如网际协议。
IP定义、内网IP和外网IP、静态IP和动态IP
IP 是 32 位二进制数据,通常以十进制表示,并以 “.” 分隔。IP 地址是一种逻辑地址,用来标识网络中一个个主机,IP 有唯一性,即每台机器的 IP 在全世界是唯一的。IP 地址 = 网络地址 + 主机地址,根据网络地址的长度不同,有三种 IP 地址的组成形式,A 类、B 类、C 类,A 类前 8 位是(0+7 位网络地址),B 类前 16 位是(10+14 位网络地址),C 类前 24 位是(110+21 位网络地址)
内网 IP 局域网,网线都是连接在同一个 交换机上面的,也就是说它们的 IP 地址是由交换机或者路由器进行分配的。
外网 IP 是全世界唯一的 IP 地址,仅分配给一个网络设备。
动态和静态IP:
区别:
1、动态 IP 需要在连接网络时自动获取 IP 地址以供用户正常上网,而静态 IP 是 ISP 在装机时分配给用户的 IP 地址,可以直接连接上网,不需要获取 IP 地址。
2、静态 IP 是可以直接上网的 IP 段,该 IP 在 ISP 装机时会划分一个 IP 地址给你,让计算机在连接网络时不再自动获取网络地址,避免了网络连接上的困扰。
使用:
1、动态 IP 上网,又叫做 DHCP 上网。自动获取 IP 上网。动态 IP 这种上网方式,在未使用路由器的情况下,只需要把这根宽带网线连接到电脑上,电脑上的 IP 地址设置为自动获得,电脑就可以实现上网了。
2、静态 IP 上网,又叫做固定 IP 地址上网。这种上网方式,宽带运营商会提供一根网线,这跟网线提供固定的 IP 地址、子网掩码、网关和 DNS 服务器地址给用户。在未使用路由器的情况下,只需要把这根入户网线连接到电脑上,并且手动设置电脑上的 IP 地址,这样电脑才能上网。
区别:
1、动态 IP 需要在连接网络时自动获取 IP 地址以供用户正常上网,而静态 IP 是 ISP 在装机时分配给用户的 IP 地址,可以直接连接上网,不需要获取 IP 地址。
2、静态 IP 是可以直接上网的 IP 段,该 IP 在 ISP 装机时会划分一个 IP 地址给你,让计算机在连接网络时不再自动获取网络地址,避免了网络连接上的困扰。
使用:
1、动态 IP 上网,又叫做 DHCP 上网。自动获取 IP 上网。动态 IP 这种上网方式,在未使用路由器的情况下,只需要把这根宽带网线连接到电脑上,电脑上的 IP 地址设置为自动获得,电脑就可以实现上网了。
2、静态 IP 上网,又叫做固定 IP 地址上网。这种上网方式,宽带运营商会提供一根网线,这跟网线提供固定的 IP 地址、子网掩码、网关和 DNS 服务器地址给用户。在未使用路由器的情况下,只需要把这根入户网线连接到电脑上,并且手动设置电脑上的 IP 地址,这样电脑才能上网。
子网掩码
子网掩码是用来判断任意两台计算机的 ip 地址是否属于同一子网络的根据。最为简单的理解就是两台计算机各自的 ip 地址与子网掩码进行 & 运算后,得出的结果是相同的,则说明这两台计算机是处于同一个子网络上的,可以进行直接的通讯。
内网通讯时,一般都用 192.168 的形式,所以子网掩码一般采用 255.255.*.* 的形式。
内网通讯时,一般都用 192.168 的形式,所以子网掩码一般采用 255.255.*.* 的形式。
网关
网关实质上是一个在不同子段网路中传输数据的设备。比如有网络 A 和网络 B,若二者子网掩码不同,即二者不属于同一个子网络。在没有路由器的情况下,两个网络之间是不能进行 TCP/IP 通信的,即使是两个网络连接在同一台交换机(或集线器)上, TCP/IP 协议也会根据子网掩码(255.255.255.0)判定两个网络中的主机处在不同的网络里。
而要实现这两个网络之间的通信,则必须通过网关。如果网络 A 中的主机发现数据包的目的主机不在本地网络中,就把数据包转发给它自己的网关,再由网关转发给网络 B 的网关,网络 B 的网关再转发给网络 B 的某个主机。网络 A 向网络 B 转发数据包的过程。
在传统 TCP/IP 术语中,网络设备只分成两种,一种为网关(gateway),另一种为主机(host)。网关能在网络间转递数据包,但主机不能转送数据包。
而要实现这两个网络之间的通信,则必须通过网关。如果网络 A 中的主机发现数据包的目的主机不在本地网络中,就把数据包转发给它自己的网关,再由网关转发给网络 B 的网关,网络 B 的网关再转发给网络 B 的某个主机。网络 A 向网络 B 转发数据包的过程。
在传统 TCP/IP 术语中,网络设备只分成两种,一种为网关(gateway),另一种为主机(host)。网关能在网络间转递数据包,但主机不能转送数据包。
DNS服务器
DNS 是指:域名服务器 (Domain Name Server)。在 Internet 上域名与 IP 地址之间是一一对应的,域名虽然便于人们记忆,但机器之间只能互相认识 IP 地址,它们之间的转换工作称为域名解析,域名解析需要由专门的域名解析服务器来完成,DNS 就是进行域名解析的服务器 。
MAC地址
MAC(Medium/Media Access Control,介质访问控制)MAC 地址是收录在 NetworkInterfaceCard(网卡 NIC) 里的.MAC 地址 , 也叫硬件地址 , 是由 48 比特 /bit 长(6 字节 /byte,1byte=8bits),16 进制的数字组成。前 24 位叫做组织唯一标志符(Organizationally Unique Identifier, 即 OUI),是识别 LAN(局域网)节点的标识。后 24 位是由厂家自己分配。网卡的物理地址通常是由网卡生产厂家烧入网卡 EPROM(一种闪存芯片,通常可以通过程序擦写),它存储的是传输数据时真正赖以标识发出数据的电脑和接收数据的主机的地址。也就是说,在网络底层的物理传输过程中,是通过物理地址来识别主机的,它一定是全球唯一的。在 OSI(Open System Interconnection,开放系统互连)7 层网络协议(物理层,数据链路层,网络层,传输层,会话层,表示层,应用层)参考模型中,MAC 对应于第二层数据链路层(Data Link),而 IP 对应于网络层。
使用IP地址与MAC地址进行标识的优点:
- 1.IP 地址的分配是根据网络的拓朴结构,而不是根据谁制造了网络设置。若将高效的路由选择方案建立在设备制造商的基础上而不是网络所处的拓朴位置基础上,这种方案是不可行的。
- 2. 当存在一个附加层的地址寻址时,设备更易于移动和维修。例如,如果一个以太网卡坏了,可以被更换,而无须取得一个新的 IP 地址。如果一个 IP 主机从一个网络移到另一个网络,可以给它一个新的 IP 地址,而无须换一个新的网卡。
- 3.无论是局域网,还是广域网中的计算机之间的通信,最终都表现为将数据包从某种形式的链路上的初始节点出发,从一个节点传递到另一个节点,最终传送到目的节点。数据包在这些节点之间的移动都是由 ARP(Address Resolution Protocol:地址解析协议)负责将 IP 地址映射到 MAC 地址上来完成的。
ARP协议工作原理:
首先,每个主机都会在自己的 ARP 缓冲区中建立一个 ARP 列表,以表示 IP 地址和 MAC 地址之间的对应关系。当源主机要发送数据时,首先检查 ARP 列表中是否有对应 IP 地址的目的主机的 MAC 地址,如果有,则直接发送数据,如果没有,就向本网段的所有主机发送 ARP 数据包,该数据包包括的内容有:源主机 IP 地址,源主机 MAC 地址,目的主机的 IP 地址。
当本网络的所有主机收到该 ARP 数据包时,首先检查数据包中的 IP 地址是否是自己的 IP 地址,如果不是,则忽略该数据包,如果是,则首先从数据包中取出源主机的 IP 和 MAC 地址写入到 ARP 列表中,如果已经存在,则覆盖,然后将自己的 MAC 地址写入 ARP 响应包中,告诉源主机自己是它想要找的 MAC 地址。
源主机收到 ARP 响应包后。将目的主机的 IP 和 MAC 地址写入 ARP 列表,并利用此信息发送数据。如果源主机一直没有收到 ARP 响应数据包,表示 ARP 查询失败。
如果目标 IP 与自己不在同一个网段,这种情况需要将包发给默认网关,所以主要获取网关的 MAC 地址。如果 arp 高速缓存有默认网关的 MAC 地址,直接发送 IP 数据报道默认网关,再由网关转发到外网;如果 arp 高速缓存没有默认网关的 MAC 地址,还是发送 ARP 广播请求默认网关的 MAC 地址,缓存该地址,并且发送数据报到网关。
首先,每个主机都会在自己的 ARP 缓冲区中建立一个 ARP 列表,以表示 IP 地址和 MAC 地址之间的对应关系。当源主机要发送数据时,首先检查 ARP 列表中是否有对应 IP 地址的目的主机的 MAC 地址,如果有,则直接发送数据,如果没有,就向本网段的所有主机发送 ARP 数据包,该数据包包括的内容有:源主机 IP 地址,源主机 MAC 地址,目的主机的 IP 地址。
当本网络的所有主机收到该 ARP 数据包时,首先检查数据包中的 IP 地址是否是自己的 IP 地址,如果不是,则忽略该数据包,如果是,则首先从数据包中取出源主机的 IP 和 MAC 地址写入到 ARP 列表中,如果已经存在,则覆盖,然后将自己的 MAC 地址写入 ARP 响应包中,告诉源主机自己是它想要找的 MAC 地址。
源主机收到 ARP 响应包后。将目的主机的 IP 和 MAC 地址写入 ARP 列表,并利用此信息发送数据。如果源主机一直没有收到 ARP 响应数据包,表示 ARP 查询失败。
如果目标 IP 与自己不在同一个网段,这种情况需要将包发给默认网关,所以主要获取网关的 MAC 地址。如果 arp 高速缓存有默认网关的 MAC 地址,直接发送 IP 数据报道默认网关,再由网关转发到外网;如果 arp 高速缓存没有默认网关的 MAC 地址,还是发送 ARP 广播请求默认网关的 MAC 地址,缓存该地址,并且发送数据报到网关。
网络攻击
XSS攻击
原理:
XSS(cross-site scripting) 攻击:跨站脚本攻击,通过向某网站写入 js 脚本或插入恶意 html 标签来实现攻击。实现盗取各类用户帐号,如机器登录帐号、用户网银帐号、各类管理员帐号;或者控制企业数据,包括读取、篡改、添加、删除企业敏感数据的能力。
XSS(cross-site scripting) 攻击:跨站脚本攻击,通过向某网站写入 js 脚本或插入恶意 html 标签来实现攻击。实现盗取各类用户帐号,如机器登录帐号、用户网银帐号、各类管理员帐号;或者控制企业数据,包括读取、篡改、添加、删除企业敏感数据的能力。
攻击类型:
【存储性(持久型)】
常见的场景:
【存储性(持久型)】
- 存储型 XSS,也叫持久型 XSS,主要是将 XSS 代码发送到服务器(不管是数据库、内存还是文件系统等。),然后在下次请求页面的时候就不用带上 XSS 代码了。用户输入的带有恶意脚本的数据存储在服务器端。当浏览器请求数据时,服务器返回脚本并执行。
- 最典型的就是留言板 XSS。用户提交了一条包含 XSS 代码的留言到数据库。当目标用户查询留言时,那些留言的内容会从服务器解析之后加载出来。浏览器发现有 XSS 代码,就当做正常的 HTML 和 JS 解析执行。XSS 攻击就发生了。例如:张三发了一篇帖子,李四进行回复,但内容却是一段 js 脚本,这篇帖子被他人浏览的时候就会中招,盗用用户 cookie(session劫持) 等等操作。
- 反射型 XSS,也叫非持久型 XSS,把用户输入的数据 “反射” 给浏览器。通常是,用户点击链接或提交表单时,攻击者向用户访问的网站注入恶意脚本。XSS 代码出现在请求 URL 中,作为参数提交到服务器,服务器解析并响应。响应结果中包含 XSS 代码,最后浏览器解析并执行。
- 从概念上可以看出,反射型 XSS 代码是首先出现在 URL 中的,然后需要服务端解析,最后需要浏览器解析之后 XSS 代码才能够攻击。
常见的场景:
- 1、攻击者挖掘到有价值网站的反射型 xss 漏洞,通过业务分析,构建出有危害性的、执行特定目的的 payload 语句。
- 2、攻击者通过社交诱使受害者点击构造的包含 payload 的 url。比如通过 qq、邮件等发送,或者嵌入在某些网页中,包含诱使用户点击的内容,如美女或紧急消息,或者结合点击劫持技术。
- 3、网站接收受害者的请求及携带的 payload 数据,没有经过充分检查,将其作为返回数据直接返回或包含在返回的页面中。
- 4、受害者的浏览器接收响应,payload 作为脚本被成功执行。
防范方法:
1. 内容安全策略 (CSP)
实质上是白名单制度,开发者明确告诉客户端,哪些外部资源可以加载和执行,等同于提供白名单。它的实现和执行全部由浏览器完成,开发者只需提供配置。
常见的策略:
2.HttpOnly 阻止 Cookie 劫持攻击
注:发起 XSS 的攻击者既然可以通过注入恶意脚本获取用户的 Cookie 信息。所以,严格来说,HttpOnly 并非阻止 XSS 攻击,而是能阻止 XSS 攻击后的 Cookie 劫持攻击。
1. 内容安全策略 (CSP)
实质上是白名单制度,开发者明确告诉客户端,哪些外部资源可以加载和执行,等同于提供白名单。它的实现和执行全部由浏览器完成,开发者只需提供配置。
常见的策略:
- 入参字符过滤,在源头控制,把输入的一些不合法的东西都过滤掉,从而保证安全性。
- 出参进行编码,如果源头没控制好,就得后期补救了:像一些常见的符号,如 <> 在输出的时候要对其进行转换编码,这样做浏览器是不会对该标签进行解释执行的,同时也不影响显示效果。
- 入参长度限制,通过以上的案例我们不难发现 xss 攻击要能达成往往需要较长的字符串,因此对于一些可以预期的输入可以通过限制长度强制截断来进行防御。
2.HttpOnly 阻止 Cookie 劫持攻击
- 服务器端 Set-Cookie 字段设置 HttpOnly 参数,这样可以避免Cookie 劫持攻击。这时候,客户端的 Document.cookie API 无法访问带有 HttpOnly 标记的 Cookie, 但可以设置 cookie。
注:发起 XSS 的攻击者既然可以通过注入恶意脚本获取用户的 Cookie 信息。所以,严格来说,HttpOnly 并非阻止 XSS 攻击,而是能阻止 XSS 攻击后的 Cookie 劫持攻击。
CSRF攻击
原理:
CSRF (Cross Site Request Forgery),跨站请求伪造。
CSRF 攻击是攻击者借助用户的 Cookie 骗取服务器的信任,以用户名义伪造请求发送给服务器。如:在请求的 url 后加入一些恶意的参数
换句话说,CSRF 就是利用用户的登录态发起恶意请求。
CSRF (Cross Site Request Forgery),跨站请求伪造。
CSRF 攻击是攻击者借助用户的 Cookie 骗取服务器的信任,以用户名义伪造请求发送给服务器。如:在请求的 url 后加入一些恶意的参数
换句话说,CSRF 就是利用用户的登录态发起恶意请求。
攻击过程:
- 1.用户 C 打开浏览器,访问受信任网站 A,输入用户名和密码请求登录网站 A;
- 2.在用户信息通过验证后,网站 A 产生 Cookie 信息并返回给浏览器,此时用户登录网站 A 成功,可以正常发送请求到网站 A;
- 3.用户未退出网站 A 之前,在同一浏览器中,打开一个新的标签页访问网站 B;
- 4.网站 B 接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问站点 A;
- 5. 浏览器在接收到这些攻击性代码后,根据网站 B 的请求,在用户不知情的情况下携带 Cookie 信息,向网站 A 发出请求。网站 A 并不知道该请求其实是由 B 发起的,所以会根据用户 C 的 Cookie 信息以 C 的权限处理该请求,导致来自网站 B 的恶意代码被执行。
方法措施:
缺点:
Referer 的值是由浏览器提供的,虽然 HTTP 协议上有明确的要求,但是每个浏览器对于 Referer 的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。使用验证 Referer 值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上来讲,这样并不安全。
缺点:
在一个网站中,可以接受请求的地方非常多,要对于每一个请求都加上 token 是很麻烦的,并且很容易漏掉.该方法还有一个缺点是难以保证 token 本身的安全。特别是在一些论坛之类支持用户自己发表内容的网站,黑客可以在上面发布自己个人网站的地址。
总结:
XSS 是利用用户对指定网站的信任
CSRF 是利用网站对用户的信任
- 1.Referer Check
缺点:
Referer 的值是由浏览器提供的,虽然 HTTP 协议上有明确的要求,但是每个浏览器对于 Referer 的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。使用验证 Referer 值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上来讲,这样并不安全。
- 2. 添加 token 验证
缺点:
在一个网站中,可以接受请求的地方非常多,要对于每一个请求都加上 token 是很麻烦的,并且很容易漏掉.该方法还有一个缺点是难以保证 token 本身的安全。特别是在一些论坛之类支持用户自己发表内容的网站,黑客可以在上面发布自己个人网站的地址。
- 3.验证码
总结:
XSS 是利用用户对指定网站的信任
CSRF 是利用网站对用户的信任
DDOS攻击
原理:
全称 Distributed Denial of Service,中文意思为 “分布式拒绝服务”,就是利用大量合法的分布式服务器对目标发送请求,从而导致正常合法用户无法获得服务。通俗点讲就是利用网络节点资源如:IDC 服务器、个人 PC、手机、智能设备、打印机、摄像头等对目标发起大量攻击请求,从而导致服务器拥塞而无法对外提供正常服务,只能宣布 game over。
不同于其他恶意篡改数据或劫持类攻击,DDoS 简单粗暴,可以达到直接摧毁目标的目的。另外,相对其他攻击手段 DDoS 的技术要求和发动攻击的成本很低,只需要购买部分服务器权限或控制一批肉鸡即可,而且攻击相应速度很快,攻击效果可视。另一方面,DDoS 具有攻击易防守难的特征,服务提供商为了保证正常客户的需求需要耗费大量的资源才能和攻击发起方进行对抗。
全称 Distributed Denial of Service,中文意思为 “分布式拒绝服务”,就是利用大量合法的分布式服务器对目标发送请求,从而导致正常合法用户无法获得服务。通俗点讲就是利用网络节点资源如:IDC 服务器、个人 PC、手机、智能设备、打印机、摄像头等对目标发起大量攻击请求,从而导致服务器拥塞而无法对外提供正常服务,只能宣布 game over。
不同于其他恶意篡改数据或劫持类攻击,DDoS 简单粗暴,可以达到直接摧毁目标的目的。另外,相对其他攻击手段 DDoS 的技术要求和发动攻击的成本很低,只需要购买部分服务器权限或控制一批肉鸡即可,而且攻击相应速度很快,攻击效果可视。另一方面,DDoS 具有攻击易防守难的特征,服务提供商为了保证正常客户的需求需要耗费大量的资源才能和攻击发起方进行对抗。
攻击方式:
Syn Flood原理:攻击者首先伪造地址对 服务器发起 SYN 请求,服务器回应 (SYN+ACK) 包,而真实的 IP 会认为,我没有发送请求,不作回应。服务 器没有收到回应,这样的话,服务器不知 道 (SYN+ACK) 是否发送成功,默认情况下会重试 5 次(tcp_syn_retries)。这样的话,对于服务器的内存,带宽都有很大的消耗。攻击者 如果处于公网,可以伪造 IP 的话,对于服务器就很难根据 IP 来判断攻击者,给防护带来很大的困难。
web 的CC攻击:当一个网页访问的人数特别多的时候,打开网页就慢了,CC 就是模拟多个用户,多少线程就是多少用户,不停地进行访问那些需要大量数据操作,就是需要大量 CPU 时间的页面,造成服务器资源的浪费,CPU 长时间处于 100%,永远都有处理不完的连接直至就网络拥塞,正常的访问被中止。
- 1.资源消耗类攻击
Syn Flood原理:攻击者首先伪造地址对 服务器发起 SYN 请求,服务器回应 (SYN+ACK) 包,而真实的 IP 会认为,我没有发送请求,不作回应。服务 器没有收到回应,这样的话,服务器不知 道 (SYN+ACK) 是否发送成功,默认情况下会重试 5 次(tcp_syn_retries)。这样的话,对于服务器的内存,带宽都有很大的消耗。攻击者 如果处于公网,可以伪造 IP 的话,对于服务器就很难根据 IP 来判断攻击者,给防护带来很大的困难。
- 2.服务消耗性攻击
web 的CC攻击:当一个网页访问的人数特别多的时候,打开网页就慢了,CC 就是模拟多个用户,多少线程就是多少用户,不停地进行访问那些需要大量数据操作,就是需要大量 CPU 时间的页面,造成服务器资源的浪费,CPU 长时间处于 100%,永远都有处理不完的连接直至就网络拥塞,正常的访问被中止。
防护方式:
DDoS 的防护是一个技术和成本不对等的工程,往往一个业务的 DDoS 防御系统建设成本要比业务本身的成本或收益更加庞大,这使得很多创业公司或小型互联网公司不愿意做更多的投入。
DDoS 的防护系统本质上是一个基于资源较量和规则过滤的智能化系统:
DDoS 的防护是一个技术和成本不对等的工程,往往一个业务的 DDoS 防御系统建设成本要比业务本身的成本或收益更加庞大,这使得很多创业公司或小型互联网公司不愿意做更多的投入。
DDoS 的防护系统本质上是一个基于资源较量和规则过滤的智能化系统:
- 资源隔离:资源隔离可以看作是用户服务的一堵防护盾。
- 用户规则:根据流量类型、请求频率、数据包特征、正常业务之间的延时间隔等,判断用户是否为攻击行为。
- 大数据智能分析:基于对海量数据进行分析,进而对合法用户进行模型化,并利用这些指纹特征。
SQL注入攻击
原理:
SQL 注入(SQLi)是一种注入攻击,,可以执行恶意 SQL 语句。它通过将任意 SQL 代码插入数据库查询,使攻击者能够完全控制 Web 应用程序后面的数据库服务器。攻击者可以使用 SQL 注入漏洞绕过应用程序安全措施;可以绕过网页或 Web 应用程序的身份验证和授权,并检索整个 SQL 数据库的内容;还可以使用 SQL 注入来添加,修改和删除数据库中的记录。
SQL 注入(SQLi)是一种注入攻击,,可以执行恶意 SQL 语句。它通过将任意 SQL 代码插入数据库查询,使攻击者能够完全控制 Web 应用程序后面的数据库服务器。攻击者可以使用 SQL 注入漏洞绕过应用程序安全措施;可以绕过网页或 Web 应用程序的身份验证和授权,并检索整个 SQL 数据库的内容;还可以使用 SQL 注入来添加,修改和删除数据库中的记录。
SQL注入的类型:
(1)基于错误的 SQL 注入:从显示的错误消息中获取有关数据库的信息
(2)基于联合的 SQL 注入:依赖于攻击者能够将 UNION ALL 被盗信息的结果与合法结果连接起来。
这两种技术都依赖于攻击者修改应用程序发送的 SQL,以及浏览器中显示的错误和返回的信息。如果应用程序开发人员或数据库开发人员无法正确地参数化他们在查询中使用的值,那么它会成功。两者都是试错法,可以检测到错误。
- 1.带内注入
(1)基于错误的 SQL 注入:从显示的错误消息中获取有关数据库的信息
(2)基于联合的 SQL 注入:依赖于攻击者能够将 UNION ALL 被盗信息的结果与合法结果连接起来。
这两种技术都依赖于攻击者修改应用程序发送的 SQL,以及浏览器中显示的错误和返回的信息。如果应用程序开发人员或数据库开发人员无法正确地参数化他们在查询中使用的值,那么它会成功。两者都是试错法,可以检测到错误。
- 2.盲注入
- 3.带外注入
防止SQL注入:
- 1.不要使用动态 SQL
- 2.不要将敏感数据保留在纯文本中
- 3.限制数据库权限和特权
- 4.避免直接向用户显示数据库错误
- 5.对访问数据库的 Web 应用程序使用 Web 应用程序防火墙(WAF)
- 6.定期测试与数据库交互的 Web 应用程序
- 7.将数据库更新为最新的可用修补程序
操作系统
操作系统
操作系统基础
什么是操作系统?
内核负责管理系统的进程、内存、设备驱动程序、文件和网络系统等等,决定着系统的性能和稳定性。是连接应用程序和硬件的桥梁。 内核就是操作系统背后黑盒的核心。
- 操作系统(Operating System,简称 OS)是管理计算机硬件与软件资源的程序,是计算机系统的内核与基石;
- 操作系统本质上是运行在计算机上的软件程序 ;
- 操作系统为用户提供一个与系统交互的操作界面 ;
- 操作系统分内核与外壳(我们可以把外壳理解成围绕着内核的应用程序,而内核就是能操作硬件的程序)。
内核负责管理系统的进程、内存、设备驱动程序、文件和网络系统等等,决定着系统的性能和稳定性。是连接应用程序和硬件的桥梁。 内核就是操作系统背后黑盒的核心。
什么是系统调用?
了解系统调用之前,需要了解用户态和系统态:
系统调用:
从用户态到系统态需要系统调用完成。我们运行的程序基本都是运行在用户态,如果我们调用操作系统提供的系统态级别的子功能,就需要系统调用。也就是说我们运行的用户程序中,凡是与系统态级别的资源有关的操作(如文件管理、进程管理。内存管理等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。
系统调用过程中也会发生CPU上下文切换。CPU寄存器会先保存用户态的状态,然后加载内核态相关内容。系统调用结束后,CPU寄存器要恢复原来保存的用户态,继续运行经常。所以,一次系统调用,发生两次CPU上下文切换。
注意:系统调用过程中,不涉及虚拟内存等进程用户态的资源,也不会切换进程。与通常所说的进程上下文切换不同:
系统调用按功能分类:
- 用户态:用户态运行的进程或者可以直接读取用户程序的数据。
- 系统态:简单理解为系统态运行的进程或程序几乎可以访问计算机的任何资源,不受限制。
系统调用:
从用户态到系统态需要系统调用完成。我们运行的程序基本都是运行在用户态,如果我们调用操作系统提供的系统态级别的子功能,就需要系统调用。也就是说我们运行的用户程序中,凡是与系统态级别的资源有关的操作(如文件管理、进程管理。内存管理等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。
系统调用过程中也会发生CPU上下文切换。CPU寄存器会先保存用户态的状态,然后加载内核态相关内容。系统调用结束后,CPU寄存器要恢复原来保存的用户态,继续运行经常。所以,一次系统调用,发生两次CPU上下文切换。
注意:系统调用过程中,不涉及虚拟内存等进程用户态的资源,也不会切换进程。与通常所说的进程上下文切换不同:
- 进程上下文切换是指,从一个进程切换到另一个进程。
- 系统调用过程中一直是同一个进程在运行。
系统调用按功能分类:
- 设备管理:完成设备的请求或释放,以及设备启动等功能。
- 文件管理:完成文件的读、写、创建及删除等功能。
- 进程控制:完成进程的创建、撤销、阻塞及唤醒等功能。
- 进程通信:完成进程之间的消息传递或信号传递等功能。
- 内存管理:完成内存分配、回收以及获取作业占用内存区大小及地址等功能。
进程和线程
进程和线程定义
- 程序:含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是程序的静态的代码。
- 进程: 程序的一次执行过程,是系统分配资源的最小单元,包含一个或者多个线程。
- 线程: 是系统调度的最小单元。线程不能独立存在,它必须是进程的一部分。
进程和线程区别
- 进程有自己的独立地址空间,线程没有
- 进程是资源分配的最小单位,线程是CPU调度的最小单位。
- 进程和线程通信的方式不同(线程之间的通信比较方便。同一进程下的线程共享数据,比如全局变量,静态变量,通过这些数据来通信不仅快捷而且方便,当然如何处理好这些访问的同步与互斥正是多线程程序的难点。而进程之间的通信只能通过进程通信的方式进行。)
- 进程上下文切换开销大,线程开销小;对进程操作一般开销比较大,对线程开销比较小。
- 一个进程挂掉了不会影响其他线程,而线程挂掉了可能会影响其他线程
上下文切换
对于单核单线程 CPU 而言,在某一时刻只能执行一条 CPU 指令。上下文切换 (Context Switch) 是一种将 CPU 资源从一个进程分配给另一个进程的机制。从用户角度看,计算机能够并行运行多个进程,这恰恰是操作系统通过快速上下文切换造成的结果。在切换的过程中,操作系统需要先存储当前进程的状态 (包括内存空间的指针,当前执行完的指令等等),再读入下一个进程的状态,然后执行此进程。
CPU 的上下文切换分三种:进程上下文切换、线程上下文切换、中断上下文切换。
CPU 的上下文切换分三种:进程上下文切换、线程上下文切换、中断上下文切换。
进程上下文切换
进程是由内核管理和调度的,进程的切换只能发生在内核态。因此,进程的上下文不但包括虚拟内存。栈、全局变量等用户空间资源,还包括内核堆栈。寄存器等内核空间状态。所以,进程的上下文切换比系统调用多了一个步骤:保存当前进程的内核状态和CPU寄存器之前,先把该进程的虚拟内存、栈等保存起来;加载下一个进程的内核态后,还需要刷新进程的虚拟内存和用户栈。保存上下文和恢复上下文需要在内核在CPU上运行才能完成。
进程上下文切换场景:
进程上下文切换场景:
- 进程时间片耗尽;
- 系统资源不足;
- 进程通过睡眠函数sleep把自己挂起来;
- 当有优先级更高的进程运行时,为了去运行优先级更高的进程,当前进程会被挂起;
- 发生硬中断,CPU上的进程会被挂起,然后去执行内核中的中断服务进程。
线程上下文切换
线程是调度的基本单位,而进程则是资源拥有的基本单位。
内核中的任务调度实际上是在调度线程,进程只是给线程提供虚拟内存、全局变量等资源。
线程上下文切换时,共享相同的虚拟内存和全局变量等资源不需要修改。而线程自己私有的数据,如栈和寄存器等,上下文切换时需要保存。
线程切换的两种情况:
内核中的任务调度实际上是在调度线程,进程只是给线程提供虚拟内存、全局变量等资源。
线程上下文切换时,共享相同的虚拟内存和全局变量等资源不需要修改。而线程自己私有的数据,如栈和寄存器等,上下文切换时需要保存。
线程切换的两种情况:
- 前后两个线程属于不同进程;
- 前后两个线程属于同一个进程(速度更快,消耗更小资源)
中断上下文切换
为了快速响应硬件事件,中断处理会打断进程的正常调度和执行,然后调用中断处理程序响应设备事件。在打断其他进程时,需要先将进程的当前状态保存下来,等中断结束后,进程仍然可以恢复过来。
跟进程的上下文不同,中断上下文切换不涉及进程的用户态。所以,即便中断过程打断了一个在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文切换只包括内核态中断服务程序执行所必须的状态,也就是CPU寄存器、内核堆栈、硬件中断参数等(内核态信息)
对同一个CPU来说,中断处理比进程拥有更高的优先级,所以中断上下文切换不会与进程上下文切换同时发生。并且,由于中断会打断正常进程的调度和执行,所以大部分中断处理程序都短小精悍,以便可以尽快完成。
跟进程的上下文不同,中断上下文切换不涉及进程的用户态。所以,即便中断过程打断了一个在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文切换只包括内核态中断服务程序执行所必须的状态,也就是CPU寄存器、内核堆栈、硬件中断参数等(内核态信息)
对同一个CPU来说,中断处理比进程拥有更高的优先级,所以中断上下文切换不会与进程上下文切换同时发生。并且,由于中断会打断正常进程的调度和执行,所以大部分中断处理程序都短小精悍,以便可以尽快完成。
僵尸进程和孤儿进程
僵尸进程
定义:
一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或者waited获取子进程的状态信息,那么子进程的进程描述符等一系列信息还会保存在系统中。这种进程称之为僵尸进程。
危害:
在Unix系统管理中,当用ps命令观察进程的状态时,经常看到某些进程的状态栏为defunct这就是所谓的僵尸进程。僵尸进程是一个早已死亡的进程,但是在进程表(process table)中仍占了一个位置。由于进程表的容量是有限的,所以defunct进程不仅会占用系统的内存资源,影响系统的性能,而且如果其数目太多,还是会导致系统瘫痪。
处理方法:
一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或者waited获取子进程的状态信息,那么子进程的进程描述符等一系列信息还会保存在系统中。这种进程称之为僵尸进程。
危害:
在Unix系统管理中,当用ps命令观察进程的状态时,经常看到某些进程的状态栏为defunct这就是所谓的僵尸进程。僵尸进程是一个早已死亡的进程,但是在进程表(process table)中仍占了一个位置。由于进程表的容量是有限的,所以defunct进程不仅会占用系统的内存资源,影响系统的性能,而且如果其数目太多,还是会导致系统瘫痪。
处理方法:
- 改写父进程,在子进程死后要为它收尸。具体的做法是接管SSIGCHLD信号。子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信号后,执行waitpid()函数为子进程收尸。原理:就算父进程没有调用wait,内核也会发送SJGCHLD消息,尽管默认处理是忽略,如果想响应这个消息,可以设置一个处理函数。
- 把父进程杀掉。父进程死后,僵尸进程成为“孤儿进程”,过继给1号进程init,init始终会负责清理僵尸进程,他产生的所有僵尸进程也会跟着消失。
孤儿进程
父进程运行结束,但是子进程还在运行(未运行结束)的子进程就称为孤儿进程。
孤儿进程最终会被init进程(进程号为1)所收养,因此init进程此时变成孤儿进程的父进程,并由init进程对他们完成状态收集工作。(Linux中,init是内核启动的第一个用户进程,init有许多很重要的任务,比如像启动getty(用于用户登录)、实现运行级别、以及处理孤儿进程。)
孤儿进程最终会被init进程(进程号为1)所收养,因此init进程此时变成孤儿进程的父进程,并由init进程对他们完成状态收集工作。(Linux中,init是内核启动的第一个用户进程,init有许多很重要的任务,比如像启动getty(用于用户登录)、实现运行级别、以及处理孤儿进程。)
进程间通信方式
概念:
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟出一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC)
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟出一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC)
管道/匿名管道
管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。写进程在管道的尾端写入数据,读进程在管道的道端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数据。管道提供了简单的流控制机制。进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样地,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。无名管道无需显示打开,创建时直接返回文件描述符,在读写时需要确定对方的存在,否则将退出。
局限:
局限:
- 只支持单向数据流
- 只能用于具有亲缘关系(父子进程或兄弟进程)的进程之间
- 没有名字
- 管道的缓冲区是有限的(管道只存在于内存中,在管道创建的时候,为缓冲区分配一个页面大小)
- 管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令。记录)等等;
有名管道
匿名管道由于没有名字,只能用于亲缘关系的进程间通信、为了克服这个缺点,就出现了有名管道。
有名管道不同于匿名管道之处在于他提供了一个路径名与之关联,以有名管道的文件形式存在于文件系统中,这样,即使与有名管道的创建进程不存在亲缘关系的进程,只要访问该路径就能够彼此通过有名管道相互通信,因此,通过有名管道不相关的进程也能交换数据。值得注意的是,有名管道严格遵守先进先出,对匿名管道及有名管道的读总是从开始处返回数据,对它们的写则是把数据添加到末尾。它们不支持诸如seek()等文件定位操作。有名管道的名字存在于文件系统中,内容存放在内存中。
有名管道在打开时需要确实对方的存在,否则将阻塞。即以读方式打开某管道,在此之前必须一个进程以写方式打开管道,否则阻塞。此外,可以以读写(O_RDWR)模式打开有名管道,即当前进程读,当前进程写,不会阻塞。
有名管道不同于匿名管道之处在于他提供了一个路径名与之关联,以有名管道的文件形式存在于文件系统中,这样,即使与有名管道的创建进程不存在亲缘关系的进程,只要访问该路径就能够彼此通过有名管道相互通信,因此,通过有名管道不相关的进程也能交换数据。值得注意的是,有名管道严格遵守先进先出,对匿名管道及有名管道的读总是从开始处返回数据,对它们的写则是把数据添加到末尾。它们不支持诸如seek()等文件定位操作。有名管道的名字存在于文件系统中,内容存放在内存中。
有名管道在打开时需要确实对方的存在,否则将阻塞。即以读方式打开某管道,在此之前必须一个进程以写方式打开管道,否则阻塞。此外,可以以读写(O_RDWR)模式打开有名管道,即当前进程读,当前进程写,不会阻塞。
信号
信号是Linux系统中用于进程之间互相通信或操作的一种机制,信号可以在任何时候发送给某一进程而不需知道该进程的状态。
如果该进程当前并未处于执行状态,则该信号就有内核保存起来,知道该进程恢复执行并传给它为止。
如果一个信号被进程设置为阻塞,则该信号的传递被延迟,知道其阻塞被取消时才被传递给进程。
如果该进程当前并未处于执行状态,则该信号就有内核保存起来,知道该进程恢复执行并传给它为止。
如果一个信号被进程设置为阻塞,则该信号的传递被延迟,知道其阻塞被取消时才被传递给进程。
消息队列
是一个在系统内核中用来保存消息的队列,它在系统内核中是以消息链表的形式出现的。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
共享内存
共享内存允许两个或多个进程访问同一个逻辑内存。这一段内存可以被两个或两个以上的进程映射至自身的地址空间中,一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取读出,从而实现了进程间的通信。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。共享内存是最快的 IPC 方式,它是针对其它进程间通信方式运行效率低而专门设计的。它往往与其它通信机制(如信号量)配合使用,来实现进程间的同步和通信。
信号量
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其它进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
套接字
套接字也是一种进程间通信机制,与其它通信机制不同的是,它可用于不同机器间的进程通信。
进程调度与算法
概念
CPU 任务 可以分为交互式任务和批处理任务,调度最终的目标是合理的使用 CPU,使得交互式任务的响应时间尽可能短,用户不至于感到延迟,同时使得批处理任务的周转时间尽可能短,减少用户等待的时间。
- 响应时间(交互式任务): 从用户输入到产生反应的时间
- 周转时间(批处理任务): 从任务开始到任务结束的时间
进程调度算法
交互式任务:
批处理任务:
- 时间片轮转
- 多级调度队列
- 多级反馈队列
批处理任务:
- 先来先服务(FCFS)
- 短作业优先(SJF)
- 最短剩余时间优先(SRTN)
- 优先权调度
- 时间片轮转算法:时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法,又称 RR(Round robin)调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
- 多级调度队列算法:按照一定的规则建立多个进程队列,不同的队列有固定的优先级,不同的队列也可以给不同的时间片和采用不同的调度方法,存在问题:没法区分I/O bound和CPU bound、存在一定程度的“饥饿”现象。
- 先来先服务算法: 从就绪队列中选择一个最先进入该队列的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
- 短作业优先算法:从就绪队列中选出一个估计运行时间最短的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
- 优先权调度:为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。存在“饥饿”现象。
- 多级反馈队列算法:前面介绍的几种进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程 。多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。因而它是目前被公认的一种较好的进程调度算法,UNIX 操作系统采取的便是这种调度算法。
互斥和同步
互斥:
指散布在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。如果用对资源的访问来定义的话,互斥某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:
指在不同进程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。如果用对资源的访问来定义的话,同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
指散布在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。如果用对资源的访问来定义的话,互斥某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:
指在不同进程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。如果用对资源的访问来定义的话,同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
实现互斥与同步的方式:
- 互斥量(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。
- 信号量(Semphares) :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量
- 事件(Event) :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操
操作系统内存管理
内存管理主要是做什么的?
内存管理主要负责内存的分配与回收(malloc 函数:申请内存,free 函数:释放内存),另外地址转换也就是将逻辑地址转换成相应的物理地址等功能也是操作系统内存管理做的事情。
常见的内存管理机制
简单分为连续分配管理方式和非连续分配管理方式这两种。连续分配管理方式是指为一个用户程序分配一个连续的内存空间,常见的如 块式管理 。同样地,非连续分配管理方式允许一个程序使用的内存分布在离散或者说不相邻的内存中,常见的如页式管理 和 段式管理。
段页式管理机制:段页式管理机制结合了段式管理和页式管理的优点。简单来说段页式管理机制就是把主存先分成若干段,每个段又分成若干页,也就是说 段页式管理机制 中段与段之间以及段的内部的都是离散的。
- 块式管理:远古时代的计算机操系统的内存管理方式。将内存分为几个固定大小的块,每个块中只包含一个进程。如果程序运行需要内存的话,操作系统就分配给它一块,如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了。这些在每个块中未被利用的空间,我们称之为碎片。
- 页式管理:把主存分为大小相等且固定的一页一页的形式,页较小,相对相比于块式管理的划分力度更大,提高了内存利用率,减少了碎片。页式管理通过页表对应逻辑地址和物理地址。
- 段式管理:页式管理虽然提高了内存利用率,但是页式管理其中的页实际并无任何实际意义。 段式管理把主存分为一段段的,每一段的空间又要比一页的空间小很多 。但是,最重要的是段是有实际意义的,每个段定义了一组逻辑信息,例如,有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 段式管理通过段表对应逻辑地址和物理地址。
段页式管理机制:段页式管理机制结合了段式管理和页式管理的优点。简单来说段页式管理机制就是把主存先分成若干段,每个段又分成若干页,也就是说 段页式管理机制 中段与段之间以及段的内部的都是离散的。
快表和多级页表
快表:
为了解决虚拟地址到物理地址的转换速度,操作系统在 页表方案 基础之上引入了 快表 来加速虚拟地址到物理地址的转换。我们可以把块表理解为一种特殊的高速缓冲存储器(Cache),其中的内容是页表的一部分或者全部内容。作为页表的 Cache,它的作用与页表相似,但是提高了访问速率。由于采用页表做地址转换,读写内存数据时 CPU 要访问两次主存。有了快表,有时只要访问一次高速缓冲存储器,一次主存,这样可加速查找并提高指令执行速度。
使用快表之后的地址转换流程是这样的:
为了解决虚拟地址到物理地址的转换速度,操作系统在 页表方案 基础之上引入了 快表 来加速虚拟地址到物理地址的转换。我们可以把块表理解为一种特殊的高速缓冲存储器(Cache),其中的内容是页表的一部分或者全部内容。作为页表的 Cache,它的作用与页表相似,但是提高了访问速率。由于采用页表做地址转换,读写内存数据时 CPU 要访问两次主存。有了快表,有时只要访问一次高速缓冲存储器,一次主存,这样可加速查找并提高指令执行速度。
使用快表之后的地址转换流程是这样的:
- 根据虚拟地址中的页号查快表;
- 如果该页在快表中,直接从快表中读取相应的物理地址;
- 如果该页不在快表中,就访问内存中的页表,再从页表中得到物理地址,同时将页表中的该映射表项添加到快表中;
- 当快表填满后,又要登记新页时,就按照一定的淘汰策略淘汰掉快表中的一个页。
多级页表:
引入多级页表的主要目的是为了避免把全部页表一直放在内存中占用过多空间,特别是那些根本就不需要的页表就不需要保留在内存中。
引入多级页表的主要目的是为了避免把全部页表一直放在内存中占用过多空间,特别是那些根本就不需要的页表就不需要保留在内存中。
为了提高内存的空间性能,提出了多级页表的概念;但是提到空间性能是以浪费时间性能为基础的,因此为了补充损失的时间性能,提出了快表(即 TLB)的概念。 不论是快表还是多级页表实际上都利用到了程序的局部性原理,局部性原理在后面的虚拟内存这部分会介绍到。
分页和分段的共同点和区别
共同点:
区别:
- 分页机制和分段机制都是为了提高内存利用率,较少内存碎片。
- 页和段都是离散存储的,所以两者都是离散分配内存的方式。但是,每个页和段中的内存是连续的。
区别:
- 页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序。
- 分页仅仅是为了满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中可以体现为代码段,数据段,能够更好满足用户的需要。
逻辑(虚拟)地址和物理地址
我们编程一般只有可能和逻辑地址打交道,比如在 C 语言中,指针里面存储的数值就可以理解成为内存里的一个地址,这个地址也就是我们说的逻辑地址,逻辑地址由操作系统决定。物理地址指的是真实物理内存中地址,更具体一点来说就是内存地址寄存器中的地址。物理地址是内存单元真正的地址。
为什么要有虚拟地址?
如果直接把物理地址暴露出来的话会带来严重问题,比如可能对操作系统造成伤害以及给同时运行多个程序造成困难。
例如:
例如:
- 用户程序可以访问任意内存,寻址内存的每个字节,这样就很容易(有意或者无意)破坏操作系统,造成操作系统崩溃。
- 想要同时运行多个程序特别困难,比如你想同时运行一个微信和一个 QQ 音乐都不行。为什么呢?举个简单的例子:微信在运行的时候给内存地址 1xxx 赋值后,QQ 音乐也同样给内存地址 1xxx 赋值,那么 QQ 音乐对内存的赋值就会覆盖微信之前所赋的值,这就造成了微信这个程序就会崩溃。
虚拟地址访问内存的优势
- 程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。
- 程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。当物理内存的供应量变小时,内存管理器会将物理内存页(通常大小为 4 KB)保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动。
- 不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。
CPU寻址
现代处理器使用的是一种称为 虚拟寻址(Virtual Addressing) 的寻址方式。使用虚拟寻址,CPU 需要将虚拟地址翻译成物理地址,这样才能访问到真实的物理内存。 实际上完成虚拟地址转换为物理地址转换的硬件是 CPU 中含有一个被称为 内存管理单元(Memory Management Unit, MMU) 的硬件
虚拟内存
什么是虚拟内存?
这个在我们平时使用电脑特别是 Windows 系统的时候太常见了。很多时候我们使用点开了很多占内存的软件,这些软件占用的内存可能已经远远超出了我们电脑本身具有的物理内存。为什么可以这样呢? 正是因为虚拟内存 可以让程序可以拥有超过系统物理内存大小的可用内存空间。另外,虚拟内存为每个进程提供了一个一致的、私有的地址空间,它让每个进程产生了一种自己在独享主存的错觉(每个进程拥有一片连续完整的内存空间)。这样会更加有效地管理内存并减少出错。
虚拟内存是计算机系统内存管理的一种技术,我们可以手动设置自己电脑的虚拟内存。不要单纯认为虚拟内存只是“使用硬盘空间来扩展内存“的技术。虚拟内存的重要意义是它定义了一个连续的虚拟地址空间,并且 把内存扩展到硬盘空间。
虚拟内存 使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如 RAM)的使用也更有效率。目前,大多数操作系统都使用了虚拟内存,如 Windows 家族的“虚拟内存”;Linux 的“交换空间”等。
虚拟内存是计算机系统内存管理的一种技术,我们可以手动设置自己电脑的虚拟内存。不要单纯认为虚拟内存只是“使用硬盘空间来扩展内存“的技术。虚拟内存的重要意义是它定义了一个连续的虚拟地址空间,并且 把内存扩展到硬盘空间。
虚拟内存 使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如 RAM)的使用也更有效率。目前,大多数操作系统都使用了虚拟内存,如 Windows 家族的“虚拟内存”;Linux 的“交换空间”等。
局部性原理
早在 1968 年的时候,就有人指出我们的程序在执行的时候往往呈现局部性规律,也就是说在某个较短的时间段内,程序执行局限于某一小部分,程序访问的存储空间也局限于某个区域。
局部性原理表现在以下两个方面:
时间局部性是通过将近来使用的指令和数据保存到高速缓存存储器中,并使用高速缓存的层次结构实现。空间局部性通常是使用较大的高速缓存,并将预取机制集成到高速缓存控制逻辑中实现。虚拟内存技术实际上就是建立了 “内存一外存”的两级存储器的结构,利用局部性原理实现髙速缓存。
局部性原理表现在以下两个方面:
- 时间局部性 :如果程序中的某条指令一旦执行,不久以后该指令可能再次执行;如果某数据被访问过,不久以后该数据可能再次被访问。产生时间局部性的典型原因,是由于在程序中存在着大量的循环操作。
- 空间局部性 :一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即程序在一段时间内所访问的地址,可能集中在一定的范围之内,这是因为指令通常是顺序存放、顺序执行的,数据也一般是以向量、数组、表等形式簇聚存储的。
时间局部性是通过将近来使用的指令和数据保存到高速缓存存储器中,并使用高速缓存的层次结构实现。空间局部性通常是使用较大的高速缓存,并将预取机制集成到高速缓存控制逻辑中实现。虚拟内存技术实际上就是建立了 “内存一外存”的两级存储器的结构,利用局部性原理实现髙速缓存。
虚拟寄存器
基于局部性原理,在程序装入时,可以将程序的一部分装入内存,而将其他部分留在外存,就可以启动程序执行。由于外存往往比内存大很多,所以我们运行的软件的内存大小实际上是可以比计算机系统实际的内存大小大的。在程序执行过程中,当所访问的信息不在内存时,由操作系统将所需要的部分调入内存,然后继续执行程序。另一方面,操作系统将内存中暂时不使用的内容换到外存上,从而腾出空间存放将要调入内存的信息。这样,计算机好像为用户提供了一个比实际内存大的多的存储器——虚拟存储器。
实际上,我觉得虚拟内存同样是一种时间换空间的策略,你用 CPU 的计算时间,页的调入调出花费的时间,换来了一个虚拟的更大的空间来支持程序的运行。不得不感叹,程序世界几乎不是时间换空间就是空间换时间。
实际上,我觉得虚拟内存同样是一种时间换空间的策略,你用 CPU 的计算时间,页的调入调出花费的时间,换来了一个虚拟的更大的空间来支持程序的运行。不得不感叹,程序世界几乎不是时间换空间就是空间换时间。
虚拟内存技术实现
虚拟内存的实现需要建立在离散分配的内存管理方式的基础上。
- 请求分页存储管理 :建立在分页管理之上,为了支持虚拟存储器功能而增加了请求调页功能和页面置换功能。请求分页是目前最常用的一种实现虚拟存储器的方法。请求分页存储管理系统中,在作业开始运行之前,仅装入当前要执行的部分段即可运行。假如在作业运行的过程中发现要访问的页面不在内存,则由处理器通知操作系统按照对应的页面置换算法将相应的页面调入到主存,同时操作系统也可以将暂时不用的页面置换到外存中。
- 请求分段存储管理 :建立在分段存储管理之上,增加了请求调段功能、分段置换功能。请求分段储存管理方式就如同请求分页储存管理方式一样,在作业开始运行之前,仅装入当前要执行的部分段即可运行;在执行过程中,可使用请求调入中断动态装入要访问但又不在内存的程序段;当内存空间已满,而又需要装入新的段时,根据置换功能适当调出某个段,以便腾出空间而装入新的段。
页面置换算法
地址映射过程中,若在页面中发现所要访问的页面不在内存中,则发生缺页中断 。
缺页中断 就是要访问的页不在主存,需要操作系统将其调入主存后再进行访问。 在这个时候,被内存映射的文件实际上成了一个分页交换文件。
当发生缺页中断时,如果当前内存中并没有空闲的页面,操作系统就必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。用来选择淘汰哪一页的规则叫做页面置换算法,我们可以把页面置换算法看成是淘汰页面的规则。
常见页面置换算法:
缺页中断 就是要访问的页不在主存,需要操作系统将其调入主存后再进行访问。 在这个时候,被内存映射的文件实际上成了一个分页交换文件。
当发生缺页中断时,如果当前内存中并没有空闲的页面,操作系统就必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。用来选择淘汰哪一页的规则叫做页面置换算法,我们可以把页面置换算法看成是淘汰页面的规则。
常见页面置换算法:
- OPT 页面置换算法(最佳页面置换算法) :最佳(Optimal, OPT)置换算法所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。但由于人们目前无法预知进程在内存下的若千页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现。一般作为衡量其他置换算法的方法。
- FIFO(First In First Out) 页面置换算法(先进先出页面置换算法) : 总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。
- LRU (Least Currently Used)页面置换算法(最近最久未使用页面置换算法) :LRU算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰。
- LFU (Least Frequently Used)页面置换算法(最少使用页面置换算法) : 该置换算法选择在之前时期使用最少的页面作为淘汰页。
Linux
Linux常用命令
目录切换命令
- cd usr :切换到该目录下的usr目录。
- cd ..(或cd ../):切换到上一层目录。
- cd / :切换到系统根目录。
- cd ~ :切换到用户主目录。
- cd - :切换到上一个操作所在的目录。
目录操作命令(增删查改)
1.列出当前目录及子目录下所有的文件和文件夹:find;
2.在/home目录下查找以.txt结尾的文件名:find /home -name "*.txt";但忽略大小写:find /home -name "*.txt"。
3.当前目录及子目录下查找所有以.txt和.pdf结尾的文件:find . \(-name "*.txt" -o -name "*.pdf"\)或find . -name "*.txt" -o -name "*.pdf"
- mkdir 目录名称 :增加目录。(增)
- ls或者ll(ll 是 ls -l的别名,可以看到该目录下的所有目录和文件的详细信息): 查看目录信息(查)
- find 目录 参数 :寻找目录(查)
1.列出当前目录及子目录下所有的文件和文件夹:find;
2.在/home目录下查找以.txt结尾的文件名:find /home -name "*.txt";但忽略大小写:find /home -name "*.txt"。
3.当前目录及子目录下查找所有以.txt和.pdf结尾的文件:find . \(-name "*.txt" -o -name "*.pdf"\)或find . -name "*.txt" -o -name "*.pdf"
- mv 目录名称 新目录名称 :修改目录的名称(改)
- mv 目录名称 目录的新位置 :移动目录的位置(改)
- cp -r 目录名称 目录拷贝的目标位置:拷贝目录,-r代表递归拷贝。(改)
- rm [-rf] 目录:删除目录(删)
文件操作命令(增删查改)
more :可以显示百分比,回车可以向下一行,空格可以向下一页,q可以退出查看
less :可以使用键盘上的PgUp和PgDn和向下翻页,q可以退出查看
tail-10 :查看文件的后10行,Ctrl+C结束
tail -f :可以对某个文件进行动态监控,例如日志文件
- touch 文件名称 : 文件创建(增)
- cat/more/less/tail 文件名称 :文件查看(查)
more :可以显示百分比,回车可以向下一行,空格可以向下一页,q可以退出查看
less :可以使用键盘上的PgUp和PgDn和向下翻页,q可以退出查看
tail-10 :查看文件的后10行,Ctrl+C结束
tail -f :可以对某个文件进行动态监控,例如日志文件
- vim 文件 :修改文件内容(改)
- rm -rf 文件 :删除文件(删)
压缩文件的操作命令
c :打包文件
v :显示运行过程
f :指定文件名
例如:test目录下有三个文件分别是:aaa.txt、bbb.txt、ccc.txt,我们要打包test目录并指定压缩后的压缩包名称为test.tar.gz
tar -zcvf test.tar.gz aaa.txt bbb.txt ccc.txt或 tar -zcvf test.tar.gz /test/
例子:
将/test下的test.tar.gz解压到当前目录下 :tar -xvf test.gar.gz
将/test下的test.tar.gz解压到根目录下/usr下:tar -xvf test.gar.gz -C /usr
- tar -zcvf 打包压缩后的文件名 要打包压缩的文件 :打包并压缩文件
c :打包文件
v :显示运行过程
f :指定文件名
例如:test目录下有三个文件分别是:aaa.txt、bbb.txt、ccc.txt,我们要打包test目录并指定压缩后的压缩包名称为test.tar.gz
tar -zcvf test.tar.gz aaa.txt bbb.txt ccc.txt或 tar -zcvf test.tar.gz /test/
- tar -xvf 压缩包 -C 指定位置 :解压压缩包到指定位置
例子:
将/test下的test.tar.gz解压到当前目录下 :tar -xvf test.gar.gz
将/test下的test.tar.gz解压到根目录下/usr下:tar -xvf test.gar.gz -C /usr
Linux的权限命令
g :group,所属群组
o :other,其他用户
a :all,全部用户
r :读入权限,数字代号为“4”
w :写入权限,输在代号为“2”
x :执行或切换权限,数字代号为“1”(可运行)
-R :递归处理
例子:
chmod u+x,g+w f01 //为文件f01设置自己可以执行,组员可以写入的权限
chmod u=rwx,g=rw,o=r f01
chmod 764 f01 //7=4+2+1(ugo)
chmod a+x f01 //对文件f01的u,g,o都设置可执行属性
需要注意的是超级用户可以无视普通用户的权限,即使文件目录权限是000,依旧可以访问。 在linux中的每个用户必须属于一个组,不能独立于组外
- ls -l :查看某个目录下的文件和权限
- chmod [-R] u=rwx,g=rwx,o=rwx 文件名 :修改文件/目录的权限的命令
g :group,所属群组
o :other,其他用户
a :all,全部用户
r :读入权限,数字代号为“4”
w :写入权限,输在代号为“2”
x :执行或切换权限,数字代号为“1”(可运行)
-R :递归处理
例子:
chmod u+x,g+w f01 //为文件f01设置自己可以执行,组员可以写入的权限
chmod u=rwx,g=rw,o=r f01
chmod 764 f01 //7=4+2+1(ugo)
chmod a+x f01 //对文件f01的u,g,o都设置可执行属性
需要注意的是超级用户可以无视普通用户的权限,即使文件目录权限是000,依旧可以访问。 在linux中的每个用户必须属于一个组,不能独立于组外
Linux用户管理
- useradd 选项 用户名 :添加用户账号
- userdel 选项 用户名 :删除用户账号
- usermod 选项 用户名 :修改账号
- passwd 用户名 :更改或创建用户的密码
- passwd -S 用户名 :显示用户账号密码信息
- passwd -d 用户名 :清除用户密码
- pwd :显示当前位置
- sudo + 其他命令 :以管理员的什么执行指令
- grep 要搜索的字符串 要搜索的文件 --color :搜索命令,--color代表高亮显示
- ps -ef或ps -aux :这两个命令都是查看当前系统正在运行的进程,两者的区别是展示格式不同。
- ps aux|grep 字符 或 pgrep 字符 -a :查看包括某字符的进程
- kill -9 进程的pid :杀死进程(-9表示强制终止)
- ifconfig :查看当前系统的网卡信息
- ping :查看与某台机器的连接情况
- netstat -an :查看当前系统端口的使用
- shutdown -h now :现在立即关机
- shut +5 “System wil shutdown after 5 minutes”:指定5分钟后关机,同时发出消息给用户
- reboot :重开机
Linux系统用户组的管理
- groupadd 选项 用户组 :增加一个新的用户组
- groupdel 用户组 :删除一个已有的用户组
- groupmod 选项 用户组 :修改用户组的属性
其他常用命令
命令大全(http://man.linuxde.net/)
数据结构与算法
数组
链表
栈
队列
树
堆
图
Java必备
基础
数据类型
数据数据
包装类
Byte、Short、Integer、Long、Float、Double、Character、Boolean
基础语法
关键字与标识符
运算符
()的运算级别最高
程序逻辑控制
顺序,分支,循环
方法
重载和重写
注意:
- 方法的重载和重写都是实现多态的方式,区别在于重载实现的是编译时的多态性,重写实现的是运行时的多态性。
- 重载发生在一个类中,同名的烦恼歌发有不同的参数列表就视为重载;重写发生在子类和父类之间,重写要求子类重写的方法与父类被重写的方法有相同的返回类型。重载对返回类型没有要求。
注意:
- 重写的返回类型、方法名、参数列表必须相同,抛出异常的范围小于父类,访问修饰符的返回大于父类。
- 父类的方法访问修饰符为private、final、static则子类不能重写该方法,但是被static修饰的方法能够再次声明。
- 构造方法不能被重写
类与对象
面向对象的三大特征
封装、继承、多态(重载或重写实现)
反射
什么是反射?
JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。
获取Class对象的三种方式
- 类文字:Class<Test> testClass = Test.class;
- Class的forName()方法:Class testClass = Class.forName("com...Test");
- Object的getClass()方法:Class testClass = test.getClass();
优缺点
- 优点:运行期类型判断。动态加载类,提高代码的灵活度。
- 缺点:1.性能瓶颈:反射相当于一系列解释操作,通知JVM要做的事情,性能比直接的Java代码要慢很多。2.安全问题:我们动态操作改变类的属性同时也增加了安全隐患。
应用场景
- 1.我们在使用 JDBC 连接数据库时使用 Class.forName()通过反射加载数据库的驱动程序;
- 2.Spring 框架的 IOC(动态加载管理 Bean)创建对象以及 AOP(动态代理)功能都和反射有联系;
- 3.动态配置实例的属性;
接口和抽象类的区别
- 抽象类和接口都不能直接实例化。必须通过实现类来实例化。
- 接口的方法默认是pubilc,所有的方法在接口中不能有实现(Java8开始接口方法可以有默认实现,设置成default或static方法),而抽象类可以有非抽象的方法。
- 接口中除了static,final变量,不能有其他变量,抽象类可以。
- 一个类可以实现的多个接口,但只能实现一个抽象类,接口可以通过extends关键字扩展多个接口。
- 接口的默认方法是pubilc,抽象类中可以有public、private、portected。
- 抽象是对类的抽象,是一种模板设计;接口是对行为的抽象,是一种行为规范。
异常
Error(错误)和Exception(异常)
注意:异常和错误的区别:异常能被程序本身处理,错误是无法处理。
- Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java 虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
- Exception(异常):是程序本身可以处理的异常。Exception 类有一个重要的子类 RuntimeException。RuntimeException 异常由 Java 虚拟机抛出。NullPointerException(要访问的变量没有引用任何对象时,抛出该异常)、ArithmeticException(算术运算异常,一个整数除以 0 时,抛出该异常)和 ArrayIndexOutOfBoundsException (下标越界异常)。
注意:异常和错误的区别:异常能被程序本身处理,错误是无法处理。
异常处理
抛出异常、捕获异常
try-catch-finally
一般情况
下是用 try 来执行一段程序,如果系统会抛出(throw)一个异常对象,可以通过
它的类型来捕获(catch)它,或通过总是执行代码块(finally)来处理;try 用
来指定一块预防所有异常的程序;catch 子句紧跟在 try 块后面,用来指定你想要
捕获的异常的类型;throw 语句用来明确地抛出一个异常;
下是用 try 来执行一段程序,如果系统会抛出(throw)一个异常对象,可以通过
它的类型来捕获(catch)它,或通过总是执行代码块(finally)来处理;try 用
来指定一块预防所有异常的程序;catch 子句紧跟在 try 块后面,用来指定你想要
捕获的异常的类型;throw 语句用来明确地抛出一个异常;
throw和throws
throws 用来声明一个方法可能抛出的各种异常(当然声明异常时允许无病呻吟)而自己则不具体处理。
常见面试问题
==和equals
1.“==”用来比较两个变量(基本类型和对象类型)的值是否相等,如果两个变量是基本数据类型,直接比较值就可以;如果两个变量是对象数据类型,它们也是比较的值,只不过该值是两个对象在栈中的地址。
2.当对象是放在堆中的,栈中存放的是对象的引用(地址),由此可见 ‘==’ 是对栈中的值进行比较的。如果要比较堆中对象的内容是否相同,那么就要重写 equals 方法了。
3. Object 类中的 equals 方法就是用 ‘==’ 来比较的,所以如果没有重写 equals 方法(string是重写了equal方法的),equals 和 == 是等价的。通常我们会重写 equals 方法,让 equals 比较两个对象的内容,而不是比较对象的引用(地址)因为往往我们觉得比较对象的内容是否相同比比较对象的引用(地址)更有意义。
2.当对象是放在堆中的,栈中存放的是对象的引用(地址),由此可见 ‘==’ 是对栈中的值进行比较的。如果要比较堆中对象的内容是否相同,那么就要重写 equals 方法了。
3. Object 类中的 equals 方法就是用 ‘==’ 来比较的,所以如果没有重写 equals 方法(string是重写了equal方法的),equals 和 == 是等价的。通常我们会重写 equals 方法,让 equals 比较两个对象的内容,而不是比较对象的引用(地址)因为往往我们觉得比较对象的内容是否相同比比较对象的引用(地址)更有意义。
为什么重写equals时需要重写HashCode
- 1.是为了提高效率,采取重写hashcode方法,先进行hashcode比较,如果不同,那么就没必要在进行equals的比较了,这样就大大减少了equals比较的次数,
- 2.保证是同一个对象,如果重写了equals方法,而没有重写hashcode方法,可能就会出现两个没有关系的对象equals相同的,也就是会出现equals相等的对象,hashcode不相等的情况,重写hashcode方法就是为了避免这种情况的出现。
String、StringBuffer、StringBuilder
String、StringBuild、StringBuffer都是用来描述字符串的。因为String类是一个final类,每次修改字符串的时候实际上都是新创建了一个String,如果字符串修改频繁的话是比较占用内存的,所以就引入了StringBuffer和StringBuilder这两个没有被final修饰的可变字符串,这两个字符串类的区别是StringBuffer是线程安全的,StringBuilder是不安全的,因为StringBuilder所有方面没有被synchronized修饰。
深拷贝和浅拷贝
- 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
- 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
博客例子:
值传递和引用传递
- 值传递:值传递传递的是真实内容的一个副本,对副本的操作不影响原内容,也就是形参怎么变化,不会影响实参对应的内容。
- 引用传递: 引用也就是指向真实内容的地址值,在方法调用时,实参的地址通过方法调用被传递给相应的形参,在方法体内,形参和实参指向相同内存地址,对形参的操作会影响的真实内容。
容器
容器简介
List,Set,Map接口的区别
- Java集合类都位于java.util包下。
- 除了以Map结尾的类之外,其他类都实现了Controller接口,以Map结尾的实现类都实现了Map接口。
- List(对付顺序的好帮手):存储元素是有序的,可重复的。
- Set(注重独一无二的性质):存储元素是无序的,不可重复的。
- Map(用Key来搜索的专家):使用键值对(Key-Value)存储,类似数学上的函数y=f(x),“x”代表key,“y”代表Value。Key是无序的不可重复的,value是无序,可重复的,每个键最多映射到一个值。
使用集合的优点
- 集合提高了数据存储的灵活性 。
- 存储不同类型不同数量的对象 。
- 可以保存具有映射关系的数据 。
迭代器
Iterator 对象称为迭代器(设计模式的一种),迭代器可以对集合进行遍历,但每一个集合内部的数据结构可能是不尽相同的,所以每一个集合存和取都很可能是不一样的,虽然我们可以人为地在每一个类中定义 hasNext() 和 next() 方法,但这样做会让整个集合体系过于臃肿。于是就有了迭代器。
迭代器是将这样的方法抽取出接口,然后在每个类的内部,定义自己迭代方式,这样做就规定了整个集合体系的遍历方式都是 hasNext()和next()方法,使用者不用管怎么实现的,会用即可。迭代器的定义为:提供一种方法访问一个容器对象中各个元素,而又不需要暴露该对象的内部细节。
迭代器是将这样的方法抽取出接口,然后在每个类的内部,定义自己迭代方式,这样做就规定了整个集合体系的遍历方式都是 hasNext()和next()方法,使用者不用管怎么实现的,会用即可。迭代器的定义为:提供一种方法访问一个容器对象中各个元素,而又不需要暴露该对象的内部细节。
List
ArrayList
ArrayList的底层是一个Object[ ]数组,所以类似将数据放入一个动态的,可操作性的数组里面。
三种构造方法:
三种构造方法:
- ArrayList( ):刚初始化的时候,会是一个共享的类变量,也就是一个Object空数组,当第一次add的时候,这个数组就会被初始化一个大小为10的数组。
- ArrayList(int initialCapacity):用户指定的初始化容量大于0,就new一个相应大小的数组,如果指定的大小为0,就复制为共享的那个空的Object数组对象。如果小于0,就直接抛出异常。
- ArrayList(Collection<? extends E> c): 首先调用给定的collection的toArray方法将其转换成一个Array。 然后根据这个array的大小进行判断,如果不为0,就调用Arrays的copyOf的方法,复制到Object数组中,完成初始化,如果为0,就直接初始化为空的Object数组。
动态扩容原理
- 当我们不指定初始化的大小的时候,添加第一个元素的时候,数据会扩容到10。
- 当我们像一个ArrayList中添加数组的时候,首先会先检查数组中是不是有足够的空间来存储这个新添加的元素。如果有的话,那就什么都不用做,直接添加。如果空间不够用了,那么就根据原始的容量增加原始容量的一半。
- 扩容是需要调用grow()方法,grow方法将数组扩容为原数组的1.5倍(右移一位),然后调用Arrays.copy 方法。 在jdk6及之前的版本中,采用的还不是右移的方法
LinkedList
LinkedList是将数据以链表的形式存储。LinkedList的底层是一个双向链表。
两种构造方法:
两种构造方法:
- LinkedList():构造一个空链表
- LinkedList(Collection<? ectends E> c):构造一个指定集合的元素的列表。
添加元素原理
采用链表的尾插法进行插入,目的是让数据先入先出,保证数据插入的次序和输入顺序的一致。
Vector
Vector容器与ArrayList容器有点相似,他们都是用一个动态的,可操作的Object[ ]数组来存储数据。不同点是Vector是线程安全的,因为其内部有很多同步代码块来保证线程安全。未指定容量增量时默认扩容是原来的2倍。
4种构造方法:
4种构造方法:
- Vector():构造一个空向量,使其内部数据数组具有大小,10并且其标准容量增量为零。
- Vector(int initialCapacity):构造一个空向量,该向量具有指定的初始容量并且其容量增量等于零。
- Vector(int initialCapacity, int capacityIncrement):构造一个具有指定初始容量和容量增量的空向量。
- Vector(Collection<? extends E> c):构造一个向量,该向量包含指定集合的元素,其顺序由集合的迭代器返回。
原理
- 增:先判断容量是否足够存放新增元素,不足时先进行扩容,扩容是又会先判断是否指定容量增量系数,未指定时采用*2,指定是直接加上增量系数。当需要扩容时,先扩容,扩容之后采用Arrays,copyOf将老数组拷贝到新数组里面。
- 删:删除的话会先判断下标是否大于数组长度,如果大于抛异常。如果不大于,删除索引所在位置,采用本地方法Syetem.arraycopy()拷贝到一个新数组然后长度减一返回。
- 查:根据下标返回下标所在位置的值。
- 改:根据下标直接进行修改。
Set
HashSet
HashSet继承自Set接口,底层是基于HashMap实现的,主要存放的是一些无序的、不可重复的数据。他不是一个线程安全的容器。
LinkedHashSet
TreeSet
Map
HashMap
简介
HashMap 主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。
JDK 1.8之前HashMap由数组+链表实现,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。
JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间。(treeifyBin() 方法)
JDK 1.8之前HashMap由数组+链表实现,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。
JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间。(treeifyBin() 方法)
底层数据结构和源码分析
ConcurrentHashMap
工作原理
Java1.7 中 ConcruuentHashMap 使用的分段锁,也就是每一个 Segment 上同时只有一个线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,它的冲突会转化为链表。但是 Segment 的个数一但初始化就不能改变。
Java1.8 中的 ConcruuentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构也由 Java7 中的 Segment 数组 + HashEntry 数组 + 链表 进化成了 Node 数组 + 链表 / 红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。
Java1.8 中的 ConcruuentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构也由 Java7 中的 Segment 数组 + HashEntry 数组 + 链表 进化成了 Node 数组 + 链表 / 红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。
如何实现多线程同步
分析Hashtable就知道,synchronized是针对整张Hash表的,即每次锁住整张表让线程独占;
ConcurrentHashMap允许多个线程修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap 内部使用段(Segment)来表示这些不用的部分,每个段其实就是一个小的hashtable,它们有自己的锁。只要多个修改操作发生在不用的段上,它们就可以并发进行。有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整张表而不仅仅是某个段。这需要按顺序锁定所有段,操作完毕后,有按顺序释放所有段的锁。这里“按顺序”很重要,否则极有可能出现死锁。
ConcurrentHashMap允许多个线程修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap 内部使用段(Segment)来表示这些不用的部分,每个段其实就是一个小的hashtable,它们有自己的锁。只要多个修改操作发生在不用的段上,它们就可以并发进行。有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整张表而不仅仅是某个段。这需要按顺序锁定所有段,操作完毕后,有按顺序释放所有段的锁。这里“按顺序”很重要,否则极有可能出现死锁。
HashTable
并发
多线程基本概念
进程和线程
见操作系统进程与线程模块
线程的状态
- NEW(初始状态):线程已经被构建,但是没有调用start( )方法。
- RUNNABLE(运行状态):Java线程将RUNNING(运行中)和READY(就绪)通称为RUNNABLE(运行状态)。
- BLOCKED(阻塞状态):表示线程阻塞与锁。
- WAITING(等待状态):表示线程进入等待转态,需要等待当前线程或者其它线程做一个动作(通州和中断)。
- TIME_WAITINGN(超出等待状态):他可以在指定时间后自动唤醒。
- TERMINATED(终止状态):表示当前线程执行完毕。
同步和异步,阻塞和非阻塞、并发和并行
同步与异步是对应于调用者与被调用者,它们是线程之间的关系,两个线程之间要么是同步的,要么是异步的:
- 同步操作时:调用者需要等待被调用者返回结果,才会进行下一步操作。
- 异步操作时:调用者不需要等待被调用者返回调用,即可进行下一步操作,被调用者通常依靠事件、回调等机制来通知调用者结果。
阻塞与非阻塞是对于同一个线程来说的,在某个时刻,线程要么处于阻塞,要么处于非阻塞。阻塞和非阻塞关注的是程序在等待调用结果(消息、返回值)时的状态:
- 阻塞:阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
- 非阻塞:非阻塞调用是指不能立刻得到返回结果之前,该调用不会阻塞当前线程。
- 并发:在同一时刻只能有一条指令执行,但是多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但是在微观上并不是同时执行的,只是把时间片分成若干段,使得多个进程快速交替执行。单核(一个处理器)就行。
- 并行:在同一时刻,有多条指令在多个处理器上同时执行。无论是宏观还是微观,两者都是一起执行的。需要多核(多个处理器)。
用户线程和守护线程
守护线程是指在程序运行时在后台提供一种通用服务的线程。比如垃圾回收线程。这种线程并不是属于程序中不可或缺的部分。当所有的非守护线程结束时,程序也就会终止,同时会杀死进程中的所有守护线程。
将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法来实现。
主线程与守护线程的区别:
将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法来实现。
主线程与守护线程的区别:
- 守护线程不会阻止进程的终止。属于某个进程的所有主线程都终止后,该进程就会被终止。所有剩余的后台线程都会停止且不会完成。
- 不管是前台线程还是后台线程,如果线程内出现了异常,都会导致进程的终止。
- 托管线程池中的线程都是后台线程,使用new Thread方式创建的线程默认都是前台线程。
锁
乐观锁、悲观锁
如果将锁在宏观上进行大的分类,那么只有两类:悲观锁和乐观锁。
AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
Java的乐观锁主要有:自旋锁、偏向锁、轻量级锁
- 悲观锁:悲观锁就是悲观思想,即认为读少写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block知道拿到锁。Java中的悲观锁就是synchronized。
- 乐观锁:乐观锁就是乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。
AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
Java的乐观锁主要有:自旋锁、偏向锁、轻量级锁
自旋锁、偏向锁、轻量级锁
自旋锁:
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功(自旋),同时又大量的线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗。
- 定义:
- 优缺点:
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功(自旋),同时又大量的线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗。
偏向锁和轻量级锁:
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。
- 定义:
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。
- 场景:
多线程锁升级原理:
锁的级别从低到高:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。
锁的级别从低到高:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
- 无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。
- 偏向锁:对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。
- 轻量级锁:轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。
- 重量级锁:又有第三个线程来访时,轻量级锁也会升级为重量级锁。当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。
对象锁、类锁
synchronized 作为锁来使用的时候,只能出现在两个地方:代码块、方法(一般方法、静态方法)。
- 类锁:使用字节码文件(即.class)作为锁。如静态同步函数(使用本类的.class),同步代码块中使用.class。synchronized 锁住一个类。
- 对象锁:使用对象作为锁。如同步函数(使用本类实例,即 this),同步代码块中使用同一个对象。synchronized 锁住一个对象。
公平锁、非公平锁
非公平锁是可以抢占的,即如果在某个时刻有线程需要获取锁,而这个时候刚好锁可用,那么这个线程会直接抢占,而这时阻塞在等待队列的线程则不会被唤醒。
公平锁 :
非公平锁 :
公平锁可以通过 new ReentrantLock (true) 来实现;非公平锁可以通过 new ReentrantLock (false) 或者默认构造函数 new ReentrantLock () 实现。
synchronized 是非公平锁,并且它无法实现公平锁。
- 公平锁:多个线程在等待同一个锁时,必须按照申请锁的先后顺序来一次获得锁。
- 非公平锁:多个线程在等待同一个锁时,不用申请锁的先后顺序来一次获得锁。也就是不用排队。
非公平锁是可以抢占的,即如果在某个时刻有线程需要获取锁,而这个时候刚好锁可用,那么这个线程会直接抢占,而这时阻塞在等待队列的线程则不会被唤醒。
公平锁 :
- 等待锁的线程不会饿死
- 效率低
非公平锁 :
- 效率高
- 有些线程可能会饿死或者说很早就在等待锁,但要等很久才会获得锁。
公平锁可以通过 new ReentrantLock (true) 来实现;非公平锁可以通过 new ReentrantLock (false) 或者默认构造函数 new ReentrantLock () 实现。
synchronized 是非公平锁,并且它无法实现公平锁。
互斥锁
互斥锁也就是我们常说的同步,即一次最多只能有一个线程持有的锁,当一个线程持有该锁的时候其它线程无法进入上锁的区域。在 Java 中 synchronized 就是互斥锁,从宏观概念来讲,互斥锁就是通过悲观锁的理念引出来的,而非互斥锁则是通过乐观锁的概念引申的。
可重入锁、不可重入锁
- 可重入锁:如果某个线程试图获取一个已经由他自己持有的锁,这个请求可以成功
- 不可重入锁:当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。
共享锁、排它锁
共享锁和排它锁多用于数据库中的事物操作,主要针对读和写的操作。而在 Java 中,对这组概念通过 ReentrantReadWriteLock 进行了实现,它的理念和数据库中共享锁与排它锁的理念几乎一致,即一条线程进行读的时候,允许其他线程进入上锁的区域中进行读操作;当一条线程进行写操作的时候,不允许其他线程进入进行任何操作。即读 + 读可以存在,读 + 写、写 + 写均不允许存在。
- 共享锁:也称读锁或 S 锁。如果事务 T 对数据 A 加上共享锁后,则其他事务只能对 A 再加共享锁,不能加排它锁。获准共享锁的事务只能读数据,不能修改数据。
- 排它锁:也称独占锁、写锁或 X 锁。如果事务 T 对数据 A 加上排它锁后,则其他事务不能再对 A 加任何类型的锁。获得排它锁的事务即能读数据又能修改数据。
死锁、活锁
死锁:所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
java中产生死锁可能性的最根本原因是:
1)是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环;
2)默认的锁申请操作是阻塞的。 如,线程在获得一个锁L1的情况下再去申请另外一个锁L2,也就是锁L1想要包含了锁L2,在获得了锁L1,并且没有释放锁L1的情况下,又去申请获得锁L2,这个是产生死锁的最根本原因。
必要条件:
产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生:
java中产生死锁可能性的最根本原因是:
1)是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环;
2)默认的锁申请操作是阻塞的。 如,线程在获得一个锁L1的情况下再去申请另外一个锁L2,也就是锁L1想要包含了锁L2,在获得了锁L1,并且没有释放锁L1的情况下,又去申请获得锁L2,这个是产生死锁的最根本原因。
必要条件:
产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生:
- 互斥:进程要求对所分配的资源进行排他性控制,此时若有其他进程请求该资源,则请求进程只能等待。
- 不剥夺:资源只能由获得该资源的进程自己来释放(只能是主动释放)。
- 请求和保持:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
- 循环等待:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。
破坏死锁的方法:
- 方案一:破坏死锁的循环等待条件。
- 方法二:破坏死锁的请求与保持条件,使用lock的特性,为获取锁操作设置超时时间。这样不会死锁(至少不会无尽的死锁)
- 方法三:设置一个条件遍历与一个锁关联。
活锁与死锁区别:
活锁和死锁在表现上是一样的两个线程都没有任何进展,但是区别在于:死锁,两个线程都处于阻塞状态,说白了就是它不会再做任何动作,我们通过查看线程状态是可以分辨出来的。而活锁呢,并不会阻塞,而是一直尝试去获取需要的锁,不断的 try,这种情况下线程并没有阻塞所以是活的状态,我们查看线程的状态也会发现线程是正常的,但重要的是整个程序却不能继续执行了,一直在做无用功。
活锁和死锁在表现上是一样的两个线程都没有任何进展,但是区别在于:死锁,两个线程都处于阻塞状态,说白了就是它不会再做任何动作,我们通过查看线程状态是可以分辨出来的。而活锁呢,并不会阻塞,而是一直尝试去获取需要的锁,不断的 try,这种情况下线程并没有阻塞所以是活的状态,我们查看线程的状态也会发现线程是正常的,但重要的是整个程序却不能继续执行了,一直在做无用功。
锁消除、锁粗化(锁膨胀)
锁消除和锁粗化,是编译器在编译代码阶段,对一些没有必要的、不会引起安全问题的同步代码取消同步(锁消除)或者对那些多次执行同步的代码且它们可以合并到一次同步的代码(锁粗化)进行的优化手段,从而提高程序的执行效率。
锁消除:
对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判断依据是来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而能被其他线程访问到,那就可以把他们当做栈上数据对待,认为他们是线程私有的,同步加锁自然就无需进行。
例如:StringBuffer的append方法
StringBuffer 的实例是共享数据,但是对该实例的引用确是每条线程内部私有的,它们互不影响互不可见。在 concatString () 方法中涉及了同步操作(append方法)。但是sb 对象被限制在方法的内部,其他线程无法访问。因此,虽然这里有锁,但是可以被安全的消除,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。
对象的实例总是存在于堆中被多线程共享,即使在局部方法中创建的实例依然存在于堆中,但是对该实例的引用是线程私有的,对其他线程不可见。
对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判断依据是来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而能被其他线程访问到,那就可以把他们当做栈上数据对待,认为他们是线程私有的,同步加锁自然就无需进行。
例如:StringBuffer的append方法
StringBuffer 的实例是共享数据,但是对该实例的引用确是每条线程内部私有的,它们互不影响互不可见。在 concatString () 方法中涉及了同步操作(append方法)。但是sb 对象被限制在方法的内部,其他线程无法访问。因此,虽然这里有锁,但是可以被安全的消除,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。
对象的实例总是存在于堆中被多线程共享,即使在局部方法中创建的实例依然存在于堆中,但是对该实例的引用是线程私有的,对其他线程不可见。
锁粗化:
原则上,要将同步块的作用范围限制的尽量小 —— 只在共享数据的实际作用域中才进行同步,使得需要同步的操作数量尽可能变小,如果存在锁禁止,那等待的线程也能尽快拿到锁。大部分情况下,这些都是正确的。
但是,如果一系列的操作都是同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那么即使没有线程竞争,频繁地进行互斥同步操作也导致不必要的性能损耗。
类似上面锁消除的 concatString () 方法。如果 StringBuffer sb = new StringBuffer (); 定义在方法体之外,那么就会有线程竞争,但是每个 append () 操作都对同一个对象反复加锁解锁,那么虚拟机探测到有这样的情况的话,会把加锁同步的范围扩展到整个操作序列的外部,即扩展到第一个 append () 操作之前和最后一个 append () 操作之后,这样的一个锁范围扩展的操作就称之为锁粗化。
原则上,要将同步块的作用范围限制的尽量小 —— 只在共享数据的实际作用域中才进行同步,使得需要同步的操作数量尽可能变小,如果存在锁禁止,那等待的线程也能尽快拿到锁。大部分情况下,这些都是正确的。
但是,如果一系列的操作都是同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那么即使没有线程竞争,频繁地进行互斥同步操作也导致不必要的性能损耗。
类似上面锁消除的 concatString () 方法。如果 StringBuffer sb = new StringBuffer (); 定义在方法体之外,那么就会有线程竞争,但是每个 append () 操作都对同一个对象反复加锁解锁,那么虚拟机探测到有这样的情况的话,会把加锁同步的范围扩展到整个操作序列的外部,即扩展到第一个 append () 操作之前和最后一个 append () 操作之后,这样的一个锁范围扩展的操作就称之为锁粗化。
线程池
什么是线程池?
线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理。如果每个请求都创建一个线程去处理,那么服务器的资源很快就会被耗尽,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。
优点:
优点:
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度:当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
Executor使用流程
1.主线程首先要创建实现 Runnable 或者 Callable 接口的任务对象。
2.把创建完成的实现 Runnable/Callable接口的 对象直接交给 ExecutorService 执行: ExecutorService.execute(Runnable command))或者也可以把 Runnable 对象或Callable 对象提交给 ExecutorService 执行(ExecutorService.submit(Runnable task)或 ExecutorService.submit(Callable <T> task))。
3.如果执行 ExecutorService.submit(…),ExecutorService 将返回一个实现Future接口的对象(我们刚刚也提到过了执行 execute()方法和 submit()方法的区别,submit()会返回一个 FutureTask 对象)。由于 FutureTask 实现了 Runnable,我们也可以创建 FutureTask,然后直接交给 ExecutorService 执行。
4.最后,主线程可以执行 FutureTask.get()方法来等待任务执行完成。主线程也可以执行 FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。
2.把创建完成的实现 Runnable/Callable接口的 对象直接交给 ExecutorService 执行: ExecutorService.execute(Runnable command))或者也可以把 Runnable 对象或Callable 对象提交给 ExecutorService 执行(ExecutorService.submit(Runnable task)或 ExecutorService.submit(Callable <T> task))。
3.如果执行 ExecutorService.submit(…),ExecutorService 将返回一个实现Future接口的对象(我们刚刚也提到过了执行 execute()方法和 submit()方法的区别,submit()会返回一个 FutureTask 对象)。由于 FutureTask 实现了 Runnable,我们也可以创建 FutureTask,然后直接交给 ExecutorService 执行。
4.最后,主线程可以执行 FutureTask.get()方法来等待任务执行完成。主线程也可以执行 FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。
使用方法
ThreadPoolExecutor pool =
(ThreadPoolExecutor) Executors.newFixedThreadPool(10);
- 需要调用Executors工具类中相应的静态工厂方法
ThreadPoolExecutor pool =
(ThreadPoolExecutor) Executors.newFixedThreadPool(10);
使用ThreadPoolExecutor创建线程池
ThreadPoolExecutor 3 个最重要的参数:
ThreadPoolExecutor其他常见参数:
2.ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
3.ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
4.ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
ThreadPoolExecutor 3 个最重要的参数:
- corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
- maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
- workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。
1.ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
2.LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按 FIFO 排序元素,吞吐量通常要高于 ArrayBlockingQueue。静态工厂方法 - Executors.newFixedThreadPool () 使用了这个队列。
3.SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQueue,静态工厂方法 Executors.newCachedThreadPool 使用了这个队列。
4.PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
ThreadPoolExecutor其他常见参数:
- keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
- unit : keepAliveTime 参数的时间单位。
- threadFactory :executor 创建新线程的时候会用到。
- handler :饱和策略。当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。在 JDK 1.5 中 Java 线程池框架提供了以下 4 种策略。
2.ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
3.ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
4.ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
execut()方法实现原理
1.如果当前运行的线程少于 corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
2.如果运行的线程等于或多于 corePoolSize,则将任务加入Blocking-Queue。
3.如果无法将任务加入 BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。
4.如果创建新线程将使当前运行的线程超出 maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution () 方法。
源码中addWorker 这个方法主要用来创建新的工作线程,如果返回true说明创建和启动工作线程成功,否则的话返回的就是false。
2.如果运行的线程等于或多于 corePoolSize,则将任务加入Blocking-Queue。
3.如果无法将任务加入 BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。
4.如果创建新线程将使当前运行的线程超出 maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution () 方法。
源码中addWorker 这个方法主要用来创建新的工作线程,如果返回true说明创建和启动工作线程成功,否则的话返回的就是false。
常见对比
Runnable与Callable
Runnable自 Java 1.0 以来一直存在,但Callable仅在 Java 1.5 中引入,目的就是为了来处理Runnable不支持的用例。Runnable 接口不会返回结果或抛出检查异常,但是Callable 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,
execute()与sumbit()
- execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
- submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
shutdown()和shutdownNow()
- shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。
- shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。
isTerminated()和isShutdown()
- isShutDown 当调用 shutdown() 方法后返回为 true。
- isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true
线程池状态
注:
- 1.RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。线程池的初始化状态是RUNNING。线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0。
- 2.SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。调用线程池的shutdown()方法时,线程池由RUNNING -> SHUTDOWN。
- 3.STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。调用线程池的shutdownNow()方法时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
- 4.TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。因为terminated()在ThreadPoolExecutor类中是空的,所以用户想在线程池变为TIDYING时进行相应的处理;可以通过重载terminated()函数来实现。
- 5.TERMINATED:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
注:
- 当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
- 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
合理配置线程池
任务的性质:CPU 密集型任务、IO 密集型任务和混合型任务。
CPU密集型任务应配置尽可能少的线程,如配置Ncpu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。
可以通过 Runtime.getRuntime ().availableProcessors () 方法获得当前设备的 CPU 个数。
- CPU 密集型任务:主要是执行计算任务,响应时间很快,cpu 一直在运行,这种任务 cpu 的利用率很高,线程个数为 CPU 核数。这几个线程可以并行执行,不存在线程切换到开销,提高了 cpu 的利用率的同时也减少了切换线程导致的性能损耗
- IO 密集型任务:主要是进行 IO 操作,执行 IO 操作的时间较长,这是 cpu 处于空闲状态,导致 cpu 的利用率不高,线程个数为 CPU 核数的两倍。到其中的线程在 IO 操作的时候,其他线程可以继续用 cpu,提高了 cpu 的利用率
CPU密集型任务应配置尽可能少的线程,如配置Ncpu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。
可以通过 Runtime.getRuntime ().availableProcessors () 方法获得当前设备的 CPU 个数。
synchronized
什么是synchronized
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
使用场景
- Synchronized修饰普通(实例)同步方法:锁对象当前实例对象;
- Synchronized修饰静态同步方法:锁对象是当前的类Class对象;
- Synchronized修饰同步代码块:锁对象是Synchronized后面括号里配置的对象,这个对象可以是某个对象(xlock),也可以是某个类(Xlock.class);
底层原理
synchronized关键字底层原理属于JVM层面。
1.synchronized同步代码块时:synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
2.synchronized修饰方法时:synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
1.synchronized同步代码块时:synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
2.synchronized修饰方法时:synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
锁升级
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
synchronized与Lock的区别
- 两者都是可重入锁
- synchronized是java的一个关键字,Lock是一个API
volatile
volatile简介
volatile 关键字的主要作用就是保证变量的可见性然后还有一个作用是防止指令重排序。
在 JDK1.2 之前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。要解决这个问题,就需要把变量声明为volatile,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。
在 JDK1.2 之前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。要解决这个问题,就需要把变量声明为volatile,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。
volatile如何防止代码重排序
使用了内存屏障
内存屏障其实就是一个CPU指令,在硬件层面上来说可以扥为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。主要有两个作用:
(1)阻止屏障两侧的指令重排序;
(2)强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
内存屏障其实就是一个CPU指令,在硬件层面上来说可以扥为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。主要有两个作用:
(1)阻止屏障两侧的指令重排序;
(2)强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
并发编程的三个重要特征
- 原子性 : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。synchronized 可以保证代码片段的原子性。
- 可见性 :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile 关键字可以保证共享变量的可见性。
- 有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。
synchronized与volatile的区别
synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在:
- volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
- 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
- volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
- volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。
ThreadLocal
ThreadLocal简介
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK中提供的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。
如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。
原理
Thread类 源代码可以看出Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是null,只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()、set()方法。
最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key ,Object 对象为 value的键值对。
比如我们在同一个线程中声明了两个 ThreadLocal 对象的话,会使用 Thread内部都是使用仅有那个ThreadLocalMap 存放数据的,ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值。
ThreadLocalMap是ThreadLocal的静态内部类。
最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key ,Object 对象为 value的键值对。
比如我们在同一个线程中声明了两个 ThreadLocal 对象的话,会使用 Thread内部都是使用仅有那个ThreadLocalMap 存放数据的,ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值。
ThreadLocalMap是ThreadLocal的静态内部类。
内存泄露问题
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。
ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法
ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法
JVM
JVM组成
执行引擎也叫做解释器 (Interpreter),负责解释命令,提交操作系统执行。
在企业级应用中已经比较少见,因为现在的异构领域间的通信很发达,比如可以使用 Socket 通信,也可以使用 Web Service 等等,不多做介绍。
总结:整个 JVM 框架由加载器加载文件,然后执行器在内存(运行时数据区和直接内存)中处理数据,需要与异构系统交互是可以通过本地接口进行。
- 1. Class Loader 类加载器
- 2. Execution Engine 执行引擎
执行引擎也叫做解释器 (Interpreter),负责解释命令,提交操作系统执行。
- 3. Native Interface 本地接口
在企业级应用中已经比较少见,因为现在的异构领域间的通信很发达,比如可以使用 Socket 通信,也可以使用 Web Service 等等,不多做介绍。
- 4.Runtime data area 运行数据区
总结:整个 JVM 框架由加载器加载文件,然后执行器在内存(运行时数据区和直接内存)中处理数据,需要与异构系统交互是可以通过本地接口进行。
JVM内存区域
堆
堆:Java虚拟机所管理的内存中最大的一块,它是所有线程共享的一块内存区域,在虚拟机启动的时候创建。此内存的主要目的是存放对象实例,几乎所有的对象实例以及数组都是在这里分配的。其他·未在堆中的对象时因为随着JIT编译期与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化。所有对象都分配在堆上就不是那么绝对了。在JDK1.7中已经默认开启了逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用,那么对象可以直接在栈上分配。
Java堆是垃圾收集器管理的主要区域,因此也被称为GC堆。垃圾收集器基本都采用分代垃圾收集算法。所以Java堆还可以分为:新生代和老年代,新生代又可分为伊甸园区、Form区域和To区域。
主要作用:1.存放对象实例。2.垃圾收集器主要管理的区域。
Java堆是垃圾收集器管理的主要区域,因此也被称为GC堆。垃圾收集器基本都采用分代垃圾收集算法。所以Java堆还可以分为:新生代和老年代,新生代又可分为伊甸园区、Form区域和To区域。
主要作用:1.存放对象实例。2.垃圾收集器主要管理的区域。
方法区
方法区(元空间):方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。运行时常量池:编译器生成的各种字面量和符号引用(编译期)存储在 class 文件的常量池中,这部分内容会在类加载之后进入运行时常量池。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
方法区也被称为永久代。很多人都会分不清方法区和永久代的关系:《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。
JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。
方法区也被称为永久代。很多人都会分不清方法区和永久代的关系:《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。
JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。
为什么要将永生代替换为元空间?
- 1.整个永生代有一个JVM本身设置固定的大小上限,无法调整,而元空间使用的是直接内存,受本机可用内存的限制,溢出的几率(出现OutOfMemoryError异常)比原来更小。我们也可以使用-XX :MaxMetaspaceSize标志来设置元空间的大小,默认值为unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize调整标志定义元空间的初始大小,如果未指定此标志,则Metaspace将根据运行时的应用程序动态地调整大小。
- 2.元空间里面存放的是类的元数据,这样加载多少类的元数据就不由MaxPermSize控制了,而由系统的实际可用空间来控制,这样加载的类就更多了。
- 3.在JDK 1.8,合并HotSpot和JRockit的代码时,JRockit从来没有一个叫永生代的区域,合并后需要额外设置一个区域。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存被频繁的使用,也可能会导致OutOfMemoryError错误的出现。JDK 1.4中新加入了NIO类,引入了一种基于通道与缓存区的I/O方式,他可以直接使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的BirectByteBuffer对象作为这块内存的引用。这样做避免了Java堆和Native对之间来回复制数据,可以提高性能。
运行时常量池
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用)。常量池受到方法区内存的限制,当常量池无法申请内存时会抛出OutOfMemoryError的错误。
- 1.JDK 1.7之前运行时,常量池逻辑包含字符串常量池,存放在方法区,此时Hotspot虚拟机对方法区的实现为永久代。
- 2.JDK 1.7字符串常量池被从方法区拿到堆中,运行时常量池剩下的东西还在方法区。
- 3.JDK 1.8用元空间代替方法区,字符串常量池还在堆中,运行时常量池还在方法区,只不过是方法区的实现在元空间。
虚拟机栈
虚拟机栈:Java虚拟机栈也是线程私有的,他的生命周期与线程相同, 随着线程的创建而创建,随着线程的死亡而死亡。 描述的是Java方法执行的内存模型,每次方法的调用的数据都是通过栈来传递的。我们通常所说的Java内存中的堆内存和栈内存,其中栈内存就指的是Java虚拟机栈,或者说虚拟机栈中的局部变量表部分。实际上,Java虚拟机栈是由一个个的栈帧组成,每一个栈帧都有:局部变量表、操作数栈、动态链接、方法出口信息。局部变量表主要存放的是编译期可知的各种数据类型(byte、short、int、long、float、double、boolean),对象引用(引用类型:reference类型,它不是不是对象本身,可能是指向对象的一个指针,也可能是指向一个代表对象的句柄或者其他与对象相关的位置)。Java虚拟机栈会出现两种错误:OutOfMemoryErrer和StackOverFlowError错误。
1.OutOfMemoryError:若虚拟机堆中没有空闲内存,并且垃圾回收器也无法提供更多的内存, 就会抛出OutOfMemoryError错误。
2.StackOverFlowError:若虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过 当前虚拟机的最大深度的时候,就会抛出StackOverFlowError错误。
那么方法/函数怎样调用呢?Java栈就和数据结构中的栈类似,遵循先进后出。每一次函数的调用都会有一个对应的栈帧被压入Java栈,每次方法调用结束后都有一个栈帧被弹出。Java方法有return语句和抛出异常两种返回方式,这两种都会导致栈帧被弹出。
1.OutOfMemoryError:若虚拟机堆中没有空闲内存,并且垃圾回收器也无法提供更多的内存, 就会抛出OutOfMemoryError错误。
2.StackOverFlowError:若虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过 当前虚拟机的最大深度的时候,就会抛出StackOverFlowError错误。
那么方法/函数怎样调用呢?Java栈就和数据结构中的栈类似,遵循先进后出。每一次函数的调用都会有一个对应的栈帧被压入Java栈,每次方法调用结束后都有一个栈帧被弹出。Java方法有return语句和抛出异常两种返回方式,这两种都会导致栈帧被弹出。
程序计数器
程序计数是一个比较小的内存区域,可以看做当前线程所执行的的字节码的行号指示器。字节码解释器通过改变这个程序计数器来选取下一条要执行的字节码指令。分支、循环、跳转、异常处理等功能都需要这个程序计数器。每个线程都有自己私有的程序计数器,其目的是为了线程切换后恢复到正确的执行位置。程序计数器不会出现OutOfMemoryError的内存区域,他的生命周期随线程的创建而创建,随线程的结束而死亡。
本地方法栈
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。
类加载过程
类的生命周期
卸载:该类的Class对象被GC。
卸载类需要满足的3个要求:
卸载类需要满足的3个要求:
- 该类的实例对象被GC,也就是在堆中不存在该类的实例化对象。
- 该类没有在其他地方被引用。
- 该类的类加载器的实例被GC。(JVM自带的类加载器不会被卸载。)
类加载过程
- 元数据验证:对字节码描述的信息进行语义分析,以保证信息符合Java语言规范。(例如:这个类是否有父类,除Object类之外,其他类都有父类;这个类是否继承被final修饰的类等)
- 字节码验证:通过对数据流与控制流的分析,确定程序语义是合法的、符号逻辑的。(例如:保证任意时刻操作数栈和指令代码序列都能配合工作。)
- 符号引用验证:确保动作能正确执行。
- 设置类变量初始值是为数据的默认值,初始化阶段才会赋值。
直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
Class文件需要加载到虚拟机中之后才可以运行和使用,系统加载Class类型的文件主要三步:加载->连接->初始化。连接过程又分为三步:验证->准备->解析。
- 1.加载:简单来说,加载指的是把 class 字节码文件从各个来源通过类加载器装载入内存中。
- 2.验证:
- 元数据验证:对字节码描述的信息进行语义分析,以保证信息符合Java语言规范。(例如:这个类是否有父类,除Object类之外,其他类都有父类;这个类是否继承被final修饰的类等)
- 字节码验证:通过对数据流与控制流的分析,确定程序语义是合法的、符号逻辑的。(例如:保证任意时刻操作数栈和指令代码序列都能配合工作。)
- 符号引用验证:确保动作能正确执行。
- 3.准备:正式为类变量分配内存并设置类变量初始值的阶段。
- 设置类变量初始值是为数据的默认值,初始化阶段才会赋值。
- 4.解析:虚拟机将常量池中的符号引用替换为直接引用。
直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
- 5.初始化:初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行类构造器 ()方法的过程。
初始化类的情况:
- 当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
- 当jvm执行new指令时会初始化类。即当程序创建一个类的实例对象。
- 当jvm执行getstatic指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
- 当jvm执行putstatic指令时会初始化类。即程序给类的静态变量赋值。
- 当jvm执行invokestatic指令时会初始化类。即程序调用类的静态方法。
-进行反射调用时,如Class.forName()。
-初始化一个类时,如果父类没有初始化,会先触发父类的初始化。
-虚拟机启动时,会先初始化包含main方法的类(主类)。
-在JDK1.8, 被default关键字修饰的接口方法,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
类加载器
JVM内置三个重要的类加载器
除了BootstrapClassLoader类加载器,其他类加载器全部继承的是java.lang.ClassLoader
- BootstrapClassLoader(启动类加载器):最底层的加载器,由C++实现,负责加载 %JAVA_HOME%/lib 目录下的jar包和类或被XBootclasspath参数指定的路径中的类。
- ExtensionClassLoader(扩展类加载器): 主要负责加载目录 %JRE_HOME%/lib/ext 目录下的jar包和类,或被 java.ext.dirs 系统变量所指定的路径下的jar包。
- AppClassLoader(应用程序类加载器): 面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。
除了BootstrapClassLoader类加载器,其他类加载器全部继承的是java.lang.ClassLoader
双亲委派模型
工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
优点:
优点:
- 保证Java程序稳定执行,避免了类的重复加载。(JVM区分不同的类的方式仅仅依靠类名,相同的类文件被不同的类加载器加载是两个不同的类。)
- 保证Java的核心API不被篡改。(如果没有使用双亲加载模型,我们可以编写一个java.lang.Object的类,在程序运行时会产生不同的Object类,引起错误。)
自定义类加载器
自定义类加载器需要继承ClassLoader。
优点:
优点:
- 隔离加载类。
- 修改类加载的方式。(不使用双亲委派模型)
- 扩展加载源。
- 防止代码泄露。
对象创建过程
对象的创建
- 1.类加载检查:虚拟机遇到一条new指令的时候,首先会检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且会检查这个符号引用所代表的类是否被加载,解析和初始化过。如果没有,会执行类的加载。
- 2.分配内存:在类检查通过后,虚拟机将会为新生对象分配内存。对象所需的内存的大小在类加载完成后便可以确定,为对象分配内存就是将一块确定大小的内存从Java堆中划分出来。
- 3.初始化零值:内存分配完成后,虚拟机需要将分配到内存空间的都初始化为零值(不包括对象头),这一步操作保证了对象实例在Java代码中可以不赋值就可以直接使用。
- 4.设置对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象头中。另外根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
- 5.执行init方法:在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
过程
分配内存的方式
分配的方式有“指针碰撞”和“空闲列表”两种,选择那种分配方式由Java堆是否规整决定,而Java堆是否规整又是由所采用的垃圾收集器是否带有压缩整理功能决定的。GC收集器的算法是“标记-清除”时,会产生内存碎片,这时堆不规整。当算法是“标记-整理”时,可以有效避免产生内存碎片,这时堆是规整的。复制算法内存也是规整的。
指针碰撞:使用于堆内存规整(没有内存碎片)的情况下,原理是将用过的内存全部整合到一起,没有用过的内存放在另一边,中间有一个分界指针,只需要将指针向没有用过的内存方向移动对象内存大小的位置即可。GC收集器:Serial、ParNew。
空闲列表:适用于堆内存不规整(有内存碎片)的情况下,原理是虚拟机会维护一个列表,这个列表会记录那些内存块是可用的,在分配的时候找一块足够大的内存块分给实例对象,然后更新列表。GC收集器:CMS。
内存并发分配,采用两种方式保证线程安全:
指针碰撞:使用于堆内存规整(没有内存碎片)的情况下,原理是将用过的内存全部整合到一起,没有用过的内存放在另一边,中间有一个分界指针,只需要将指针向没有用过的内存方向移动对象内存大小的位置即可。GC收集器:Serial、ParNew。
空闲列表:适用于堆内存不规整(有内存碎片)的情况下,原理是虚拟机会维护一个列表,这个列表会记录那些内存块是可用的,在分配的时候找一块足够大的内存块分给实例对象,然后更新列表。GC收集器:CMS。
内存并发分配,采用两种方式保证线程安全:
- CAS + 失败重试:CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
- TLAB(Thread Local Allocation Buffer):为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
对象的内存布局
在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头、实例数据和对齐填充。
- 1.Hotspot 虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的自身运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
- 2. 实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。
- 3. 对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍。
对象的使用(访问方式)
Java 程序通过栈上的 reference 数据来操作堆上的具体对象。目前主流的访问方式有①使用句柄和②直接指针两种:
使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
- 句柄:如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
- 直接指针:如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。
使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
对象回收
对对象进行标记,判断是否回收。
对象标记算法:引用计数法和可达性分析算法
宣告一个对象死亡,至少要经历两次标记
对象标记算法:引用计数法和可达性分析算法
宣告一个对象死亡,至少要经历两次标记
- 1.第一次标记
- 2.第二次标记
内存泄漏与内存溢出
内存泄漏
定义:
当某些对象不再被应用程序所使用,但是由于仍然被引用而导致垃圾收集器不能释放 (Remove, 移除) 他们.这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。
场景:
当某些对象不再被应用程序所使用,但是由于仍然被引用而导致垃圾收集器不能释放 (Remove, 移除) 他们.这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。
场景:
- 长生命周期的对象持有短生命周期对象的引用
- 修改对象的地址(常见于hashset的哈希值)从而无法找到该对象。
- 机器的连接数和关闭时间设置,长时间开启非常耗费资源的连接
内存溢出
定义:
指程序运行过程中无法申请到足够的内存而导致的一种错误。
场景:
如何避免:
指程序运行过程中无法申请到足够的内存而导致的一种错误。
场景:
- 堆内存溢出(对象过多)
- 方法区内存溢出(加载的类过多)
- 线程栈溢出(递归太深或者方法层级过多)
如何避免:
- 尽早释放无用对象的引用
- 使用字符串处理,避免使用 String,应大量使用 StringBuffer,每一个 String 对象都得独立占用内存一块区域
- 尽量少用静态变量,因为静态变量存放在永久代(方法区),永久代基本不参与垃圾回收避免在循环中创建对象
- 开启大型文件或数据库。不要一次拿了太多的数据
JVM垃圾回收过程
对象在堆中的过程
- 1.大部分对象优先在Eden区创建,当Eden没有足够内存时,会触发Minor GC。Minor GC会判断Eden区中的那些对象是可回收的,如果对象可以回收,就直接将它回收;如果对象不可以回收,对象就会进入From区,对象的年龄加一。
- 2.随着程序的运行,当Eden区再次占满,会触发第二次Minor GC。如果对象可以回收,就直接将它回收;如果对象不可以回收,对象就进入From区,对象年龄再加一,From区与To区互换。
- 3.依次循环,当对象的年龄达到15时会晋升到老年代(Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值),注意:当对象占From或To区的50%以上就直接进入老年代。
- 4.当老年代的内存占满时,会触发Full GC,会出现STW(停顿)现象
Minor GC和Full GC
- Minor GC:又称新生代GC,指发生在新生代的垃圾收集动作;因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快。
- Full GC:又称Major GC或老年代GC,指发生在老年代的GC;出现Full GC经常会伴随至少一次的Minor GC(不是绝对的,Parallel Scavenge收集器就可以选择设置Major GC策略);Major GC速度一般比Minor GC 慢10倍以上。
判断对象失效的算法
引用计数法
给对象添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可在使用的。
存在问题:
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。
存在问题:
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。
可达性分析法
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。它解决了引用计数器法不能解决的循环引用问题。
GCRoots:GC Roots是一些由堆外指向堆内的引用,包括但不限于:Java 方法栈桢中的局部变量;已加载类的静态变量;JNI handles;已启动且未停止的 Java 线程。
存在问题:在多线程环境下,其他线程可能会更新已经访问过的对象中的引用,而我们的可达性分析线程却没有同步到最新的内容。那么就会造成误报或者漏报。
GCRoots:GC Roots是一些由堆外指向堆内的引用,包括但不限于:Java 方法栈桢中的局部变量;已加载类的静态变量;JNI handles;已启动且未停止的 Java 线程。
存在问题:在多线程环境下,其他线程可能会更新已经访问过的对象中的引用,而我们的可达性分析线程却没有同步到最新的内容。那么就会造成误报或者漏报。
引用
强引用
以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
Object object = new Object();
String str = "StrongReference";
如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。
Object object = new Object();
String str = "StrongReference";
如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。
软引用
如果一个对象只具有软引用,那就类似于有用但并不是必需的生活用品。在Java中用java.lang.ref.SoftReference类来表示。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。 只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。
弱引用
如果一个对象只具有弱引用,那就类似于可有可无的生活用品。在java中,用java.lang.ref.WeakReference类来表示。
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。所以被 软引用关联的对象只有在内存不足时才会被回收,而被弱引用关联的对象在JVM进行垃圾回收时总会被回收。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。所以被 软引用关联的对象只有在内存不足时才会被回收,而被弱引用关联的对象在JVM进行垃圾回收时总会被回收。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
虚引用
虚引用,它是最弱的一种引用关系。无法通过虚引用来取得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集时收到一个系统通知。虚引用必须和引用队列(ReferenceQueue)联合使用。
垃圾收集算法
标记-清除算法
该算法分为“标记”和“清除”两项工作。标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行清除回收。
标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
缺点: 1.效率问题 2.产生空间碎片
标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
缺点: 1.效率问题 2.产生空间碎片
复制算法
复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
它比标记-清除算法要高效,但不适用于存活对象较多的内存,因为复制的时候会有较多的时间消耗。它的致命缺点是会有一半的内存浪费。
它比标记-清除算法要高效,但不适用于存活对象较多的内存,因为复制的时候会有较多的时间消耗。它的致命缺点是会有一半的内存浪费。
标记-整理算法
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。
适用于存活对象较多的场合
适用于存活对象较多的场合
分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
优点:
1.可以根据各个年代的特点选择合适的垃圾收集算法。
2.类似于一个金字塔的形状,让更少的对象进入老年代,减少STW的出现。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
优点:
1.可以根据各个年代的特点选择合适的垃圾收集算法。
2.类似于一个金字塔的形状,让更少的对象进入老年代,减少STW的出现。
垃圾收集器
垃圾收集器分类
按位置分
按并发与并行分
- 并发垃圾收集: 指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行), 用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;如CMS、G1(也有并行);
- 并行垃圾收集:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态; 如ParNew、Parallel Scavenge、Parallel Old;
Serial收集器
Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World(STW)" ),直到它收集结束。
新生代采用复制算法,老年代采用标记-整理算法
新生代采用复制算法,老年代采用标记-整理算法
- 优点:简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。
- 缺点:会引起Stop The World
ParNew收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略、Stop The World等等)和 Serial 收集器完全一样。
新生代采用复制算法,老年代采用标记-整理算法
新生代采用复制算法,老年代采用标记-整理算法
- 优点: 在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;
- 缺点:在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。
ParallelScavenge收集器
ParallelScavenge收集器也是多线程收集器,它看上去几乎和ParNew都一样,它的关注点是吞吐量(高效率利用CPU)。
采用算法:复制算法
应用场景:
采用算法:复制算法
应用场景:
- 高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;
- 当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互;
Serial Old收集器
Serial 收集器的老年代版本,它同样是一个单线程收集器。
采用算法:标记-整理算法
应用场景:
采用算法:标记-整理算法
应用场景:
- 在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用。
- 作为 CMS 收集器的后备方案。
Parallel Old收集器
Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本,也是多线程收集。
采用算法:复制-整理算法
应用场景:
采用算法:复制-整理算法
应用场景:
- JDK1.6及之后用来代替老年代的Serial Old收集器;
- 特别是在Server模式,多CPU的情况下;
- 这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge加Parallel Old收集器的"给力"应用组合;
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用。是 HotSpot 虚拟机第一款真正意义上的并发收集器。
采用算法:标记-清除算法(产生空间碎片)
应用场景:
采用算法:标记-清除算法(产生空间碎片)
- 优点:并发收集、低停顿
- 缺点:1.对CPU敏感(并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。)。2.无法处理浮动垃圾( 在并发清除时,用户线程新产生的垃圾,称为浮动垃圾)。3.产生空间碎片
应用场景:
- 与用户交互较多的场景;
- 希望系统停顿时间最短,注重服务的响应速度;
- 以给用户带来较好的体验;
G1收集器
G1收集器是一个面向服务器的垃圾收集器。低停顿并可建立可预测的停顿时间模型。其它收集器的工作范围是整个新生代或者老年代、G1收集器的工作范围是整个堆。
在使用G1收集器时,它将整个Java堆划分为多个大小相等的独立区域(Region)。虽然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔离的,他们都是一部分Region(不需要连续)的集合。
采用算法:集合多种算法,从整体看,是基于标记-整理算法;从局部看,是基于复制算法。都不会产生空间碎片。
应用场景:
在使用G1收集器时,它将整个Java堆划分为多个大小相等的独立区域(Region)。虽然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔离的,他们都是一部分Region(不需要连续)的集合。
采用算法:集合多种算法,从整体看,是基于标记-整理算法;从局部看,是基于复制算法。都不会产生空间碎片。
应用场景:
- 面向服务端应用,针对具有大内存、多处理器的机器;
- 最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案;
为什么能建立可预测的停顿时间模型?
因为它有计划的避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各个 Region 里面的垃圾堆积的大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。这样就保证了在有限的时间内可以获取尽可能高的收集效率。
框架架构
Spring
Spring简介
- Spring 是一个Java轻量级的开源应用框架,
- 它具有分层的体系架构,是许多模块的组成,
- 最核心的部分是IOC容器和AOP面向切面编程。
Spring IOC容器
什么是IOC容器?
- IOC容器是Spring创建和管理Bean的地方。
- 容器通过读取提供的配置元数据来接收对象进行实例化,配置和组装的指令。
- 它是通过依赖注入来创建对象的。
常用的IOC容器
- BeanFactory容器:XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("Bean.xml"))
- ApplicationContext容器:1.FileSystemXmlApplicationContext 2.ClassPathXmlApplicationContext 3.WebXmlApplicationContext
什么是依赖注入?
- 您不必创建对象,但必须描述如何创建它们。
- 我们不是直接在代码中将组件和服务连接在一起,而是描述配置文件中哪些组件需要哪些服务。由 IoC容器将它们装配在一起。
依赖注入3种方式
- 构造函数注入
- setter注入
- 接口注入
什么是控制反转?
将由上到下的依赖变成由下到上进行依赖,轮子 -->车身 --> 汽车,当调整轮子大小时,只需要修改轮子就行。
原理
Spring.xml --> ResourceLoader加载 --> BeanDefinitionReader读取 --> BeadDefinitionRegisty注册 --> Map<beanName , BeanDefinition)> --> Map<beanName , Object>
Spring Bean
什么是SpringBean?
- Spring Bean是构成用户应用程序的主干对象
- 它们是基于用户提供给IOC容器的配置元数据创建的
包含:所有配置元数据,怎样创建一个bean,它的生命周期,依赖
怎样给IOC容器提供配置元数据?
- Xml配置文件
- 基于注解
- 用Java类
Bean的作用域?
通过定义bean中的scope属性来定义
- singleton:唯一bean实例,Spring默认情况。
- prototype:每次请求都会创建一个新的bean实例。
- request:每一次http请求都会创建一个新的bean,该bean仅在当前的HTTP request内有效。
- session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效。
- global-session:全局session作用域,仅仅在基于portlet的web应用中才可以用,不过在spring5中已经没有了。Portlet是能够生成语义代码(例如:HTML)片段的小型Java Web插件。
Bean的生命周期
当要销毁Bean的时候:
- Bean容器找到配置文件中Spring Bean的定义。
- Bean容器利用Java Reflection API创建一个Bean实例。
- Spring使用依赖注入填充所有属性。
- 如果bean实现了BeanNameAware接口,则工厂通过传递bean的ID来调用setBeanName( )。
- 如果bean实现BeanFactoryAware接口,工厂通过传递自身的实例来调用setBeanFactory( )。
- 只要实现了每个*Aware的接口,就调用响应的方法。
- 如果存在与Bean关联的任何BeanPostProcessors,则调用proProcessBeforeInitialization()。
- 如果Bean实现了InitializingBean接口,执行afterPropertiesSet()。
- 如果Bean在配置文件中定义了init-method属性,执行指定的方法。
- 最后,如果存在bean相连的任何BeanPostProcessors,则调用postProcessAfterInitialization()。
当要销毁Bean的时候:
- 如果bean实现DisposableBean接口,当spring容器关闭时,会调用destory()。
- 如果Bean在配置文件中定义了destory-method属性,执行指定的方法。
Bean的自动装配
Spring容器能够自动装配有联系的bean,不需要我们进行配置。
局限性:
设置autowire属性的方式:
局限性:
- 重写的可能性:重写自动装配的 <constructor-arg>和 <property> 设置来指定依赖关系。
- 不能自动装配所谓的简单类型包括基本类型,字符串和类。
- 具有模糊性,不能准确定位一个bean
设置autowire属性的方式:
- no:默认的设置,不使用自动装配。
- byName:通过参数名自动装配,Spring容器在配置文件中发现bean的autowire属性被设置成byName时,就会尝试匹配具有参数名一样的Bean进行连接。
- byType:通过参数类型自动装配,Spring容器在配置文件中发现bean的autowire属性被设置成byType时,就会在容器中寻找和该bean的属性具有相同类型的bean进行连接。如果有多个bean符合条件,就会报错,抛出异常。
- constructor:这种方式类似于byType,需要提供给构造器参数,如果没有确定带参数的构造器参数类型,就会报错,抛出异常。
- autodetect:首先尝试用constructor来自动装配,如果无法工作,则使用byType的方式。
bean是线程安全的吗?
不是
Spring AOP
什么是Spring AOP?
AOP:面向切面编程,将系统的业务代码与功能代码分离,减少系统的重复代码,降低耦合度。它的基本单元是切面(Aspect)。
Spring AOP就是负责实施切面的框架,他将切面所定义的横切逻辑加入到切面所指定的连接点中。
Spring AOP就是负责实施切面的框架,他将切面所定义的横切逻辑加入到切面所指定的连接点中。
什么是切面(Aspect)?
切面(Aspect)由切点(pointcut)和通知(adivce)组成。它包含了横切逻辑(通知的方式)的一些定义,也包含了连接点的一些定义。
实现方式:
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>
@Aspect
public class AspectModule {
}
实现方式:
- xml:
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>
- 注解:
@Aspect
public class AspectModule {
}
什么是切点(pointcut)?
匹配连接点的表达式,通过切点可以匹配到连接点。
实现方式:
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
@Pointcut("execution(* com.xyz.myapp.service.*.*(..))") // expression
private void businessService() {} // signature
实现方式:
- xml:
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
- 注解:
@Pointcut("execution(* com.xyz.myapp.service.*.*(..))") // expression
private void businessService() {} // signature
什么是通知(adivice)?
在连接点(joinpoint)处,切面所采取的一些动作。
通知类型:
实现方式:
public void doBeforeTask(){
...
}
通知类型:
- Before(前置通知):这些类型的通知在连接点方法之前执行。
- After(后置通知):在连接点方法执行之后执行。
- After Returning(成功返回后通知):在连接点方法成功返回后执行。
- After Throwing(抛出异常后通知):在连接点方法抛出异常时执行。
- Around(环绕通知):在连接点方法之前和之后执行。
实现方式:
- xml:<aop:before pointcut-ref="切点" method="要执行的方法"/>
- 注解:
public void doBeforeTask(){
...
}
什么是连接点(joinpoint)?
需要被切面拦截的哪些方法。
Spring JDBC
什么是Spring JDBC?
Spring用来操作数据库的那个模块。
JDBC反射
通过反射com.mysql.jdbc.Driver类,实例化该类的时候会执行该类内部的静态代码块,该代码块会在Java实现的DriverManager类中注册自己,DriverManager管理所有已经注册的驱动类,当调用DriverManager.geConnection方法时会遍历这些驱动类,并尝试去连接数据库,只要有一个能连接成功,就返回Connection对象,否则则报异常。
简单使用
- 配置数据源,配置jdbcTemplate
- 增,删。改操作:获得jdbcTemplate调用update方法执行SQL。
- 查操作:先写一个mapper类继承实现RowMapper<实体类>的mapRow方法,然后将那些返回的属性set进去,jdbcTemplate调用quert方法执行SQL返回呢个实现的mapper类类型。
dao中使用
- 配置数据源,配置jdbcTemplate。
- 写一个实体类。
- 写一个dao接口,定义一些操作方法。
- 写一个dao接口的实现类,将jdbcTemplate注入,实现那些操作方法。
- 将这个实现类配置到配置文件中。就可以调用dao来使用了。
Spring事务
什么是事务?
一系列操作要么完整地执行,要么完全不执行(要么一起成功,要么一起失败)。
事务特征
(ACID)原子性,一致性,持久性,隔离性
事务并发问题
2.提交覆盖丢失:一个事务修改数据提交,另一个事物也修改了该数据提交,那么第一次事务修改提交的数据会被覆盖。
- 脏读:一个事务读取到另一个事务未提交的数据。
- 不可重复读:一个事务多次读取一个数据时结果不一致,因为在多次读取的中间,另一个事务修改了这个数据。
- 幻读:一个事务多次读取几行数据时不一致,应为在多次读取中间,另一个事务新增了几行数据。
- 丢失修改:
2.提交覆盖丢失:一个事务修改数据提交,另一个事物也修改了该数据提交,那么第一次事务修改提交的数据会被覆盖。
Spring事务的配置方式
声明式事务:利用AOP在xml文件中配置需要事务的操作。
- 编程式事务:我们通过编程的方式管理事务,带来极大的灵活性,
public void create(){
try{
......
transactionManager.commit(status);
}catch(){
transactionManager.rollback(status);
}
}
声明式事务:利用AOP在xml文件中配置需要事务的操作。
Spring隔离级别
- 读取未提交(READ-UNCOMMITTED): 最低隔离级别,允许读取事务未提交的数据变更。可能会导致脏读,不可重复读,幻读。
- 读取已提交(READ-COMMITTED):允许读取并发事务已提交的数据变更。可能会导致不可重复读,幻读。
- 可重复读(REPEATABLE-READ): 对同一字段的数据多次读取的结果是一致的,除非是事务自己修改的,可能会导致幻读。(锁定数据库一行)
- 可串行化(SERIALIZABLE): 最高隔离级别,所有事务依次执行,可解决脏读,不可重复读,幻读。(锁定数据库表)
Spring事务传播
不要事务
- PROPAGHATION_NEVER:不支持当前事务;如果存在当前事务,就抛出一个异常。
- PROPAGATION_NOT_SUPPORTED:没有就非事务执行,有就挂起,然后非事务执行。
可有可无
- PORPAGATION_SUPPORTED:有事务就用,没有就不用。
必须有事务
- PORPAGATION_REQUIRES_NEW:有没有都新建事务,如果原来有,就将原来的挂起。
- PORPAGATION_NESTED:如果没有,就新建一个事务;如果有,就在当前事务中嵌套其他事务。
- PORPAGATION_REQYIRED:如果没有,就新建一个事物;如果有,就加入当前事务。
- PORPAGATION_MANDATORY:如果没有,就抛出异常;如果有,就使用当前事务。
Spring MVC
什么是Spring MVC框架?
MVC是一种模型-视图-控制的设计模式。SpringMVC是实现了这一种设计模式的框架,他把web应用分成了逻辑清晰的几部分。在Spring MVC中一般把项目分为Contriller层,Service层,dao层和Entity层
工作原理
1.用户发送request请求的URL给DispatcharServlet控制器。
2.DispatcharSerlvet会根据这个请求调用处理映射器找到对应的handler(Controller控制器)返回给自己。
3.DispacherServlet再把这个找到的Handler交给处理适配器处理,返回一个ModelAndView对象,Model是数据对象,View只是一个逻辑上的View,不是真正的view。
4.视图解析器会根据逻辑view去找到真正的View将他返回给DispatcherServlet。
5.DispatcherServlet把之前返回的Model传给View进行视图渲染。
6.最后把view返回给请求者。(response响应)。
2.DispatcharSerlvet会根据这个请求调用处理映射器找到对应的handler(Controller控制器)返回给自己。
3.DispacherServlet再把这个找到的Handler交给处理适配器处理,返回一个ModelAndView对象,Model是数据对象,View只是一个逻辑上的View,不是真正的view。
4.视图解析器会根据逻辑view去找到真正的View将他返回给DispatcherServlet。
5.DispatcherServlet把之前返回的Model传给View进行视图渲染。
6.最后把view返回给请求者。(response响应)。
Spring Boot
什么是SpringBoot?
- SpringBoot是一个快速开发框架。
- 相比于Spring,他可以快速整合第三方框架,
- 它内部还有自带的Tomcat容器,对于一些项目只需要打一个jar就可以进行部署,简化了我们的操作。
- 它采用javaConfig和注解的形式进行配置,不需要复杂的XML配置。
Web开发
静态资源访问
默认在resources下的static,public、resources、META-INF/resources和webapp下。
修改默认:如设置spring.resources.staticLocations=classpath\:/html/,在resources下的html可以访问,static、public、resources就不可以访问
修改默认:如设置spring.resources.staticLocations=classpath\:/html/,在resources下的html可以访问,static、public、resources就不可以访问
渲染Web页面
Thymeleaf
freemarker
数据访问
使用jdbcTemplate
使用Spring-Data-jpa
使用Mybatis
整合多数据源
事务管理
加上@Transactional
日志管理
log4j
lombok
缓存支持
EhCache
Redis
热部署
Spring-boot-devtools的jar包
监控管理
Actuator的jar包
Mybatis
Mybatis简介
什么是MyBatis?
- MyBatis是一个半ORM框架,它内部封装了JDBC,与JDBC相比,开发时我们只需要关注SQL就行,不需要过多的关注连接方面。
- Mybatis是通过文件或注解的方式配置的。
- 它还可以把查询的结果映射为java对象返回。
resultMap
一对一:property实体类,column数据库属性,association关联表
一对多:多加一个collection
一对多:多加一个collection
动态SQL
if,where,set,trim,foreach,choose,when,otherwise
常见面试题
#{}和${}的区别?
mybatis在处理#{}时,会将其替换为?号,然后再调用PreparedStatement的set方法来赋值,这样做可以防止一些SQL注入,提高安全性。而在处理${}时,会将其直接替换成变量的值。#{}是预编译处理,${}是字符串替换。
Mybatis怎样分页?
使用pageHelper组件,在查询语句之前加入PageHelper.startPage(page,zize)方法,再把查询后的数据放入PageInfo就实现了分页。
猜想原理:拦截要执行的SQL,在SQL中加入dialect方言吧。
猜想原理:拦截要执行的SQL,在SQL中加入dialect方言吧。
为什么是半ORM框架?
因为需要手动编写SQL。
数据库
MySQL
MySQL简介
什么是MySQL?
MySQL是一个关系型数据库管理系统(RDBMS),它是可以处理拥有上千万条记录的大型数据库,支持多种存储引擎,它还支持事务与索引。
存储引擎MylSAM与lnnDB的区别?
- innoDB是聚集索引,支持事务,支持行级锁;MylSQM是非聚集索引,不支持事务,只支持表级锁
MylSAM是MySQL 5.0之前的默认数据库引擎。InnoDB是MySQL 5.5起默认的数据库引擎。 - InnoDB支持事务,MyISAM不支持事务,但每次查询都是原子的。
- MyISAM支持表级锁,InnoDB支持行级锁(某些情况下还是锁整表,如 update table set a=1 where user like ‘% lee%’)。
- MyISAM不支持外键,InnoDB支持。
- 对于自增长的字段,InnoDB 中必须包含只有该字段的索引,但是在 MyISAM 表中可以和其他字段一起建立联合索引。
- InnoDB 中不保存表的行数,如 select count () from table 时,InnoDB 需要扫描一遍整个表来计算有多少行,但是 MyISAM 只要简单的读出保存好的行数即可。注意的是,当 count () 语句包含 where 条件时 MyISAM 也需要扫描整个表。
- DELETE FROM table 时,InnoDB 不会重新建立表,而是一行一行的 删除,效率非常慢。MyISAM 则会重建表。
- MyISAM 适合查询以及插入为主的应用。InnoDB 适合频繁修改以及涉及到安全性较高的应用。
数据类型
数值类型
- int是Integer的同义词,int与Integer类型没有区别。
日期/时间类型
字符串类型
- Char是定长字符串,VarChar是变长字符串。Char的长度范围是1~255,当Char值被存储时,它们被用空格填充到特定的长度,检索时Char值需要删除尾随空格。
- BLOB 是一个二进制对象,可以容纳可变数量的数据。TEXT 是一个不区分大小写的 BLOB。BLOB 和 TEXT 类型之间的唯一区别在于对 BLOB 值进行排序和比较时区分大小
写,对 TEXT 值不区分大写。
主键、外键、超键、候选键
- 主键:数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值。
- 外键:在一个表中存在的另一个表的主键称此表的外键。
- 超键:在关系中能唯一标识元祖的属性集称为关系模式的超键。一个属性可以作为一个超键,多个属性组合在一起也可以作为一个超键。超键包含候选键和主键。
- 候选键:最小超键,即没有冗余元素的超键。
存储过程
- 存储过程是一个预编译的 SQL 语句,优点是允许模块化的设计,就是说只需创建一次,以后在该程序中就可以调用多次。如果某次操作需要执行多次 SQL,使用存储过程比单纯 SQL 语句执行要快。
- 触发器是一种特殊的存储过程,主要是通过事件来触发而被执行的。他可以强化约束,来维护数据的完整性和一致性,可以跟踪数据库内的操作从而不允许未经许可的更新和变化。可以联级运算。如,某表上的触发器上包含对另一个表的数据操作,而该操作又会导致该表触发器被触发。
- 存储过程与函数的区别:存储过程是第一次编译之后就会被存储的下来的预编译对象,之后无论何时调用它都会去执行已经编译好的代码。而函数每次执行都需要编译一次。
视图
视图是一种虚拟的表,具有和物理表相同的功能。可以对视图进行增,改,查,操作,视图通常是有一个表或者多个表的行或列的子集。对视图的修改会影响基本表。它使得我们获取数据更容易,相比多表查询。
优点:
缺点:
优点:
- 简化了操作,把经常使用的数据定义为视图。
- 安全性,用户只能查询和修改能看到的数据。
- 逻辑上的独立性,屏蔽了真实表的结构带来的影响。
缺点:
- 性能差:数据库必须把视图查询转化成对基本表的查询,如果这个视图是由一个复杂的多表查询所定义,那么,即使是视图的一个简单查询,数据库也要把它变成一个复杂的结合体,需要花费一定的时间。
- 修改限制:当用户试图修改视图的某些信息时,数据库必须把它转化为对基本表的某些信息的修改,对于简单的视图来说,这是很方便的,但是,对于比较复杂的视图,可能是不可修改的。
游标
游标是对查询出来的结果集作为一个单元来有效的处理。游标可以定在该单元中的特定行,从结果集的当前行检索一行或多行。可以对结果集当前行做修改。一般不使用游标,但是需要逐条处理数据的时候,游标显得十分重要。
临时表
MySQL 临时表在我们需要保存一些临时数据时是非常有用的。临时表只在当前连接可见,当关闭连接时,MySQL会自动删除表并释放所有空间。
数据库范式
- 第一范式(确保每列保持原子性) :所有字段值都是不可分解的原子值。
- 第二范式(确保表中的每列都和主键相关):符合1NF, 在一个数据库表中,一个表中只能保存一种数据,不可以把多种数据保存在同一张数据库表中。
- 第三范式:符合2NF,并要求任何非主属性不依赖于其他非主属性。
- BC范式(BCNF)(确保每列都和主键列直接相关,而不是间接相关) :符合3NF,数据表中的每一列数据都和主键直接相关,而不能间接相关。
- 第四范式:消除多值依赖,要求把同一表内的多对多关系删除。
- 第五范式:消除传递依赖,从最终结构重新建立原始结构。
MySQL基本操作
SQL语言
- DDL(Data Definiton Language):数据定义语言,用来定义数据库对象:库,表,列等;
- DML(Data Manipulation Language):数据操作语言,用来定义数据库记录(数据):增,删,改的表记录
- DCL(Data Control Language):数据控制语言,用来定义访问权限和安全级别
- DQL(Data Query Language):数据查询语言,用来查询表记录(数据)
七种join
MySQL事务与锁
事务ACID
行锁,表锁,页级锁,读锁,写锁,悲观锁,乐观锁
隔离级别,依次解决的问题(脏读、不可重复读、幻读)、隔离级别与加锁的关系
MySQL索引
什么是索引?
数据库索引,是数据库管理系统中一个排序的数据结构,索引的实现通常使用 B 树及其变种 B + 树。
数据库系统除了保存数据之外,还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据,这样就可以在这些数据结构上实现高级查找算法。这种实现了排序的高级查找算法的数据结构就是索引。
作用:协助快速查询,更新数据库表中数据。
代价:1.增加了数据库的存储空间。2.在插入和修改数据时要花费较多的时间。
数据库系统除了保存数据之外,还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据,这样就可以在这些数据结构上实现高级查找算法。这种实现了排序的高级查找算法的数据结构就是索引。
作用:协助快速查询,更新数据库表中数据。
代价:1.增加了数据库的存储空间。2.在插入和修改数据时要花费较多的时间。
索引底层结构
B+树
Hash索引
Hash索引
索引优化方式
1.应该在这些列上创建索引(唯一,不为空,经常被查询):
2.不应该在这些列创建索引
- 在经常需要搜索的列上
- 在作为主键的列上,强制该列的唯一性和组织表中数据的排列结构
- 在经常用在连接的列上,这些列主要是一些外键,可以加快连接的速度
- 在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序加快排序查询的时间
- 在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的在经常使用在WHERE字句的列上创建索引,加快条件的判断速度
2.不应该在这些列创建索引
- 对于那些在查询中很少使用的列
- 对于那些只有很少数值的列
- 对于那些定义为text、image、bit这些数据量很大的数据类型的列
- 当修改性能远远大于检索性能时,不应该创建索引。修改性能与检索性能时互相矛盾的。当增加索引时,会提高检索性能,但是会降低修改性能。反之亦然。
索引失效条件
- 在where子句中使用 != 或 <>操作符
- 在where子句中使用or来连接条件,当连接的字段有字段没有索引时,将会导致所有字段的索引失效
- 在where子句中对字段进行null值判断
- 在where子句中like的模糊匹配以%开头
- 在where子句中对有索引的字段进行表达式或函数操作
- 如果执行引擎估计使用全表扫描要比使用索引快,则不使用索引
聚集索引和非聚集索引
- 聚集索引表记录的排列顺序和索引的排列顺序一致,所以查询效率快,只要找到第一个索引值记录,其余就连续性的记录在物理也一样连续存放。聚集索引对应的缺点就是修改慢,因为为了保证表中记录的物理和索引顺序一致,在记录插入的时候,会对数据页重新排序。
- 非聚集索引指定了表中记录的逻辑顺序,但是记录的物理和索引不一定一致,两种索引都采用 B + 树结构,非聚集索引的叶子层并不和实际数据页相重叠,而采用叶子层包含一个指向表中的记录在数据页中的指针方式。非聚集索引层次多,不会造成数据重排。
- 根本区别:聚集索引和非聚集索引的根本区别是表记录的排列顺序和与索引的排列顺序是否一致(类似链表和数组的区别)。
辅助索引、主键索引、聚簇索引、非聚簇索引、索引回表、索引覆盖、索引下推
主键索引就是聚集索引,聚集索引会保存行上的所有数据,因此不需要额外的 IO
辅助索引 (Secondary Index) , 叶子节点只保存了行的键值和指向对应行的 "书签" , 一般指向的是聚集索引,此外 innodb 实现了覆盖索引 (Covering index) , 即叶子节点除了保存该行的键值还保存了对应索引列的值,如果不需要额外数据的话则不需要另外对聚集索引中的数据进行 IO
辅助索引 (Secondary Index) , 叶子节点只保存了行的键值和指向对应行的 "书签" , 一般指向的是聚集索引,此外 innodb 实现了覆盖索引 (Covering index) , 即叶子节点除了保存该行的键值还保存了对应索引列的值,如果不需要额外数据的话则不需要另外对聚集索引中的数据进行 IO
主从复制
分库分表
读写分离
Redis
0 条评论
下一页