失效链接处理 |
2017-2020年字节跳动Android面条题汇总附答案 PDF 下载 下载地址:
提取码:jnqq
相关截图: 主要内容:
2017-2020 字节跳动 Android 面试真题
解析
目录
2017-2020 字节跳动 Android 面试真题解析................................................................................... 1
目录..................................................................................................................................................... 1
第一章计算机基础面试题.........................................................................................................1
第一节、网络面试题.........................................................................................................1
第二节、操作系统面试题 (⭐⭐⭐)........................................................................ 21
第三节、数据库面试题 (⭐).................................................................................... 23
第二章 数据结构和算法面试题.............................................................................................25
数据结构与算法...............................................................................................................25
第三章 Java 面试题..................................................................................................................33
第一节 Java 基础面试题..................................................................................................33
第二节 Java 并发面试题.................................................................................................81
第三节 Java 虚拟机面试题 (⭐⭐⭐)......................................................................121
第四章 Android 面试题.........................................................................................................140
第一节 Android 基础面试题 (⭐⭐⭐).................................................................. 140
第二节 Android 高级面试题 (⭐⭐⭐)................................................................... 208
第五章 其他扩展面试题.......................................................................................................346
一、Kotlin (⭐⭐).................................................................................................... 346
二、大前端 (⭐⭐)...................................................................................................346
三、脚本语言 (⭐⭐)...............................................................................................349
第六章非技术面试题.............................................................................................................350
一、高频题集 (⭐⭐⭐)...........................................................................................350
二、次高频题集 (⭐⭐)...........................................................................................352
第一章计算机基础面试题
第一节、网络面试题
一、HTTP/HTTPS (⭐⭐⭐)1、HTTP 与 HTTPS 有什么区别?
HTTPS 是一种通过计算机网络进行安全通信的传输协议。HTTPS 经由 HTTP 进行通信,但利
用 SSL/TLS 来加密数据包。HTTPS 开发的主要目的,是提供对网站服务器的身份 认证,保
护交换数据的隐私与完整性。
HTPPS 和 HTTP 的概念:
HTTPS(全称:Hypertext Transfer Protocol over Secure Socket Layer),是以安全为目标
的 HTTP 通道,简单讲是 HTTP 的安全版。即 HTTP 下加入 SSL 层,HTTPS 的安全基础是 SSL,
因此加密的详细内容就需要 SSL。它是一个 URI scheme(抽象标识符体系),句法类同 http:
体系。用于安全的 HTTP 数据传输。https:URL 表明它使用了 HTTP,但 HTTPS 存在不同于
HTTP 的默认端口及一个加密/身份验证层(在 HTTP 与 TCP 之间)。这个系统的最初研发由
网景公司进行,提供了身份验证与加密通讯方法,现在它被广泛用于万维网上安全敏感的通
讯,例如交易支付方面。
超文本传输协议 (HTTP-Hypertext transfer protocol) 是一种详细规定了浏览器和万维网服
务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议。
https 协议需要到 ca 申请证书,一般免费证书很少,需要交费。http 是超文本传输协议,
信息是明文传输,https 则是具有安全性的 ssl 加密传输协议 http 和 https 使用的是完全不
同的连接方式用的端口也不一样,前者是 80,后者是 443。http 的连接很简单,是无状态的
HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议 要比 http 协
议安全 HTTPS 解决的问题:1 . 信任主机的问题. 采用 https 的 server 必须从 CA 申请一个
用于证明服务器用途类型的证书. 改证书只有用于对应的 server 的时候,客户度才信任次主
机 2 . 防止通讯过程中的数据的泄密和被窜改
如下图所示,可以很明显的看出两个的区别:
注:TLS 是 SSL 的升级替代版,具体发展历史可以参考传输层安全性协议。HTTP 与 HTTPS 在写法上的区别也是前缀的不同,客户端处理的方式也不同,具体说来:
如果 URL 的协议是 HTTP,则客户端会打开一条到服务端端口 80(默认)的连接,并向其
发送老的 HTTP 请求。 如果 URL 的协议是 HTTPS,则客户端会打开一条到服务端端口 443
(默认)的连接,然后与服务器握手,以二进制格式与服务器交换一些 SSL 的安全参数,附
上加密的 HTTP 请求。 所以你可以看到,HTTPS 比 HTTP 多了一层与 SSL 的连接,这也就
是客户端与服务端 SSL 握手的过程,整个过程主要完成以下工作:
交换协议版本号 选择一个两端都了解的密码 对两端的身份进行认证 生成临时的会话密
钥,以便加密信道。 SSL 握手是一个相对比较复杂的过程,更多关于 SSL 握手的过程细节
可以参考 TLS/SSL 握手过程
SSL/TSL 的常见开源实现是 OpenSSL,OpenSSL 是一个开放源代码的软件库包,应用程序
可以使用这个包来进行安全通信,避免窃听,同时确认另一端连接者的身份。这个包广泛被
应用在互联网的网页服务器上。 更多源于 OpenSSL 的技术细节可以参考 OpenSSL。
2、Http1.1 和 Http1.0 及 2.0 的区别?
HTTP1.0 和 HTTP1.1 的一些区别
HTTP1.0 最早在网页中使用是在 1996 年,那个时候只是使用一些较为简单的网页上和网络
请求上,而 HTTP1.1 则在 1999 年才开始广泛应用于现在的各大浏览器网络请求中,同时
HTTP1.1 也是当前使用最为广泛的 HTTP 协议。 主要区别主要体现在:
1、缓存处理,在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做
为缓存判断的标准,HTTP1.1 则引入了更多的缓存控制策略例如 Entity tag,
If-Unmodified-Since, If-Match, If-None-Match 等更多可供选择的缓存头来控制缓
存策略。
2、带宽优化及网络连接的使用,HTTP1.0 中,存在一些浪费带宽的现象,例如客户
端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点
续传功能,HTTP1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,
即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利
用带宽和连接。
3、错误通知的管理,在 HTTP1.1 中新增了 24 个错误状态响应码,如 409(Conflict)
表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资
源被永久性的删除。
4、Host 头处理,在 HTTP1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,
请求消息中的 URL 并没有传递主机名(hostname)。但随着虚拟主机技术的发展,
在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个 IP 地址。HTTP1.1 的请求消息和响应消息都应支持 Host 头域,且请求
消息中如果没有 Host 头域会报告一个错误(400 Bad Request)。
5、长连接,HTTP 1.1 支持长连接(PersistentConnection)和请求的流水线
(Pipelining)处理,在一个 TCP 连接上可以传送多个 HTTP 请求和响应,减少了建
立和关闭连接的消耗和延迟,在 HTTP1.1 中默认开启 Connection: keep-alive,一
定程度上弥补了 HTTP1.0 每次请求都要创建连接的缺点。
SPDY
在讲 Http1.1 和 Http2.0 的区别之前,还需要说下 SPDY,它是 HTTP1.x 的优化方案:
2012 年 google 如一声惊雷提出了 SPDY 的方案,优化了 HTTP1.X 的请求延迟,解决了
HTTP1.X 的安全性,具体如下:
1、降低延迟,针对HTTP高延迟的问题,
SPDY优雅的采取了多路复用(multiplexing)。
多路复用通过多个请求 stream 共享一个 tcp 连接的方式,解决了 HOL blocking 的
问题,降低了延迟同时提高了带宽的利用率。
2、请求优先级(request prioritization)。多路复用带来一个新的问题是,在连接
共享的基础之上有可能会导致关键请求被阻塞。SPDY 允许给每个 request 设置优先
级,这样重要的请求就会优先得到响应。比如浏览器加载首页,首页的 html 内容应
该优先展示,之后才是各种静态资源文件,脚本文件等加载,这样可以保证用户能
第一时间看到网页内容。
3、header 压缩。前面提到 HTTP1.x 的 header 很多时候都是重复多余的。选择合适
的压缩算法可以减小包的大小和数量。
4、基于 HTTPS 的加密协议传输,大大提高了传输数据的可靠性。
5、服务端推送(server push),采用了 SPDY 的网页,例如我的网页有一个 sytle.css
的请求,在客户端收到 sytle.css 数据的同时,服务端会将 sytle.js 的文件推送给客户
端,当客户端再次尝试获取 sytle.js 时就可以直接从缓存中获取到,不用再发请求了。
SPDY 构成图:SPDY 位于 HTTP 之下,TCP 和 SSL 之上,这样可以轻松兼容老版本的 HTTP 协议(将 HTTP1.x
的内容封装成一种新的 frame 格式),同时可以使用已有的 SSL 功能。
HTTP2.0 和 HTTP1.X 相比的新特性
新的二进制格式(Binary Format),HTTP1.x 的解析是基于文本。基于文本协议的
格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然
很多,二进制则不同,只认 0 和 1 的组合。基于这种考虑 HTTP2.0 的协议解析决定
采用二进制格式,实现方便且健壮。
多路复用(MultiPlexing),即连接共享,即每一个 request 都是是用作连接共享机
制的。一个 request 对应一个 id,这样一个连接上可以有多个 request,每个连接的
request 可以随机的混杂在一起,接收方可以根据 request 的 id 将 request 再归属
到各自不同的服务端请求里面。
header 压缩,如上文中所言,对前面提到过 HTTP1.x 的 header 带有大量信息,而
且每次都要重复发送,HTTP2.0 使用 encoder 来减少需要传输的 header 大小,通讯
双方各自 cache 一份 header fields 表,既避免了重复 header 的传输,又减小了需
要传输的大小。
服务端推送(server push),同 SPDY 一样,HTTP2.0 也具有 server push 功能。需要更深的理解请点击这里
3、Https 请求慢的解决办法
1、不通过 DNS 解析,直接访问 IP
2、解决连接无法复用
http/1.0 协议头里可以设置 Connection:Keep-Alive 或者 Connection:Close,选择是否允许
在一定时间内复用连接(时间可由服务器控制)。但是这对 App 端的请求成效不大,因为
App 端的请求比较分散且时间跨度相对较大。
方案 1.基于 tcp 的长连接 (主要) 移动端建立一条自己的长链接通道,通道的实现是基于
tcp 协议。基于 tcp 的 socket 编程技术难度相对复杂很多,而且需要自己定制协议。但信息
的上报和推送变得更及时,请求量爆发的时间点还能减轻服务器压力(避免频繁创建和销毁
连接)
方案 2.http long-polling 客户端在初始状态发送一个 polling 请求到服务器,服务器并不会
马上返回业务数据,而是等待有新的业务数据产生的时候再返回,所以链接会一直被保持。
一但结束当前连接,马上又会发送一个新的 polling 请求,如此反复,保证一个连接被保持。
存在问题: 1)增加了服务器的压力 2)网络环境复杂场景下,需要考虑怎么重建健康的
连接通道 3)polling的方式稳定性不好 4)polling的response可能被中间代理cache住 ……
方案 3.http streaming 和 long-polling 不同的是,streaming 方式通过再 server response
的头部增加“Transfer Encoding:chuncked”来告诉客户端后续还有新的数据到来 存在问题:
1)有些代理服务器会等待服务器的 response 结束之后才将结果推送给请求客户端。
streaming 不会结束 response 2)业务数据无法按照请求分割 ……
方案 4.web socket 和传统的 tcp socket 相似,基于 tcp 协议,提供双向的数据通道。它的
优势是提供了 message 的概念,比基于字节流的 tcp socket 使用更简单。技术较新,不是
所有浏览器都提供了支持。
3、解决 head of line blocking
它的原因是队列的第一个数据包(队头)受阻而导致整列数据包受阻
使用 http pipelining,确保几乎在同一时间把 request 发向了服务器
4、Http 的 request 和 response 的协议组成
1、Request 组成
客户端发送一个 HTTP 请求到服务器的请求消息包括以下格式:
请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。请求行以一个方法符号开头,以空格分开,后面跟着请求的 URI 和协议的版本。
Get 请求例子
GET /562f25980001b1b106000338.jpg HTTP/1.1
Host
img.mukewang.com
User-Agent Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/51.0.2704.106 Safari/537.36
Accept image/webp,image/*,*/*;q=0.8
Referer http://www.imooc.com/
Accept-Encoding gzip, deflate, sdch
Accept-Language zh-CN,zh;q=0.8
第一部分:请求行,用来说明请求类型,要访问的资源以及所使用的 HTTP 版本. GET 说明请
求类型为 GET,[/562f25980001b1b106000338.jpg]为要访问的资源,该行的最后一部分说明
使用的是 HTTP1.1 版本。 第二部分:请求头部,紧接着请求行(即第一行)之后的部分,
用来说明服务器要使用的附加信息 从第二行起为请求头部,HOST 将指出请求的目的
地.User-Agent,服务器端和客户端脚本都能访问它,它是浏览器类型检测逻辑的重要基础.该
信息由你的浏览器来定义,并且在每个请求中自动发送等等 第三部分:空行,请求头部后面
的空行是必须的 即使第四部分的请求数据为空,也必须有空行。 第四部分:请求数据也叫
主体,可以添加任意的其他数据。 这个例子的请求数据为空。
POST 请求例子
POST / HTTP1.1
Host:www.wrox.com
User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR
2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)Content-Type:application/x-www-form-urlencoded
Content-Length:40
Connection: Keep-Alive
name=Professional%20Ajax&publisher=Wiley
第一部分:请求行,第一行明了是 post 请求,以及 http1.1 版本。
第二部分:请求头部,第二行至第六行。
第三部分:空行,第七行的空行。
第四部分:请求数据,第八行。
2、Response 组成
一般情况下,服务器接收并处理客户端发过来的请求后会返回一个 HTTP 的响应消息。
HTTP 响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。
第一部分:状态行,由 HTTP 协议版本号, 状态码, 状态消息 三部分组成。
第一行为状态行,(HTTP/1.1)表明 HTTP 版本为 1.1 版本,状态码为 200,状态消息为(ok)
第二部分:消息报头,用来说明客户端要使用的一些附加信息
第二行和第三行为消息报头, Date:生成响应的日期和时间;Content-Type:指定了 MIME
类型的 HTML(text/html),编码类型是 UTF-8
第三部分:空行,消息报头后面的空行是必须的第四部分:响应正文,服务器返回给客户端的文本信息。
空行后面的 html 部分为响应正文。
5、谈谈对 http 缓存的了解。
HTTP 的缓存机制也是依赖于请求和响应 header 里的参数类实现的,最终响应式从缓存中
去,还是从服务端重新拉取,HTTP 的缓存机制的流程如下所示:
HTTP 的缓存可以分为两种:
强制缓存:需要服务端参与判断是否继续使用缓存,当客户端第一次请求数据是,服务端返
回了缓存的过期时间(Expires 与 Cache-Control),没有过期就可以继续使用缓存,否则则
不适用,无需再向服务端询问。 对比缓存:需要服务端参与判断是否继续使用缓存,当客
户端第一次请求数据时,服务端会将缓存标识(Last-Modified/If-Modified-Since 与
Etag/If-None-Match)与数据一起返回给客户端,客户端将两者都备份到缓存中 ,再次请
求数据时,客户端将上次备份的缓存 标识发送给服务端,服务端根据缓存标识进行判断,
如果返回 304,则表示通知客户端可以继续使用缓存。 强制缓存优先于对比缓存。
上面提到强制缓存使用的的两个标识:
Expires:Expires 的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返
回的到期时间,直接使用缓存数据。到期时间是服务端生成的,客户端和服务端的时间可能
有误差。 Cache-Control:Expires 有个时间校验的问题,所有 HTTP1.1 采用 Cache-Control
替代 Expires。 Cache-Control 的取值有以下几种:private: 客户端可以缓存。 public: 客户端和代理服务器都可缓存。 max-age=xxx: 缓存的
内容将在 xxx 秒后失效 no-cache: 需要使用对比缓存来验证缓存数据。 no-store: 所有内
容都不会缓存,强制缓存,对比缓存都不会触发。 我们再来看看对比缓存的两个标识:
Last-Modified/If-Modified-Since
Last-Modified 表示资源上次修改的时间。
当客户端发送第一次请求时,服务端返回资源上次修改的时间:
Last-Modified: Tue, 12 Jan 2016 09:31:27 GMT
客户端再次发送,会在 header 里携带 If-Modified-Since。将上次服务端返回的资源时间上
传给服务端。
If-Modified-Since: Tue, 12 Jan 2016 09:31:27 GMT
服务端接收到客户端发来的资源修改时间,与自己当前的资源修改时间进行对比,如果自己
的资源修改时间大于客户端发来的资源修改时间,则说明资源做过修改, 则返回 200 表示
需要重新请求资源,否则返回 304 表示资源没有被修改,可以继续使用缓存。
上面是一种时间戳标记资源是否修改的方法,还有一种资源标识码 ETag 的方式来标记是否
修改,如果标识码发生改变,则说明资源已经被修改,ETag 优先级高于 Last-Modified。
Etag/If-None-Match
ETag 是资源文件的一种标识码,当客户端发送第一次请求时,服务端会返回当前资源的标
识码:
ETag: "5694c7ef-24dc"
客户端再次发送,会在 header 里携带上次服务端返回的资源标识码:
If-None-Match:"5694c7ef-24dc" 服务端接收到客户端发来的资源标识码,则会与自己当前
的资源吗进行比较,如果不同,则说明资源已经被修改,则返回 200,如果相同则说明资源
没有被修改,返回 304,客户端可以继续使用缓存。
6、Http 长连接。
Http1.0 是短连接,HTTP1.1 默认是长连接,也就是默认 Connection 的值就是 keep-alive。
但是长连接实质是指的 TCP 连接,而不是 HTTP 连接。TCP 连接是一个双向的通道,它是可
以保持一段时间不关闭的,因此 TCP 连接才有真正的长连接和短连接这一说。
Http1.1 为什么要用使用 tcp 长连接?
长连接是指的 TCP 连接,也就是说复用的是 TCP 连接。即长连接情况下,多个 HTTP 请求
可以复用同一个 TCP 连接,这就节省了很多 TCP 连接建立和断开的消耗。此外,长连接并不是永久连接的。如果一段时间内(具体的时间长短,是可以在 header 当
中进行设置的,也就是所谓的超时时间),这个连接没有 HTTP 请求发出的话,那么这个长
连接就会被断掉。
需要更深的理解请点击这里
7、Https 加密原理。
加密算法的类型基本上分为了两种:
对称加密,加密用的密钥和解密用的密钥是同一个,比较有代表性的就是 AES 加
密算法;
非对称加密,加密用的密钥称为公钥,解密用的密钥称为私钥,经常使用到的 RSA
加密算法就是非对称加密的;
此外,还有 Hash 加密算法
HASH 算法:MD5, SHA1, SHA256
相比较对称加密而言,非对称加密安全性更高,但是加解密耗费的时间更长,速度慢。
想了解更多加密算法请点击这里
HTTPS = HTTP + SSL,HTTPS 的加密就是在 SSL 中完成的。
这就要从 CA 证书讲起了。CA 证书其实就是数字证书,是由 CA 机构颁发的。至于 CA 机
构的权威性,那么是毋庸置疑的,所有人都是信任它的。CA 证书内一般会包含以下内容:
证书的颁发机构、版本
证书的使用者
证书的公钥
证书的有效时间
证书的数字签名 Hash 值和签名 Hash 算法
...
客户端如何校验 CA 证书?
CA 证书中的 Hash 值,其实是用证书的私钥进行加密后的值(证书的私钥不在 CA 证书
中)。然后客户端得到证书后,利用证书中的公钥去解密该 Hash 值,得到 Hash-a ;然
后再利用证书内的签名 Hash 算法去生成一个 Hash-b 。最后比较 Hash-a 和 Hash-b 这
两个的值。如果相等,那么证明了该证书是对的,服务端是可以被信任的;如果不相等,那
么就说明该证书是错误的,可能被篡改了,浏览器会给出相关提示,无法建立起 HTTPS 连
接。除此之外,还会校验 CA 证书的有效时间和域名匹配等。HTTPS 中的 SSL 握手建立过程
假设现在有客户端 A 和服务器 B :
1 、 首 先 , 客 户 端 A 访 问 服 务 器 B , 比 如 我 们 用 浏 览 器 打 开 一 个 网
页 www.baidu.com ,这时,浏览器就是客户端 A ,百度的服务器就是服务器 B 了。
这时候客户端 A 会生成一个随机数 1,把随机数 1 、自己支持的 SSL 版本号以及
加密算法等这些信息告诉服务器 B 。
2、服务器 B 知道这些信息后,然后确认一下双方的加密算法,然后服务端也生成
一个随机数 B ,并将随机数 B 和 CA 颁发给自己的证书一同返回给客户端 A 。
3、客户端 A 得到 CA 证书后,会去校验该 CA 证书的有效性,校验方法在上面
已经说过了。校验通过后,客户端生成一个随机数 3 ,然后用证书中的公钥加密随
机数 3 并传输给服务端 B 。
4、服务端 B 得到加密后的随机数 3,然后利用私钥进行解密,得到真正的随机数
3。
5、最后,客户端 A 和服务端 B 都有随机数 1、随机数 2、随机数 3,然后双方利
用这三个随机数生成一个对话密钥。之后传输内容就是利用对话密钥来进行加解密
了。这时就是利用了对称加密,一般用的都是 AES 算法。
6、客户端 A 通知服务端 B ,指明后面的通讯用对话密钥来完成,同时通知服务
器 B 客户端 A 的握手过程结束。
7、服务端 B 通知客户端 A,指明后面的通讯用对话密钥来完成,同时通知客户端
A 服务器 B 的握手过程结束。
8、SSL 的握手部分结束,SSL 安全通道的数据通讯开始,客户端 A 和服务器 B 开
始使用相同的对话密钥进行数据通讯。
简化如下:
1、客户端和服务端建立 SSL 握手,客户端通过 CA 证书来确认服务端的身份;
2、互相传递三个随机数,之后通过这随机数来生成一个密钥;
3、互相确认密钥,然后握手结束;
4、数据通讯开始,都使用同一个对话密钥来加解密;
可以发现,在 HTTPS 加密原理的过程中把对称加密和非对称加密都利用了起来。即利用了
非对称加密安全性高的特点,又利用了对称加密速度快,效率高的好处。
需要更深的理解请点击这里
8、HTTPS 如何防范中间人攻击?
什么是中间人攻击?
当数据传输发生在一个设备(PC/手机)和网络服务器之间时,攻击者使用其技能和工具将
自己置于两个端点之间并截获数据;尽管交谈的两方认为他们是在与对方交谈,但是实际上
他们是在与干坏事的人交流,这便是中间人攻击。有几种攻击方式?
1、嗅探:嗅探或数据包嗅探是一种用于捕获流进和流出系统/网络的数据包的技术。
网络中的数据包嗅探就好像电话中的监听。
2、数据包注入: 在这种技术中,攻击者会将恶意数据包注入常规数据中。这样用
户便不会注意到文件/恶意软件,因为它们是合法通讯流的一部分。
3、会话劫持: 在你登录进你的银行账户和退出登录这一段期间便称为一个会话。
这些会话通常都是黑客的攻击目标,因为它们包含潜在的重要信息。在大多数案例
中,黑客会潜伏在会话中,并最终控制它。
4、SSL 剥离: 在 SSL 剥离攻击中,攻击者使 SSL/TLS 连接剥落,随之协议便从安
全的 HTTPS 变成了不安全的 HTTP。
HTTPS 如何防范中间人攻击:
请见 https 加密原理。
9、有哪些响应码,分别都代表什么意思?
1** 信息,服务器收到请求,需要请求者继续执行操作
2** 成功,操作被成功接收并处理
3** 重定向,需要进一步的操作以完成请求
4** 客户端错误,请求包含语法错误或无法完成请求
5** 服务器错误,服务器在处理请求的过程中发生了错误
二、TCP/UDP (⭐⭐⭐)
1、为什么 tcp 要经过三次握手,四次挥手?
重要标志位
ACK : TCP 协议规定,只有 ACK=1 时有效,也规定连接建立后所有发送的报文的 ACK 必
须为 1SYN(SYNchronization) : 在连接建立时用来同步序号。当 SYN=1 而 ACK=0 时,表明这是
一个连接请求报文。对方若同意建立连接,则应在响应报文中使SYN=1和ACK=1. 因此, SYN
置 1 就表示这是一个连接请求或连接接受报文。
FIN (finis)即完,终结的意思, 用来释放一个连接。当 FIN = 1 时,表明此报文段的发
送方的数据已经发送完毕,并要求释放连接。
三次握手、四次挥手过程
三次握手:
第一次握手:建立连接。客户端发送连接请求报文段,将 SYN 位置为 1,Sequence Number
为 x;然后,客户端进入 SYN_SEND 状态,等待服务器的确认;
第二次握手:服务器收到 SYN 报文段。服务器收到客户端的 SYN 报文段,需要对这个 SYN
报文段进行确认,设置 Acknowledgment Number 为 x+1(Sequence Number+1);同时,
自己自己还要发送 SYN 请求信息,将 SYN 位置为 1,Sequence Number 为 y;服务器端将
上述所有信息放到一个报文段(即 SYN+ACK 报文段)中,一并发送给客户端,此时服务器
进入 SYN_RECV 状态;
第三次握手:客户端收到服务器的 SYN+ACK 报文段。然后将 Acknowledgment Number
设置为 y+1,向服务器发送 ACK 报文段,这个报文段发送完毕以后,客户端和服务器端都
进入 ESTABLISHED 状态,完成 TCP 三次握手。四次挥手:
第一次分手:主机 1(可以使客户端,也可以是服务器端),设置 Sequence Number 和
Acknowledgment Number,向主机 2 发送一个 FIN 报文段;此时,主机 1 进入 FIN_WAIT_1
状态;这表示主机 1 没有数据要发送给主机 2 了;
第二次分手:主机 2 收到了主机 1 发送的 FIN 报文段,向主机 1 回一个 ACK 报文段,
Acknowledgment Number 为 Sequence Number 加 1;主机 1 进入 FIN_WAIT_2 状态;主
机 2 告诉主机 1,我“同意”你的关闭请求;
第三次分手:主机 2 向主机 1 发送 FIN 报文段,请求关闭连接,同时主机 2 进入 LAST_ACK
状态;
第四次分手:主机 1 收到主机 2 发送的 FIN 报文段,向主机 2 发送 ACK 报文段,然后主机
1 进入 TIME_WAIT 状态;主机 2 收到主机 1 的 ACK 报文段以后,就关闭连接;此时,主机
1 等待 2MSL 后依然没有收到回复,则证明 Server 端已正常关闭,那好,主机 1 也可以关闭
连接了。
“三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生
错误”。主要目的防止 server 端一直等待,浪费资源。换句话说,即是为了保证服务端能收
接受到客户端的信息并能做出正确的应答而进行前两次(第一次和第二次)握手,为了保证客
户端能够接收到服务端的信息并能做出正确的应答而进行后两次(第二次和第三次)握手。
“四次挥手”原因是因为 tcp 是全双工模式,接收到 FIN 时意味将没有数据再发来,但是还是
可以继续发送数据。
2、TCP 可靠传输原理实现(滑动窗口)。确认和重传:接收方收到报文后就会进行确认,发送方一段时间没有收到确认就会重传。
数据校验。
数据合理分片与排序,TCP 会对数据进行分片,接收方会缓存为按序到达的数据,重新排序
后再提交给应用层。
流程控制:当接收方来不及接收发送的数据时,则会提示发送方降低发送的速度,防止包丢
失。
拥塞控制:当网络发生拥塞时,减少数据的发送。
关于滑动窗口、流量控制、拥塞控制实现原理请点击这里
3、Tcp 和 Udp 的区别?
1、基于连接与无连接;
2、对系统资源的要求(TCP 较多,UDP 少);
3、UDP 程序结构较简单;
4、流模式与数据报模式 ;
5、TCP 保证数据正确性,UDP 可能丢包;
6、TCP 保证数据顺序,UDP 不保证。
4、如何设计在 UDP 上层保证 UDP 的可靠性传输?
传输层无法保证数据的可靠传输,只能通过应用层来实现了。实现的方式可以参照 tcp 可靠
性传输的方式。如不考虑拥塞处理,可靠 UDP 的简单设计如下:
1、添加 seq/ack 机制,确保数据发送到对端
2、添加发送和接收缓冲区,主要是用户超时重传。
3、添加超时重传机制。
具体过程即是:送端发送数据时,生成一个随机 seq=x,然后每一片按照数据大小分配 seq。
数据到达接收端后接收端放入缓存,并发送一个 ack=x 的包,表示对方已经收到了数据。
发送端收到了 ack 包后,删除缓冲区对应的数据。时间到后,定时任务检查是否需要重传数
据。
目前有如下开源程序利用 udp 实现了可靠的数据传输。分别为 RUDP、RTP、UDT:
1、RUDP(Reliable User Datagram Protocol)
RUDP 提供一组数据服务质量增强机制,如拥塞控制的改进、重发机制及淡化服务器算法等。2、RTP(Real Time Protocol)
RTP 为数据提供了具有实时特征的端对端传送服务,如在组播或单播网络服务下的交互式视
频音频或模拟数据。
3、UDT(UDP-based Data Transfer Protocol)
UDT 的主要目的是支持高速广域网上的海量数据传输。
关于 RUDP、RTP、UDT 的更多介绍请查看此处
三、其它重要网络概念 (⭐⭐)
1、socket 断线重连怎么实现,心跳机制又是怎样实现?
socket 概念
套接字(socket)是通信的基石,是支持 TCP/IP 协议的网络通信的基本操作单元。它是网
络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地
主机的 IP 地址,本地进程的协议端口,远地主机的 IP 地址,远地进程的协议端口。
为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与 TCP/IP 协议交互
提供了套接字(Socket)接口。应 用层可以和传输层通过 Socket 接口,区分来自不同应用程
序进程或网络连接的通信,实现数据传输的并发服务。
建立 socket 连接
建立 Socket 连接至少需要一对套接字,其中一个运行于客户端,称为 ClientSocket ,另一
个运行于服务器端,称为 ServerSocket 。
套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。
服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的
状态,实时监控网络状态,等待客户端的连接请求。
客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。
为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端- -
套接字的地址和端口号,然后就向服务器端套接字提出连接请求。 连接确认:当服
务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接
字的请求,建立一个新的线程,把服务器端套接字的描述发 给客户端,一旦客户端
确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续
接收其他客户端套接字的连接请求。
SOCKET 连接与 TCP 连接创建 Socket 连接时,可以指定使用的传输层协议,Socket 可以支持不同的传输层协议(TCP
或 UDP),当使用 TCP 协议进行连接时,该 Socket 连接就是一个 TCP 连接。
Socket 连接与 HTTP 连接
由于通常情况下 Socket 连接就是 TCP 连接,因此 Socket 连接一旦建立,通信双方即可开
始相互发送数据内容,直到双方连接断开。但在实际网 络应用中,客户端到服务器之间的
通信往往需要穿越多个中间节点,例如路由器、网关、防火墙等,大部分防火墙默认会关闭
长时间处于非活跃状态的连接而导致 Socket 连接断连,因此需要通过轮询告诉网络,该连
接处于活跃状态。
而 HTTP 连接使用的是“请求—响应”的方式,不仅在请求时需要先建立连接,而且需要客户
端向服务器发出请求后,服务器端才能回复数据。
很多情况下,需要服务器端主动向客户端推送数据,保持客户端与服务器数据的实时与同步。
此时若双方建立的是 Socket 连接,服务器就可以直接将数 据传送给客户端;若双方建立的
是 HTTP 连接,则服务器需要等到客户端发送一次请求后才能将数据传回给客户端,因此,
客户端定时向服务器端发送连接请求, 不仅可以保持在线,同时也是在“询问”服务器是否
有新的数据,如果有就将数据传给客户端。TCP(Transmission Control Protocol) 传输控制
协议
socket 断线重连实现
正常连接断开客户端会给服务端发送一个 fin 包,服务端收到 fin 包后才会知道连接断开。而
断网断电时客户端无法发送 fin 包给服务端,所以服务端没办法检测到客户端已经短线。 为
了缓解这个问题,服务端需要有个心跳逻辑,就是服务端检测到某个客户端多久没发送任何
数据过来就认为客户端已经断开, 这需要客户端定时向服务端发送心跳数据维持连接。
心跳机制实现
长连接的实现:心跳机制,应用层协议大多都有 HeartBeat 机制,通常是客户端每隔一小段
时间向服务器发送一个数据包,通知服务器自己仍然在线。并传输一些可能必要的数据。使
用心跳包的典型协议是 IM,比如 QQ/MSN/飞信等协议
1、在 TCP 的机制里面,本身是存在有心跳包的机制的,也就是 TCP 的选项:SO_KEEPALIVE。
系统默认是设置的 2 小时的心跳频率。但是它检查不到机器断电、网线拔出、防火墙这些断
线。 而且逻辑层处理断线可能也不是那么好处理。一般,如果只是用于保活还是可以的。
通过使用 TCP 的 KeepAlive 机制(修改那个 time 参数),可以让连接每隔一小段时间就产
生一些 ack 包,以降低被踢掉的风险,当然,这样的代价是额外的网络和 CPU 负担。
2、应用层心跳机制实现。
2、Cookie 与 Session 的作用和原理。 Session 是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存
在集群、数据库、文件中。
Cookie 是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现
Session 的一种方式。
Session:
由于 HTTP 协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来
识具体的用户,这个机制就是 Session.典型的场景比如购物车,当你点击下单按钮时,由于
HTTP 协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特
定的 Session,用用于标识这个用户,并且跟踪用户,这样才知道购物车里面有几本书。这
个 Session 是保存在服务端的,有一个唯一标识。在服务端保存 Session 的方法很多,内存、
数据库、文件都有。集群的时候也要考虑 Session 的转移,在大型的网站,一般会有专门的
Session 服务器集群,用来保存用户会话,这个时候 Session 信息都是放在内存的。
具体到 Web 中的 Session 指的就是用户在浏览某个网站时,从进入网站到浏览器关闭所经
过的这段时间,也就是用户浏览这个网站所花费的时间。因此从上述的定义中我们可以看到,
Session 实际上是一个特定的时间概念。
当客户端访问服务器时,服务器根据需求设置 Session,将会话信息保存在服务器上,同时
将标示 Session 的 SessionId 传递给客户端浏览器,
浏览器将这个 SessionId 保存在内存中,我们称之为无过期时间的 Cookie。浏览器关闭后,
这个 Cookie 就会被清掉,它不会存在于用户的 Cookie 临时文件。
以后浏览器每次请求都会额外加上这个参数值,服务器会根据这个 SessionId,就能取得客
户端的数据信息。
如果客户端浏览器意外关闭,服务器保存的 Session 数据不是立即释放,此时数据还会存在,
只要我们知道那个 SessionId,就可以继续通过请求获得此 Session 的信息,因为此时后台的
Session 还存在,当然我们可以设置一个 Session 超时时间,一旦超过规定时间没有客户端
请求时,服务器就会清除对应 SessionId 的 Session 信息。
Cookie
Cookie 是由服务器端生成,发送给 User-Agent(一般是 web 浏览器),浏览器会将 Cookie
的 key/value 保存到某个目录下的文本文件内,下次请求同一网站时就发送该 Cookie 给服
务器(前提是浏览器设置为启用 Cookie)。Cookie 名称和值可以由服务器端开发自己定义,
对于 JSP 而言也可以直接写入 Sessionid,这样服务器可以知道该用户是否合法用户以及是
否需要重新登录等。
3、IP 报文中的内容。版本:IP 协议的版本,目前的 IP 协议版本号为 4,下一代 IP 协议版本号为 6。
首部长度:IP 报头的长度。固定部分的长度(20 字节)和可变部分的长度之和。共占 4 位。
最大为 1111,即 10 进制的 15,代表 IP 报头的最大长度可以为 15 个 32bits(4 字节),也
就是最长可为 15*4=60 字节,除去固定部分的长度 20 字节,可变部分的长度最大为 40 字
节。
服务类型:Type Of Service。
总长度:IP 报文的总长度。报头的长度和数据部分的长度之和。
标识:唯一的标识主机发送的每一分数据报。通常每发送一个报文,它的值加一。当 IP 报
文长度超过传输网络的 MTU(最大传输单元)时必须分片,这个标识字段的值被复制到所
有数据分片的标识字段中,使得这些分片在达到最终目的地时可以依照标识字段的内容重新
组成原先的数据。
标志:共 3 位。R、DF、MF 三位。目前只有后两位有效,DF 位:为 1 表示不分片,为 0
表示分片。MF:为 1 表示“更多的片”,为 0 表示这是最后一片。
片位移:本分片在原先数据报文中相对首位的偏移位。(需要再乘以 8)
生存时间:IP 报文所允许通过的路由器的最大数量。每经过一个路由器,TTL 减 1,当为 0
时,路由器将该数据报丢弃。TTL 字段是由发送端初始设置一个 8 bit 字段.推荐的初始值由
分配数字 RFC 指定,当前值为 64。发送 ICMP 回显应答时经常把 TTL 设为最大值 255。协议:指出 IP 报文携带的数据使用的是那种协议,以便目的主机的 IP 层能知道要将数据报
上交到哪个进程(不同的协议有专门不同的进程处理)。和端口号类似,此处采用协议号,
TCP 的协议号为 6,UDP 的协议号为 17。ICMP 的协议号为 1,IGMP 的协议号为 2.
首部校验和:计算 IP 头部的校验和,检查 IP 报头的完整性。
源 IP 地址:标识 IP 数据报的源端设备。
目的 IP 地址:标识 IP 数据报的目的地址。
最后就是可变部分和数据部分。
四、常见网络流程机制 (⭐⭐)
1、浏览器输入地址到返回结果发生了什么?
总体来说分为以下几个过程:
1、DNS 解析,此外还有 DNSy 优化(DNS 缓存、DNS 负载均衡)
2、TCP 连接
3、发送 HTTP 请求
4、服务器处理请求并返回 HTTP 报文
5、浏览器解析渲染页面
6、连接结束
Web 前端的本质
将信息快速并友好的展示给用户并能够与用户进行交互。
如何尽快的加载资源(网络优化)?
答案就是能不从网络中加载的资源就不从网络中加载,当我们合理使用缓存,将资源放在浏
览器端,这是最快的方式。如果资源必须从网络中加载,则要考虑缩短连接时间,即 DNS
优化部分;减少响应内容大小,即对内容进行压缩。另一方面,如果加载的资源数比较少的
话,也可以快速的响应用户。
第二节、操作系统面试题 (⭐⭐⭐)
1、操作系统如何管理内存的?2、进程调度。
3、说下 Linux 进程和线程的区别。
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的
地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只
是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有
单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多
线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些
要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
简而言之,一个程序至少有一个进程,一个进程至少有一个线程。
线程的划分尺度小于进程,使得多线程程序的并发性高。
另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从
而极大地提高了程序的运行效率。
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行
的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依
存在应用程序中,由应用程序提供多个线程执行控制。
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可
以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现
进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
4、你能解释一下 Linux 的软链接和硬链接吗?
Linux 链接分两种,一种被称为硬链接(Hard Link),另一种被称为符号链接
(Symbolic Link)。默认情况下,ln 命令产生硬链接。
硬连接
硬连接指通过索引节点来进行连接。在 Linux 的文件系统中,保存在磁盘分区中
的文件不管是什么类型都给它分配一个编号,称为索引节点号(Inode Index)。在
Linux 中,多个文件名指向同一索引节点是存在的。一般这种连接就是硬连接。
硬连接的作用是允许一个文件拥有多个有效路径名,这样用户就可以建立硬连接到重要文件,以防止“误删”的功能。其原因如上所述,因为对应该目录的索引节
点有一个以上的连接。只删除一个连接并不影响索引节点本身和其它的连接,只
有当最后一个连接被删除后,文件的数据块及目录的连接才会被释放。也就是说,
文件真正删除的条件是与之相关的所有硬连接文件均被删除。
软连接
另外一种连接称之为符号连接(Symbolic Link),也叫软连接。软链接文件有类
似于 Windows 的快捷方式。它实际上是一个特殊的文件。在符号连接中,文件
实际上是一个文本文件,其中包含的有另一文件的位置信息。
5、安卓权限管理,为何在清单中注册权限,安卓 APP 就可以使用,反之不可
以?
此题考查 Android 的权限管理在 Android 的安全架构中的作用。
Android 是一个权限分隔的操作系统,其中每个应用都有其独特的系统标识
(Linux 用户 ID 和组 ID)。系统各部分也分隔为不同的标识。Linux 据此将不
同的应用以及应用与系统分隔开来。
其他更详细的安全功能通过“权限”机制提供,此机制会限制特定进程可以执行的
具体操作,并且根据 URI 权限授权临时访问特定的数据段。
Android 安全架构的中心设计点是:在默认情况下任何应用都没有权限执行对其
他应用、操作系统或用户有不利影响的任何操作。这包括读取或写入用户的私有
数据(例如联系人或电子邮件)、读取或写入其他应用程序的文件、执行网络访
问、使设备保持唤醒状态等。
由于每个 Android 应用都是在进程沙盒中运行,因此应用必须显式共享资源和
数据。它们的方法是声明需要哪些权限来获取基本沙盒未提供的额外功能。应用
以静态方式声明它们需要的权限,然后 Android 系统提示用户同意。
第三节、数据库面试题 (⭐)1、数据库的四大特征,数据库的隔离级别?
事务(Transaction)是并发控制的基本单位。所谓的事务,它是一个操作序列,
这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。例如,银
行转账工作:从一个账号扣款并使另一个账号增款,这两个操作要么都执行,要
么都不执行。所以,应该把它们看成一个事务。事务是数据库维护数据一致性的
单位,在每个事务结束时,都能保持数据一致性。事务具有以下 4 个基本特征:
数据库的四大特征:
(1)原子性(Atomicity)
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚。
(2)一致性(Consistency)
一个事务执行之前和执行之后都必须处于一致性状态。
(3)隔离性(Isolation)
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个
用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
(4)持久性(Durability)
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性
的。
数据库的隔离级别:
1)Serializable(串行化):可避免脏读、不可重复读、幻读的发生。
2)Repeatable read (可重复读):可避免脏读、不可重复读的发生。
3)Read committed (读已提交):可避免脏读的发生。4)Read uncommitted (读未提交):最低级别,任何情况都无法保证。
2、数据库设计中常讲的三范式是指什么?
1)第一范式 1NF(域的原子性)
如果数据库表中的所有字段值都是不可分解的原子值,就说明该数据库表满足了
第一范式
2)第二范式 2NF(表中除主键外的字段都完全依赖主键)
第二范式是在第一范式基础上建立的。第二范式有两个重点:(1)表中必须有主键;
(2)其他非主属性必须完全依赖主键,不能只依赖主键的一部分(主要针对联合
主键而言)。
3)第三范式 3NF(表中除主键外的字段都完全直接依赖,不能是传递依赖)
不能是传递依赖,即不能存在:非主键列 A 依赖于非主键列 B,非主键列 B 依
赖于主键的情况。第二范式和第三范式区分的关键点:2NF:非主键列是否完全
依赖于主键,还是依赖于主键的一部分;3NF:非主键列是直接依赖于主键,还
是直接依赖于非主键列。
第二章 数据结构和算法面试题
数据结构与算法
对于算法面试准备,无疑就是刷《剑指 Offer》+ LeetCode 效果最佳。刷《剑
指 Offer》是为了建立全面的算法面试思维,打下坚实的基础,刷 LeetCode 则
是为了不断强化与开阔我们自己的算法思想。这两块 CS-Notes 中已经实现地很完美了,建议大家将《剑指 Offer》刷完,然后再至少刷 100 道 LeetCode 题目
以上。
1、剑指 Offer 题解
2、Leetcode 题解
建议刷完《剑指 Offer》+ 至少 100 道 LeetCode 题目后,再去进
一步看看下面的高频题目有哪些没有刷到。
一、高频题集 (⭐⭐⭐)
1、无重复字符的最长子串
2、简化路径
3、复原 IP 地址
4、三数之和
5、岛屿的最大面积
6、搜索旋转排序数组
7、朋友圈
8、接雨水
9、反转链表
10、两数相加
11、合并两个有序链表
12、合并 K 个排序链表
13、买卖股票的最佳时机
14、买卖股票的最佳时机 II
15、最大子序和16、最小栈
17、LRU 缓存机制
18、寻找两个有序数组的中位数
19、最长回文子串
20、合并两个有序数组
21、整数反转
22、排序链表
23、子集
24、全排列
25、实现二叉树中序遍历(不使用递归)
26、爬楼梯(斐波那契数列)
27、滑动窗口的最大值
28、判断单链表成环与否?
29、如何从一百万个数里面找到最小的一百个数,考虑算法的时间复杂度和空间复杂度
30、手写数组实现队列
31、java 排序算法和查找算法 (写出你所知道的排序算法及时空复杂度,稳定性)
http://www.jianshu.com/p/8c915179fd02
http://xiaojun-it.iteye.com/blog/2291852
二、次高频题集 (⭐⭐)
1、算法熟悉么?给了一个二叉排序树,出了一个给定节点找到它的下一个元素(指的是大小顺
序的下一个)的算法题。
2、x 个苹果,一天只能吃一个、两个、或者三个,问多少天可以吃完
3、求二叉树第 n 层节点数4、如何设计一个抽奖系统,比如满 200 抽 20,满 500 抽 50。
5、求无序数组中的中位数
6、二叉树深度算法
7、堆和栈在内存中的区别是什么(数据结构方面以及实际实现方面)
8、最快的排序算法是哪个?给阿里 2 万多名员工按年龄排序应该选择哪个算法?
9、堆和树的区别?
10、求 1000 以内的水仙花数以及 40 亿以内的水仙花数;
11、子串包含问题(KMP 算法)写代码实现;
12、万亿级别的两个 URL 文件 A 和 B,如何求出 A 和 B 的差集 C,(Bit 映射->hash 分组->多文件
读写效率->磁盘寻址以及应用层面对寻址的优化)
13、蚁群算法与蒙特卡洛算法;
14、百度 POI 中如何试下查找最近的商家功能(坐标镜像+R 树)。
15、5 枚硬币,2 正 3 反如何划分为两堆然后通过翻转让两堆中正面向上的硬币和反面向上的硬
币个数相同;
16、时针走一圈,时针分针重合几次;
17、N * N 的方格纸,里面有多少个正方形;
18、请在 100 个电话号码找出 135 的电话号码 注意 不能用正则,(类似怎么最好的遍历 LogGat
日志)
19、一个青蛙跳台阶,一次可以跳一步和两步,如果一共有 N 个台阶,可以有几种跳法?
20、写代码实现队列的基本操作,外加查找最大值;
21、图:有向无环图的解释
22、二叉树 深度遍历与广度遍历
23、B 树、B+树
24、密码学中两大加密算法是什么
25、判断环(猜测应该是链表环)26、有一个 List 列表,去掉列表中的某一 Object 对象,如何在 for 循环里面写;
27、设计移动端的联系人存储与查询的功能,要求快速搜索联系人,可以用到哪些数据结构?
(二叉排序树,建立索引)
28、一道简单不易的算法题
int a = 10;
int b=5;
怎么在不引入其他变量的情况下,让 a 和 b 互换?
```
public class Test {
int a = 10;
int b=5;
public static void main(String[] args) {
a = a+b;
b=a-b;
a =a-b;
System.out.println("b="+b);
System.out.println("a="+a);
}
}
----输出:
b=10
a=5
```
29、回形打印二维数组30、二叉树,给出根节点和目标节点,找出从根节点到目标节点的路径
31、一个无序,不重复数组,输出 N 个元素,使得 N 个元素的和相加为 M,给出时间复杂度、空
间复杂度。手写算法
32、两个不重复的数组集合中,求共同的元素。
33、上一问扩展,海量数据,内存中放不下,怎么求出。
34、从长度为 m 的 int 数组中随机取出 n 个元素,每次取的元素都是之前未取过的,如何优化
35、逆序一个字符串,不能调用 String 的 reverse 方法(考察编码风格)
36、算法:将一个有序数组去重得到一个新数组(空间复杂度为 O(N))
37、算法:如何从 1T 的无序数组(长度为 n)里面找出前 k 大的数据,复杂度要求为 O(logN)
38、m * n 的矩阵,能形成几个正方形(2 * 2 能形成 1 个正方形,2 * 3 2 个,3 * 3 6 个)
计数的关键是要观察到任意一个倾斜的正方形必然唯一内接于一个非倾斜的正方形,而一个非倾
斜的边长为 k 的非倾斜正方形,一条边上有 k-1 个内点,每个内点恰好确定一个内接于其中的
倾斜正方形,加上非倾斜正方形本身,可知,将边长为 k 的非倾斜正方形数目乘以 k,再按 k 求
和即可得到所有正方形的数目。
设 2≤n≤m,k≤n-1,则边长为 k 的非倾斜有
(n-k)(m-k)个,故所有正方形有
∑(m-k)(n-k)k 个
例如 m=n=4
正方形有 331+222+113=20 个。
39、面试头条的时候在线编程:从上到下从左到右输出二叉树40、从长度为 m 的 int 数组中随机取出 n 个元素,每次取的元素都是之前未取过的,如何优化
41、逆序一个字符串,不能调用 String 的 reverse 方法(考察编码风格)
42、算法:将一个有序数组去重得到一个新数组(空间复杂度为 O(N))
43、算法:如何从 1T 的无序数组(长度为 n)里面找出前 k 大的数据,复杂度要求为 O(logN)
44、堆和树的区别
45、堆和栈在内存中的区别是什么(解答提示:可以从数据结构方面以及实际实现方面两个方面
去回答)?
46、什么是深拷贝和浅拷贝
47、手写链表逆序代码
48、讲一下对图的理解
49、手写一段代码,如何找出一段字符串中,出现最多的汉字是哪个?
50、单向链表逆序。
51、实现一个数组插入。(处理异常判别,不使用 Collections 相关接口)。
52、找到无序数组的最大连续求和。
53、找到多个员工的共同繁忙时段。
https://github.com/banking/algorithm-dish/blob/master/algorithm-question/
src/main/java/TimeAirbnb.java
54、输出一个集合{A,B,C,D}的全部子集**
55、手写代码:遍历文件目录;
56、电梯运行的算法分析;
57、手写实现单链表的 get 操作;
58、100 个数字排序怎么做?
59、一个集合里有 1000 万个随机元素,如何快速计算他们的和(我特喵的以为是考算法,想半
天没有 O(n)以下的方案,结果他居然说多线程)60、切饼问题:1 刀切 2 块,2 刀切 4 块,10 刀最多切几块。
61、追击问题:2 辆火车相向同时出发,一个从 A 点出发,速度是 30km/h,一个从 B 点出发,速
度是 20km/h,A、B 之间的距离是 S,同时一只鸟也从 A 点出发,速度是 50km/h,鸟在两辆火车
间折返飞行,问三者相遇时,鸟飞的总路程。
62、算法:给定一段已排好序的数列,数列中元素有可能是重复的,找出数列中目标值的最小
索引,要求不能用递归,只能用循环。
63、一个文件中有 100 万个整数,由空格分开,在程序中判断用户输入的整数是否在此文件中。
说出最优的方法
64、2000 万个整数,找出第五十大的数字?
65、烧一根不均匀的绳,从头烧到尾总共需要 1 个小时。现在有若干条材质相同的绳子,问如
何用烧绳的方法来计时一个小时十五分钟呢?
66、求 1000 以内的水仙花数以及 40 亿以内的水仙花数
67、称重问题:10 个箱子,每个箱子里都有若干砖头,其中有一个箱子里每个砖头重 9 克,其
他的箱子里的砖头每个重 10 克,给你一个秤,要求只能用一次称重找出砖头重 9 克的箱子。
68、时针走一圈,时针分针重合几次
69、N*N 的方格纸,里面有多少个正方形
70、标号 1-n 的 n 个人首尾相接,1 到 3 报数,报到 3 的退出,求最后一个人的标号
71、给定一个字符串,求第一个不重复的字符 abbcad -> c
72、上网站写代码,如下: 有一个容器类 ArrayList,保存整数类型的元素,现在要求编写一
个帮助类,类内提供一个帮助函数,帮助函数的功能是删除 容器中<10 的元素。
73、写代码,LeetCode 上股票利益最大化问题
74、写代码,剑指 offer 上第一次只出现一次的字符
75、算法:连续子数组的最大和
76、子串包含问题(KMP 算法)写代码实现
77、ViewGroup 的层级深度,转换为二叉树的层级深度
78、String 字符串的数字相加
79、使用三个线程顺序打印有序的数组80、给定一个有序的数组和目标数,找出与目标数最近接的 index,要求复杂度是 log(n)的时
间复杂度
81、给定一个二叉树和一个目标数,在二叉树中是否存在一条路径的所有节点的和与目标数是
相同的 case,并且打印。
82、二叉树,读取每一层最右边的节点
83、int 数组,除了一个数字外,其他数字都出现两次,找出这个只出现一次的数字
84、一个类,内部有一个链表的数据结构,实现 void add(Node n)和 void remove(int index)
的函数
85、手写消费者生产者模型的代码
86、一个无序的 int 数组,给一个 target 数字,找出数组中两个数字相加为 target,并输出坐
标
87、数组中存有 1-3 的三种数字,例如[1,2,3,1,2,2,1,3,3],将其排序为[1,1,1,2,2,2,3,3,3],
要求时间复杂度,后续将内容变为一个对象,继续排序
88、"之"字形打印二叉树
89、1~100 盏灯,都是亮的,第一次将能被 1 整除的数的灯按下,变暗,第二次将能被 2 整除的
数的等按下,变亮,第三次将能被 3 整除的数的等按下,变暗…第 100 次将能被 100 整除的数
的灯按下,问,最后有多少盏灯是亮的。
90、实现一个 o(n)复杂度的堆和最大数。
91、实现一个数组的窗口扫描算法。
92、识别一个字符串是否是 ipv4 地址。
93、o(n)复杂度实现偶数递增奇数递减单向链接排序。
第三章 Java 面试题
第一节 Java 基础面试题
一、面向对象 (⭐⭐⭐)
1、谈谈对 java 多态的理解?多态是指父类的某个方法被子类重写时,可以产生自己的功能行为,同一个操作
作用于不同对象,可以有不同的解释,产生不同的执行结果。
多态的三个必要条件:
继承父类。
重写父类的方法。
父类的引用指向子类对象。
什么是多态
面向对象的三大特性:封装、继承、多态。从一定角度来看,封装和继承几乎都
是为多态而准备的。这是我们最后一个概念,也是最重要的知识点。
多态的定义:指允许不同类的对象对同一消息做出响应。即同一消息可以根据发
送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)
实现多态的技术称为:动态绑定(dynamic binding),是指在执行期间判断所
引用对象的实际类型,根据其实际的类型调用其相应的方法。
多态的作用:消除类型之间的耦合关系。
现实中,关于多态的例子不胜枚举。比方说按下 F1 键这个动作,如果当前在
Flash 界面下弹出的就是 AS 3 的帮助文档;如果当前在 Word 下弹出的就是
Word 帮助;在 Windows 下弹出的就是 Windows 帮助和支持。同一个事件发
生在不同的对象上会产生不同的结果。
多态的好处:
1.可替换性(substitutability)。多态对已存在代码具有可替换性。例如,多态
对圆 Circle 类工作,对其他任何圆形几何体,如圆环,也同样工作。
2.可扩充性(extensibility)。多态对代码具有可扩充性。增加新的子类不影响已
存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易
获得多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态基础上,很容易
增添球体类的多态性。3.接口性(interface-ability)。多态是超类通过方法签名,向子类提供了一个共
同接口,由子类来完善或者覆盖它而实现的。
4.灵活性(flexibility)。它在应用中体现了灵活多样的操作,提高了使用效率。
5.简化性(simplicity)。多态简化对应用软件的代码编写和修改过程,尤其在处
理大量对象的运算和操作时,这个特点尤为突出和重要。
Java 中多态的实现方式:接口实现,继承父类进行方法重写,同一个类中进行方
法重载。
2、你所知道的设计模式有哪些?
答:Java 中一般认为有 23 种设计模式,我们不需要所有的都会,但是其中常用
的种设计模式应该去掌握。下面列出了所有的设计模式。要掌握的设计模式我单
独列出来了,当然能掌握的越多越好。
总体来说设计模式分为三大类:
创建型模式,共五种:
工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:
适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模
式。
行为型模式,共十一种:
策略模式、模板方法模式、观者模式、迭代子模式、责任链模式、命令模式、备
忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
具体可见我的设计模式总结笔记
3、通过静态内部类实现单例模式有哪些优点?
1. 不用 synchronized ,节省时间。
2. 调用 getInstance() 的时候才会创建对象,不调用不创建,节省空间,这
有点像传说中的懒汉式。4、静态代理和动态代理的区别,什么场景使用?
静态代理与动态代理的区别在于代理类生成的时间不同,即根据程序运行前代理
类是否已经存在,可以将代理分为静态代理和动态代理。如果需要对多个类进行
代理,并且代理的功能都是一样的,用静态代理重复编写代理类就非常的麻烦,
可以用动态代理动态的生成代理类。
// 为目标对象生成代理对象
public Object getProxyInstance() {
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("开启事务");
// 执行目标对象方法
Object returnValue = method.invoke(target, args);
System.out.println("提交事务");
return null;
}
});
}
静态代理使用场景:四大组件同 AIDL 与 AMS 进行跨进程通信
动态代理使用场景:Retrofit 使用了动态代理极大地提升了扩展性和可维
护性。5、简单工厂、工厂方法、抽象工厂、Builder 模式的区别?
简单工厂模式:一个工厂方法创建不同类型的对象。
工厂方法模式:一个具体的工厂类负责创建一个具体对象类型。
抽象工厂模式:一个具体的工厂类负责创建一系列相关的对象。
Builder 模式:对象的构建与表示分离,它更注重对象的创建过程。
6、装饰模式和代理模式有哪些区别 ?与桥接模式相比呢?
1、装饰模式是以客户端透明的方式扩展对象的功能,是继承关系的一个
替代方案;而代理模式则是给一个对象提供一个代理对象,并由代理对象
来控制对原有对象的引用。
2、装饰模式应该为所装饰的对象增强功能;代理模式对代理的对象施加
控制,但不对对象本身的功能进行增加。
3、桥接模式的作用于代理、装饰截然不同,它主要是为了应对某个类族
有多个变化维度导致子类类型急剧增多的场景。通过桥接模式将多个变化
维度隔离开,使得它们可以独立地变化,最后通过组合使它们应对多维变
化,减少子类的数量和复杂度。
7、外观模式和中介模式的区别?
外观模式重点是对外封装统一的高层接口,便于用户使用;而中介模式则是避免
多个互相协作的对象直接引用,它们之间的交互通过一个中介对象进行,从而使
得它们耦合松散,能够易于应对变化。
8、策略模式和状态模式的区别?
虽然两者的类型结构是一致的,但是它们的本质却是不一样的。策略模式重在整
个算法的替换,也就是策略的替换,而状态模式则是通过状态来改变行为。
9、适配器模式,装饰者模式,外观模式的异同?
这三个模式的相同之处是,它们都作用于用户与真实被使用的类或系统之间,作
一个中间层,起到了让用户间接地调用真实的类的作用。它们的不同之外在于,
如上所述的应用场合不同和本质的思想不同。
代理与外观的主要区别在于,代理对象代表一个单一对象,而外观对象代表一个
子系统,代理的客户对象无法直接访问对象,由代理提供单独的目标对象的访问,
而通常外观对象提供对子系统各元件功能的简化的共同层次的调用接口。代理是
一种原来对象的代表,其它需要与这个对象打交道的操作都是和这个代表交涉的。而适配器则不需要虚构出一个代表者,只需要为应付特定使用目的,将原来
的类进行一些组合。
外观与适配器都是对现存系统的封装。外观定义的新的接口,而适配器则是复用
一个原有的接口,适配器是使两个已有的接口协同工作,而外观则是为现存系统
提供一个更为方便的访问接口。如果硬要说外观是适配,那么适配器有用来适配
对象的,而外观是用来适配整个子系统的。也就是说,外观所针对的对象的粒度
更大。
代理模式提供与真实的类一致的接口,意在用代理类来处理真实的类,实现一些
特定的服务或真实类的部分功能,Facade(外观)模式注重简化接口,Adapter
(适配器)模式注重转换接口。
10、代码的坏味道:
1、代码重复:
代码重复几乎是最常见的异味了。他也是 Refactoring 的主要目标之一。代码重
复往往来自于 copy-and-paste 的编程风格。
2、方法过长:
一个方法应当具有自我独立的意图,不要把几个意图放在一起。
3、类提供的功能太多:
把太多的责任交给了一个类,一个类应该仅提供一个单一的功能。
4、数据泥团:
某些数据通常像孩子一样成群玩耍:一起出现在很多类的成员变量中,一起出现
在许多方法的参数中…..,这些数据或许应该自己独立形成对象。 比如以单例的
形式对外提供自己的实例。
5、冗赘类:
一个干活不多的类。类的维护需要额外的开销,如果一个类承担了太少的责任,
应当消除它。6、需要太多注释:
经常觉得要写很多注释表示你的代码难以理解。如果这种感觉太多,表示你需要
Refactoring。
11、是否能从 Android 中举几个例子说说用到了什么设计模式 ?
AlertDialog、Notification 源码中使用了 Bulider(建造者)模式完成参数的初始化:
在 AlertDialog 的 Builder 模式中并没有看到 Direcotr 角色的出现,其实在很多场
景中,Android 并没有完全按照 GOF 的经典设计模式来实现,而是做了一些修
改,使得这个模式更易于使用。这个的 AlertDialog.Builder 同时扮演了上下文中
提到的 builder、ConcreteBuilder、Director 的角色,简化了 Builder 模式的设计。
当模块比较稳定,不存在一些变化时,可以在经典模式实现的基础上做出一些精
简,而不是照搬 GOF 上的经典实现,更不要生搬硬套,使程序失去架构之美。
定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不
同的表示。即将配置从目标类中隔离出来,避免过多的 setter 方法。
优点:
1、良好的封装性,使用建造者模式可以使客户端不必知道产品内部组成
的细节。
2、建造者独立,容易扩展。
缺点:
会产生多余的 Builder 对象以及 Director 对象,消耗内存。
日常开发的 BaseActivity 抽象工厂模式:
定义:为创建一组相关或者是相互依赖的对象提供一个接口,而不需要指定它们
的具体类。
主题切换的应用:比如我们的应用中有两套主题,分别为亮色主题 LightTheme 和暗色主题
DarkTheme,这两种主题我们可以通过一个抽象的类或接口来定义,而在对应主
题下我们又有各类不同的 UI 元素,比如 Button、TextView、Dialog、ActionBar
等,这些 UI 元素都会分别对应不同的主题,这些 UI 元素我们也可以通过抽象的
类或接口定义,抽象的主题、具体的主题、抽象的 UI 元素和具体的 UI 元素之间
的关系就是抽象工厂模式最好的体现。
优点:
分离接口与实现,面向接口编程,使其从具体的产品实现中解耦,同时基
于接口与实现的分离,使抽象该工厂方法模式在切换产品类时更加灵活、
容易。
缺点:
类文件的爆炸性增加。
新的产品类不易扩展。
Okhttp 内部使用了责任链模式来完成每个 Interceptor 拦截器的调用:
定义:使多个对象都有机会处理请求,从而避免了请求的发送者和接收者之间的
耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理
它为止。
ViewGroup 事件传递的递归调用就类似一条责任链,一旦其寻找到责任者,那
么将由责任者持有并消费掉该次事件,具体体现在 View 的 onTouchEvent 方法
中返回值的设置,如果 onTouchEvent 返回 false,那么意味着当前 View 不会是
该次事件的责任人,将不会对其持有;如果为 true 则相反,此时 View 会持有该
事件并不再向下传递。
优点:
将请求者和处理者关系解耦,提供代码的灵活性。
缺点:对链中请求处理者的遍历中,如果处理者太多,那么遍历必定会影响性能,特别
是在一些递归调用中,要慎重。
RxJava 的观察者模式:
定义:定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有
依赖于它的对象都会得到通知并被自动更新。
ListView/RecyclerView 的 Adapter 的 notifyDataSetChanged 方法、广播、事件
总线机制。
观察者模式主要的作用就是对象解耦,将观察者与被观察者完全隔离,只依赖于
Observer 和 Observable 抽象。
优点:
观察者和被观察者之间是抽象耦合,应对业务变化。
增强系统灵活性、可扩展性。
缺点:
在 Java 中消息的通知默认是顺序执行,一个观察者卡顿,会影响整体的
执行效率,在这种情况下,一般考虑采用异步的方式。
AIDL 代理模式:
定义:为其他对象提供一种代理以控制对这个对象的访问。
静态代理:代码运行前代理类的 class 编译文件就已经存在。
动态代理:通过反射动态地生成代理者的对象。代理谁将会在执行阶段决定。将
原来代理类所做的工作由 InvocationHandler 来处理。
使用场景:
当无法或不想直接访问某个对象或访问某个对象存在困难时可以通过一
个代理对象来间接访问,为了保证客户端使用的透明性,委托对象与代理
对象需要实现相同的接口。
缺点:
对类的增加。
ListView/RecyclerView/GridView 的适配器模式:
适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接
口不匹配而无法在一起工作的两个类能够在一起工作。
使用场景:
接口不兼容。
想要建立一个可以重复使用的类。
需要一个统一的输出接口,而输入端的类型不可预知。
优点:
更好的复用性:复用现有的功能。
更好的扩展性:扩展现有的功能。
缺点:
过多地使用适配器,会让系统非常零乱,不易于整体把握。例如,明明看
到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果
出现太多这种情况,无异于一场灾难。
Context/ContextImpl 外观模式:
要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行,门面模式
提供一个高层次的接口,使得子系统更易于使用。
使用场景:
为一个复杂子系统提供一个简单接口。
优点:
对客户程序隐藏子系统细节,因而减少了客户对于子系统的耦合,能够拥
抱变化。
外观类对子系统的接口封装,使得系统更易用使用。
缺点:
外观类接口膨胀。
外观类没有遵循开闭原则,当业务出现变更时,可能需要直接修改外观类。二、集合框架 (⭐⭐⭐)
1、集合框架,list,map,set 都有哪些具体的实现类,区别都是什么?
Java 集合里使用接口来定义功能,是一套完善的继承体系。Iterator 是所有集合
的总接口,其他所有接口都继承于它,该接口定义了集合的遍历操作,Collection
接口继承于 Iterator,是集合的次级接口(Map 独立存在),定义了集合的一些
通用操作。
Java 集合的类结构图如下所示:
List:有序、可重复;索引查询速度快;插入、删除伴随数据移动,速度慢;
Set:无序,不可重复;
Map:键值对,键唯一,值多个;
1.List,Set 都是继承自 Collection 接口,Map 则不是;
2.List 特点:元素有放入顺序,元素可重复;
Set 特点:元素无放入顺序,元素不可重复,重复元素会盖掉,(注意:元素虽
然无放入顺序,但是元素在 set 中位置是由该元素的 HashCode 决定的,其位置
其实是固定,加入 Set 的 Object 必须定义 equals()方法;另外 list 支持 for 循环,也就是通过下标来遍历,也可以使用迭代器,但是 set
只能用迭代,因为他无序,无法用下标取得想要的值)。
3.Set 和 List 对比:
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改
变。
List:和数组类似,List 可以动态增长,查找元素效率高,插入删除元素效率低,
因为会引起其他元素位置改变。
4.Map 适合储存键值对的数据。
5.线程安全集合类与非线程安全集合类
LinkedList、ArrayList、HashSet 是非线程安全的,Vector 是线程安全的;
HashMap 是非线程安全的,HashTable 是线程安全的;
StringBuilder 是非线程安全的,StringBuffer 是线程安的。
下面是这些类具体的使用介绍:
ArrayList 与 LinkedList 的区别和适用场景
Arraylist:
优点:ArrayList 是实现了基于动态数组的数据结构,因地址连续,一旦数据存储
好了,查询操作效率会比较高(在内存里是连着放的)。
缺点:因为地址连续,ArrayList 要移动数据,所以插入和删除操作效率比较低。
LinkedList:优点:LinkedList 基于链表的数据结构,地址是任意的,其在开辟内存空间的时
候不需要等一个连续的地址,对新增和删除操作 add 和 remove,LinedList 比较
占优势。LikedList 适用于要头尾操作或插入指定位置的场景。
缺点:因为 LinkedList 要移动指针,所以查询操作性能比较低。
适用场景分析:
当需要对数据进行对此访问的情况下选用 ArrayList,当要对数据进行多次增加删
除修改时采用 LinkedList。
ArrayList 和 LinkedList 怎么动态扩容的吗?
ArrayList:
ArrayList 初始化大小是 10 (如果你知道你的 arrayList 会达到多少容量,可以
在初始化的时候就指定,能节省扩容的性能开支) 扩容点规则是,新增的时候
发现容量不够用了,就去扩容 扩容大小规则是,扩容后的大小= 原始大小+原
始大小/2 + 1。(例如:原始大小是 10 ,扩容后的大小就是 10 + 5+1 = 16)
LinkedList:
linkedList 是一个双向链表,没有初始化大小,也没有扩容的机制,就是一直在
前面或者后面新增就好。
ArrayList 与 Vector 的区别和适用场景
ArrayList 有三个构造方法:
public ArrayList(intinitialCapacity)// 构造一个具有指定初始容量的空列表。
public ArrayList()// 构造一个初始容量为 10 的空列表。public ArrayList(Collection<? extends E> c)// 构造一个包含指定 collection 的元
素的列表
Vector 有四个构造方法:
public Vector() // 使用指定的初始容量和等于零的容量增量构造一个空向量。
public Vector(int initialCapacity) // 构造一个空向量,使其内部数据数组的大小,其
标准容量增量为零。
public Vector(Collection<? extends E> c)// 构造一个包含指定 collection 中的元
素的向量
public Vector(int initialCapacity, int capacityIncrement)// 使用指定的初始容量
和容量增量构造一个空的向量
ArrayList 和 Vector 都是用数组实现的,主要有这么四个区别:
1)Vector 是多线程安全的,线程安全就是说多线程访问代码,不会产生不确定的
结果。而 ArrayList 不是,这可以从源码中看出,Vector 类中的方法很多有
synchronied 进行修饰,这样就导致了 Vector 在效率上无法与 ArrayLst 相比;
2)两个都是采用的线性连续空间存储元素,但是当空间充足的时候,两个类的增
加方式是不同。
3)Vector 可以设置增长因子,而 ArrayList 不可以。
4)Vector 是一种老的动态数组,是线程同步的,效率很低,一般不赞成使用。
适用场景:
1.Vector 是线程同步的,所以它也是线程安全的,而 ArraList 是线程异步的,是
不安全的。如果不考虑到线程的安全因素,一般用 ArrayList 效率比较高。
2.如果集合中的元素的数目大于目前集合数组的长度时,在集合中使用数据量比
较大的数据,用 Vector 有一定的优势。HashSet 与 TreeSet 的区别和适用场景
1.TreeSet 是二叉树(红黑树的树据结构)实现的,Treest 中的数据是自动排好
序的,不允许放入 null 值。
2.HashSet 是哈希表实现的,HashSet 中的数据是无序的可以放入 null,但只能
放入一个 null,两者中的值都不重复,就如数据库中唯一约束。
3.HashSet 要求放入的对象必须实现 HashCode()方法,放的对象,是以 hashcode
码作为标识的,而具有相同内容的 String 对象,hashcode 是一样,所以放入的
内容不能重复但是同一个类的对象可以放入不同的实例。
适用场景分析:
HashSet 是基于 Hash 算法实现的,其性能通常都优于 TreeSet。为快速查找而
设计的 Set,我们通常都应该使用 HashSet,在我们需要排序的功能时,我们才
使用 TreeSet。
HashMap 与 TreeMap、HashTable 的区别及适用场景
HashMap 非线程安全
HashMap:基于哈希表(散列表)实现。使用 HashMap 要求的键类明确定义了
hashCode()和 equals()[可以重写 hasCode()和 equals()],为了优化 HashMap 空
间的使用,您可以调优初始容量和负载因子。其中散列表的冲突处理主分两种,
一种是开放定址法,另一种是链表法。HashMap 实现中采用的是链表法。
TreeMap:非线程安全基于红黑树实现。TreeMap 没有调优选项,因为该树总处
于平衡状态。
适用场景分析:HashMap 和 HashTable:HashMap 去掉了 HashTable 的 contain 方法,但是加上
了 containsValue()和 containsKey()方法。HashTable 是同步的,而 HashMap 是
非同步的,效率上比 HashTable 要高。HashMap 允许空键值,而 HashTable 不
允许。
HashMap:适用于 Map 中插入、删除和定位元素。
Treemap:适用于按自然顺序或自定义顺序遍历键(key)。 (ps:其实我们工作的过
程中对集合的使用是很频繁的,稍注意并总结积累一下,在面试的时候应该会回答
的很轻松)
2、set 集合从原理上如何保证不重复?
1)在往 set 中添加元素时,如果指定元素不存在,则添加成功。
2)具体来讲:当向 HashSet 中添加元素的时候,首先计算元素的 hashcode 值,
然后用这个(元素的 hashcode)%(HashMap 集合的大小)+1 计算出这个元
素的存储位置,如果这个位置为空,就将元素添加进去;如果不为空,则用 equals
方法比较元素是否相等,相等就不添加,否则找一个空位添加。
3、HashMap 和 HashTable 的主要区别是什么?,两者底层实现的数据结构是什么?
HashMap 和 HashTable 的区别:
二者都实现了 Map 接口,是将唯一的键映射到特定的值上,主要区别在于:
1)HashMap 没有排序,允许一个 null 键和多个 null 值,而 Hashtable 不允许;
2)HashMap 把 Hashtable 的 contains 方法去掉了,改成 containsvalue 和
containsKey, 因为 contains 方法容易让人引起误解;3)Hashtable 继承自 Dictionary 类,HashMap 是 Java1.2 引进的 Map 接口的
实现;
4)Hashtable 的方法是 Synchronized 的,而 HashMap 不是,在多个线程访问
Hashtable 时,不需要自己为它的方法实现同步,而 HashMap 就必须为之提供
额外的同步。Hashtable 和 HashMap 采用的 hash/rehash 算法大致一样,所以
性能不会有很大的差异。
HashMap 和 HashTable 的底层实现数据结构:
HashMap 和 Hashtable 的底层实现都是数组 + 链表结构实现的(jdk8 以前)
4、HashMap、ConcurrentHashMap、hash()相关原理解析?
HashMap 1.7 的原理:
HashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实
现稍有不同。
负载因子:
给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往
里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量
进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常
消耗性能。
因此通常建议能提前预估 HashMap 的大小最好,尽量的减少扩容带来
的性能损耗。
其实真正存放数据的是 Entry<K,V>[] table,Entry 是 HashMap 中的一个静态
内部类,它有 key、value、next、hash(key 的 hashcode)成员变量。
put 方法:
判断当前数组是否需要初始化。
如果 key 为空,则 put 一个空值进去。
根据 key 计算出 hashcode。
根据计算出的 hashcode 定位出所在桶。
如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入
key 相等,如果相等则进行覆盖,并返回原来的值。
如果桶是空的,说明当前位置没有数据存入,新增一个 Entry 对象写入
当前位置。(当调用 addEntry 写入 Entry 时需要判断是否需要扩容。
如果需要就进行两倍扩充,并将当前的 key 重新 hash 并定位。而在
createEntry 中会将当前位置的桶传入到新建的桶中,如果当前桶有值就
会在位置形成链表。)
get 方法:
首先也是根据 key 计算出 hashcode,然后定位到具体的桶中。
判断该位置是否为链表。
不是链表就根据 key、key 的 hashcode 是否相等来返回值。
为链表则需要遍历直到 key 及 hashcode 相等时候就返回值。
啥都没取到就直接返回 null 。
HashMap 1.8 的原理:
当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效
率就会越来越低;时间复杂度为 O(N),因此 1.8 中重点优化了这个查询效率。
TREEIFY_THRESHOLD 用于判断是否需要将链表转换为红黑树的阈值。HashEntry 修改为 Node。
put 方法:
判断当前桶是否为空,空的就需要初始化(在 resize 方法 中会判断是否
进行初始化)。
根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表
明没有 Hash 冲突就直接在当前位置创建一个新桶即可。
如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的
hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会
统一进行赋值及返回。
如果当前桶为红黑树,那就要按照红黑树的方式写入数据。
如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当
前桶的后面(形成链表)。
接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑
树。
如果在遍历过程中找到 key 相同时直接退出遍历。
如果 e != null 就相当于存在相同的 key,那就需要将值覆盖。
最后判断是否需要进行扩容。
get 方法:
首先将 key hash 之后取得所定位的桶。
如果桶为空则直接返回 null 。
否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的
key,是就直接返回 value。
如果第一个不匹配,则判断它的下一个是红黑树还是链表。
红黑树就按照树的查找方式返回值。
不然就按照链表的方式遍历匹配返回值。
修改为红黑树之后查询效率直接提高到了 O(logn)。但是 HashMap 原有的问题
也都存在,比如在并发场景下使用时容易出现死循环:
在 HashMap 扩容的时候会调用 resize() 方法,就是这里的并发操作容
易在一个桶上形成环形链表;这样当获取一个不存在的 key 时,计算出
的 index 正好是环形链表的下标就会出现死循环:在 1.7 中 hash 冲突
采用的头插法形成的链表,在并发条件下会形成循环链表,一旦有查询落
到了这个链表上,当获取不到值时就会死循环。
ConcurrentHashMap 1.7 原理:
ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于
ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同
步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)
的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的
Segment。
put 方法:
首先是通过 key 定位到 Segment,之后在对应的 Segment 中进行具体的
put。
虽然 HashEntry 中的 value 是用 volatile 关键词修饰的,但是并不能保
证并发的原子性,所以 put 操作时仍然需要加锁处理。
首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞
争,则利用 scanAndLockForPut() 自旋获取锁:尝试自旋获取锁。 如果重试的次数达到了 MAX_SCAN_RETRIES 则改为
阻塞锁获取,保证能获取成功。
将当前 Segment 中的 table 通过 key 的 hashcode 定位到
HashEntry。
遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是
否相等,相等则覆盖旧的 value。
为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断
是否需要扩容。
最后会使用 unlock()解除当前 Segment 的锁。
get 方法:
只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次
Hash 定位到具体的元素上。
由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内
存可见性,所以每次获取时都是最新值。
ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要
加锁。
ConcurrentHashMap 1.8 原理:
1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但
依然存在 HashMap 在 1.7 版本中的问题:那就是查询遍历链表效率太低。和
1.8 HashMap 结构类似:其中抛弃了原有的 Segment 分段锁,而采用了 CAS +
synchronized 来保证并发安全性。CAS:
如果 obj 内的 value 和 expect 相等,就证明没有其他线程改变过这个变量,那
么就更新它为 update,如果这一步的 CAS 没有成功,那就采用自旋的方式继续
进行 CAS 操作。
问题:
目前在 JDK 的 atomic 包里提供了一个类 AtomicStampedReference 来解
决 ABA 问题。这个类的 compareAndSet 方法作用是首先检查当前引用是
否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以
原子方式将该引用和该标志的值设置为给定的更新值。
如果 CAS 不成功,则会原地自旋,如果长时间自旋会给 CPU 带来非常大
的执行开销。
put 方法:
根据 key 计算出 hashcode 。
判断是否需要进行初始化。
如果当前 key 定位出的 Node 为空表示当前位置可以写入数据,利用
CAS 尝试写入,失败则自旋保证成功。
如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
如果都不满足,则利用 synchronized 锁写入数据。
最后,如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。
get 方法:
根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
如果是红黑树那就按照树的方式获取值。
就不满足那就按照链表的方式遍历获取值。
1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率
(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出
在新版的 JDK 中对 synchronized 优化是很到位的。
HashMap、ConcurrentHashMap 1.7/1.8 实现原理
hash()算法全解析
HashMap 何时扩容:
当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值---
即大于当前数组的长度乘以加载因子的值的时候,就要自动扩容。
扩容的算法是什么:
扩容(resize)就是重新计算容量,向 HashMap 对象里不停的添加元素,而
HashMap 对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,
以便能装入更多的元素。当然 Java 里的数组是无法自动扩容的,方法是使用一
个新的数组代替已有的容量小的数组。
Hashmap 如何解决散列碰撞(必问)?
Java 中 HashMap 是利用“拉链法”处理 HashCode 的碰撞问题。在调用 HashMap
的 put 方法或 get 方法时,都会首先调用 hashcode 方法,去查找相关的 key,
当有冲突时,再调用 equals 方法。hashMap 基于 hasing 原理,我们通过 put
和 get 方法存取对象。当我们将键值对传递给 put 方法时,他调用键对象的
hashCode()方法来计算 hashCode,然后找到 bucket(哈希桶)位置来存储对象。当获取对象时,通过键对象的 equals()方法找到正确的键值对,然后返回值对象。
HashMap 使用链表来解决碰撞问题,当碰撞发生了,对象将会存储在链表的下
一个节点中。hashMap 在每个链表节点存储键值对对象。当两个不同的键却有
相同的 hashCode 时,他们会存储在同一个 bucket 位置的链表中。键对象的
equals()来找到键值对。
Hashmap 底层为什么是线程不安全的?
并发场景下使用时容易出现死循环,在 HashMap 扩容的时候会调用
resize() 方法,就是这里的并发操作容易在一个桶上形成环形链表;这样
当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标就
会出现死循环;
在 1.7 中 hash 冲突采用的头插法形成的链表,在并发条件下会形成循
环链表,一旦有查询落到了这个链表上,当获取不到值时就会死循环。
5、ArrayMap 跟 SparseArray 在 HashMap 上面的改进?
HashMap 要存储完这些数据将要不断的扩容,而且在此过程中也需要不断的做
hash 运算,这将对我们的内存空间造成很大消耗和浪费。
SparseArray:
SparseArray 比 HashMap 更省内存,在某些条件下性能更好,主要是因为它避
免了对 key 的自动装箱(int 转为 Integer 类型),它内部则是通过两个数组来进
行数据存储的,一个存储 key,另外一个存储 value,为了优化性能,它内部对
数据还采取了压缩的方式来表示稀疏数组的数据,从而节约内存空间,我们从源
码中可以看到 key 和 value 分别是用数组表示:
private int[] mKeys;private Object[] mValues;
同时,SparseArray 在存储和读取数据时候,使用的是二分查找法。也就是在 put
添加数据的时候,会使用二分查找法和之前的 key 比较当前我们添加的元素的
key 的大小,然后按照从小到大的顺序排列好,所以,SparseArray 存储的元素
都是按元素的 key 值从小到大排列好的。 而在获取数据的时候,也是使用二分
查找法判断元素的位置,所以,在获取数据的时候非常快,比 HashMap 快的多。
ArrayMap:
ArrayMap 利用两个数组,mHashes 用来保存每一个 key 的 hash 值,mArrray
大小为 mHashes 的 2 倍,依次保存 key 和 value。
mHashes[index] = hash;
mArray[index<<1] = key;
mArray[(index<<1)+1] = value;
当插入时,根据 key 的 hashcode()方法得到 hash 值,计算出在 mArrays 的 index
位置,然后利用二分查找找到对应的位置进行插入,当出现哈希冲突时,会在
index 的相邻位置插入。
假设数据量都在千级以内的情况下:
1、如果 key 的类型已经确定为 int 类型,那么使用 SparseArray,因为它避免了
自动装箱的过程,如果 key 为 long 类型,它还提供了一个 LongSparseArray 来
确保 key 为 long 类型时的使用
2、如果 key 类型为其它的类型,则使用 ArrayMap。
三、反射 (⭐⭐⭐)1、说说你对 Java 反射的理解?
答:Java 中的反射首先是能够获取到 Java 中要反射类的字节码, 获取字节码
有三种方法:
1.Class.forName(className)
2.类名.class
3.this.getClass()。
然后将字节码中的方法,变量,构造函数等映射成相应的 Method、Filed、
Constructor 等类,这些类提供了丰富的方法可以被我们所使用。
深入解析 Java 反射(1) - 基础
Java 基础之—反射(非常重要)
四、泛型 (⭐⭐)
1、简单介绍一下 java 中的泛型,泛型擦除以及相关的概念,解析与分派?
泛型是 Java SE1.5 的新特性,泛型的本质是参数化类型,也就是说所操的数据类
型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称
为泛型类、泛型接口、泛型方法。 Java 语言引入泛型的好处是安全简单。
在 Java SE 1.5 之前,没有泛型的情况的下,通过对类型 Object 的引用来实现参
数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是
要求开发者实际参数类型可以预知的情况下进行的。对于强制类型换错误的情
况,编译器可能不提示错误,在运行的时候出现异常,这是一个安全隐患。
泛型的好处是在编译的时候检查类型安全,并且所有的转换都是自动和隐式的,
提高代码的重用率。1、泛型的类型参数只能是类类型(包括自定义类),不是简单类型。
2、同一种泛型可以对应多个版本(因为参数类型是不确的),不同版本的泛型
类实例是不兼容的。
3、泛型的类型参数可以有多个。
4、泛型的参数类型可以使用 extends 语句,例如。习惯上称为“有界类型”。
5、泛型的参数类型还可以是通配符类型。例如 Class<?> classType =
Class.forName("java.lang.String");
泛型擦除以及相关的概念
泛型信息只存在代码编译阶段,在进入 JVM 之前,与泛型关的信息都会被擦除
掉。
在类型擦除的时候,如果泛型类里的类型参数没有指定上限,则会被转成 Object
类型,如果指定了上限,则会被传转换成对应的类型上限。
Java 中的泛型基本上都是在编译器这个层次来实现的。生成的 Java 字节码中是
不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编
译的时候擦除掉。这个过程就称为类型擦除。
类型擦除引起的问题及解决方法:
1、先检查,在编译,以及检查编译的对象和引用传递的题
2、自动类型转换
3、类型擦除与多态的冲突和解决方法
4、泛型类型变量不能是基本数据类型5、运行时类型查询
6、异常中使用泛型的问题
7、数组(这个不属于类型擦除引起的问题)
9、类型擦除后的冲突
10、泛型在静态方法和静态类中的问题
五、注解 (⭐⭐)
1、说说你对 Java 注解的理解?
注解相当于一种标记,在程序中加了注解就等于为程序打上了某种标记。程序可
以利用 ava 的反射机制来了解你的类及各种元素上有无何种标记,针对不同的标
记,就去做相应的事件。标记可以加在包,类,字段,方法,方法的参数以及局
部变量上。
六、其它 (⭐⭐)
1、Java 的 char 是两个字节,是怎么存 Utf-8 的字符的?
是否熟悉 Java char 和字符串(初级)
char 是 2 个字节,utf-8 是 1~3 个字节。
字符集(字符集不是编码):ASCII 码与 Unicode 码。
字符 -> 0xd83dde00(码点)。
是否了解字符的映射和存储细节(中级)
人类认知:字符 => 字符集:0x4e2d(char) => 计算机存储(byte):01001110:4e、
00101101:2d编码:UTF-16
“中”.getBytes("utf-6"); -> fe ff 4e 2d:4 个字节,其中前面的 fe ff 只是字节序标
志。
是否能触类旁通,横向对比其他语言(高级)
Python2 的字符串:
byteString = "中"
unicodeString = u"中"
令人迷惑的字符串长度
emoij = u"表情"
print(len(emoji)
Java 与 python 3.2 及以下:2 字节 python >= 3.3:1 字节
注意:Java 9 对 latin 字符的存储空间做了优化,但字符串长度还是!= 字符数。
总结
Java char 不存 UTF-8 的字节,而是 UTF-16。
Unicode 通用字符集占两个字节,例如“中”。
Unicode 扩展字符集需要用一对 char 来表示,例如“表情”。
Unicode 是字符集,不是编码,作用类似于 ASCII 码。
Java String 的 length 不是字符数。
2、Java String 可以有多长?
是否对字符串编解码有深入了解(中级)分配到栈:
String longString = "aaa...aaa";
分配到堆:
byte[] bytes = loadFromFile(new File("superLongText.txt");
String superLongString = new String(bytes);
是否对字符串在内存当中的存储形式有深入了解(高级)
是否对 Java 虚拟机字节码有足够的了解(高级)
源文件:*.java
String longString = "aaa...aaa";
字节数 <= 65535
字节码:*.class
CONSTANT_Utf8_info {
u1 tag;
u2 length;
(0~65535) u1 bytes[length];
最多 65535 个字节
}
javac 的编译器有问题,< 65535 应该改为< = 65535。
Java String 栈分配
受字节码限制,字符串最终的 MUTF-8 字节数不超过 65535。
Latin 字符,受 Javac 代码限制,最多 65534 个。
非 Latin 字符最终对应字节个数差异较大,最多字节个数是 65535。
如果运行时方法区设置较小,也会受到方法区大小的限制。
是否对 java 虚拟机指令有一定的认识(高级)
new String(bytes)内部是采用了一个字符数组,其对应的虚拟机指令是 newarray
[int] ,数组理论最大个数为 Integer.MAX_VALUE,有些虚拟机需要一些头部信
息,所以 MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8。
Java String 堆分配
受虚拟机指令限制,字符数理论上限为 Integer.MAX_VALUE。
受虚拟机实现限制,实际上限可能会小于 Integer.MAX_VALUE。
如果堆内存较小,也会受到堆内存的限制。
总结
Java String 字面量形式
字节码中 CONSTANT_Utf8_info 的限制
Javac 源码逻辑的限制
方法区大小的限制
Java String 运行时创建在堆上的形式
Java 虚拟机指令 newarray 的限制
Java 虚拟机堆内存大小的限制
3、Java 的匿名内部类有哪些限制?
考察匿名内部类的概念和用法(初级)
匿名内部类的名字:没有人类认知意义上的名字
只能继承一个父类或实现一个接口
包名.OuterClass$1,表示定位的第一个匿名内部类。外部类加$N,N 是
匿名内部类的顺序。
考察语言规范以及语言的横向对比等(中级)
匿名内部类的继承结构:Java 中的匿名内部类不可以继承,只有内部类才可以有
实现继承、实现接口的特性。而 Kotlin 是的匿名内部类是支持继承的,如
val runnableFoo = object: Foo(),Runnable {
override fun run() {
}
}
作为考察内存泄漏的切入点(高级)
匿名内部类的构造方法(深入源码字节码探索语言本质的能力):
匿名内部类会默认持有外部类的引用,可能会导致内存泄漏。
由编译器生成的。
其参数列表包括
外部对象(定义在非静态域内)
父类的外部对象(父类非静态)
父类的构造方法参数(父类有构造方法且参数列表不为空)
外部捕获的变量(方法体内有引用外部 final 变量)
Lambda 转换(SAM 类型,仅支持单一接口类型):
如果 CallBack 是一个 interface,不是抽象类,则可以转换为 Lambda 表达式。
CallBack callBack = () -> {
...};
总结
没有人类认知意义上的名字。
只能继承一个父类或实现一个接口。
父类是非静态的类型,则需父类外部实例来初始化。
如果定义在非静态作用域内,会引用外部类实例。
只能捕获外部作用域内的 final 变量。
创建时只有单一方法的接口可以用 Lambda 转换。
技巧点拨
关注语言版本的变化:
体现对技术的热情
体现好学的品质
显得专业
4、Java 中对异常是如何进行分类的?
异常整体分类:
Java 异常结构中定义有 Throwable 类。 Exception 和 Error 为其子类。
Error 是程序无法处理的错误,比如 OutOfMemoryError、StackOverflowError。
这些异常发生时, Java 虚拟机(JVM)一般会选择线程终止。
Exception 是程序本身可以处理的异常,这种异常分两大类运行时异常和非运行
时异常,程序中应当尽可能去处理这些异常。
运行时异常都是 RuntimeException 类及其子类异常,如 NullPointerException、
IndexOutOfBoundsException 等, 这些异常是不检查异常,程序中可以选择捕
获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的, 程序应该从
逻辑角度尽可能避免这类异常的发生。异常处理的两个基本原则:
1、尽量不要捕获类似 Exception 这样的通用异常,而是应该捕获特定异常。
2、不要生吞异常。
NoClassDefFoundError 和 ClassNotFoundException 有什么区别?
ClassNotFoundException 的产生原因主要是: Java 支持使用反射方式在运行时
动态加载类,例如使用 Class.forName 方法来动态地加载类时,可以将类名作为
参数传递给上述方法从而将指定类加载到 JVM 内存中,如果这个类在类路径中
没有被找到,那么此时就会在运行时抛出 ClassNotFoundException 异常。 解决
该问题需要确保所需的类连同它依赖的包存在于类路径中,常见问题在于类名书
写错误。 另外还有一个导致 ClassNotFoundException 的原因就是:当一个类已
经某个类加载器加载到内存中了,此时另一个类加载器又尝试着动态地从同一个
包中加载这个类。通过控制动态类加载过程,可以避免上述情况发生。
NoClassDefFoundError 产生的原因在于: 如果 JVM 或者 ClassLoader 实例尝试
加载(可以通过正常的方法调用,也可能是使用 new 来创建新的对象)类的时
候却找不到类的定义。要查找的类在编译的时候是存在的,运行的时候却找不到
了。这个时候就会导致 NoClassDefFoundError. 造成该问题的原因可能是打包过
程漏掉了部分类,或者 jar 包出现损坏或者篡改。解决这个问题的办法是查找那
些在开发期间存在于类路径下但在运行期间却不在类路径下的类。
5、String 为什么要设计成不可变的?
String 是不可变的(修改 String 时,不会在原有的内存地址修改,而是重新指向
一个新对象),String 用 final 修饰,不可继承,String 本质上是个 final 的 char[]
数组,所以 char[]数组的内存地址不会被修改,而且 String 也没有对外暴露修改
char[]数组的方法。不可变性可以保证线程安全以及字符串串常量池的实现。6、Java 里的幂等性了解吗?
幂等性原本是数学上的一个概念,即:f(x) = f(f(x)),对同一个系统,使用同样的
条件,一次请求和重复的多次请求对系统资源的影响是一致的。
幂等性最为常见的应用就是电商的客户付款,试想一下如果你在付款的时候因为
网络等各种问题失败了,然后去重复的付了一次,是一种多么糟糕的体验。幂等
性就是为了解决这样的问题。
实现幂等性可以使用 Token 机制。
核心思想是为每一次操作生成一个唯一性的凭证,也就是 token。一个 token 在
操作的每一个阶段只有一次执行权,一旦执行成功则保存执行结果。对重复的请
求,返回同一个结果。
例如:电商平台上的订单 id 就是最适合的 token。当用户下单时,会经历多个环
节,比如生成订单,减库存,减优惠券等等。每一个环节执行时都先检测一下该
订单 id 是否已经执行过这一步骤,对未执行的请求,执行操作并缓存结果,而
对已经执行过的 id,则直接返回之前的执行结果,不做任何操 作。这样可以在
最大程度上避免操作的重复执行问题,缓存起来的执行结果也能用于事务的控制
等。
7、为什么 Java 里的匿名内部类只能访问 final 修饰的外部变量?
匿名内部类用法:
public class TryUsingAnonymousClass {
public void useMyInterface() {
final Integer number = 123;
System.out.println(number);
MyInterface myInterface = new MyInterface() {
@Override
public void doSomething() {System.out.println(number);
}
};
myInterface.doSomething();
System.out.println(number);
}
}
编译后的结果
class TryUsingAnonymousClass$1
implements MyInterface {
private final TryUsingAnonymousClass this$0;
private final Integer paramInteger;
TryUsingAnonymousClass$1(TryUsingAnonymousClass this$0, Integer
paramInteger) {
this.this$0 = this$0;
this.paramInteger = paramInteger;
}
public void doSomething() {
System.out.println(this.paramInteger);
}
}
因为匿名内部类最终会编译成一个单独的类,而被该类使用的变量会以构造函数
参数的形式传递给该类,例如:Integer paramInteger,如果变量不定义成 final的,paramInteger 在匿名内部类被可以被修改,进而造成和外部的 paramInteger
不一致的问题,为了避免这种不一致的情况,因次 Java 规定匿名内部类只能访
问 final 修饰的外部变量。
8、讲一下 Java 的编码方式?
为什么需要编码
计算机存储信息的最小单元是一个字节即 8bit,所以能示的范围是 0~255,这个
范围无法保存所有的字符,所以要一个新的数据结构 char 来表示这些字符,从
char 到 byte 需要编码。
常见的编码方式有以下几种:
ASCII:总共有 128 个,用一个字节的低 7 位表示,031 是控制字符如换行回
车删除等;32126 是打印字符,可以通过键盘输入并且能够显示出来。
GBK:码范围是 8140~FEFE(去掉 XX7F)总共有 23940 个码位,它能表示
21003 个汉字,它的编码是和 GB2312 兼容的,也就是说用 GB2312 编码的汉
字可以用 GBK 来解码,并且不会有乱码。
UTF-16:UTF-16 具体定义了 Unicode 字符在计算机中存取方法。UTF-16 用
两个字节来表示 Unicode 转化格式,这个是定长的表示方法,不论什么字符都
可以用两个字节表示,两个字节是 16 个 bit,所以叫 UTF-16。UTF-16 表示字
符非常方便,每两个字节表示一个字符,这个在字符串操作时就大大简化了操作,
这也是 Java 以 UTF-16 作为内存的字符存储格式的一个很重要的原因。
UTF-8:统一采用两个字节表示一个字符,虽然在表示上非常简单方便,但是也
有其缺点,有很大一部分字符用一个字节就可以表示的现在要两个字节表示,存
储空间放大了一倍,在现在的网络带宽还非常有限的今天,这样会增大网络传输的流量,而且也没必要。而 UTF-8 采用了一种变长技术,每个编码区域有不同
的字码长度。不同类型的字符可以是由 1~6 个字节组成。
Java 中需要编码的地方一般都在字符到字节的转换上,这个一般包括磁盘 IO 和
网络 IO。
Reader 类是 Java 的 I/O 中读字符的父类,而 InputStream 类是读字节的父
类,InputStreamReader 类就是关联字节到字符的桥梁,它负责在 I/O 过程中
处理读取字节到字符的转换,而具体字节到字符解码实现由 StreamDecoder 去
实现,在 StreamDecoder 解码过程中必须由用户指定 Charset 编码格式。
9、String,StringBuffer,StringBuilder 有哪些不同?
三者在执行速度方面的比较:StringBuilder > StringBuffer > String
String 每次变化一个值就会开辟一个新的内存空间
StringBuilder:线程非安全的
StringBuffer:线程安全的
对于三者使用的总结:
1.如果要操作少量的数据用 String。
2.单线程操作字符串缓冲区下操作大量数据用 StringBuilder。
3.多线程操作字符串缓冲区下操作大量数据用 StringBuffer。String 是 Java 语言非常基础和重要的类,提供了构造和管理字符串的各种基本
逻辑。它是典型的 Immutable 类,被声明成为 final class,所有属性也都是 final
的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String
对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影
响。
StringBuffer 是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,
我们可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定
位置。StringBuffer 本质是一个线程安全的可修改字符序列,它保证了线程安全,
也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用
它的后继者,也就是 StringBuilder。
StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 没有本质区别,
但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串
拼接的首选。
10、什么是内部类?内部类的作用。
内部类可以有多个实例,每个实例都有自己的状态信息,并且与其他外围对象的
信息相互独立。
在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同
一个类。
创建内部类对象并不依赖于外围类对象的创建。
内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。
内部类提供了更好的封装,除了该外围类,其他类都不能访问。。
11、抽象类和接口区别?
共同点
是上层的抽象层。
都不能被实例化。
都能包含抽象的方法,这些抽象的方法用于描述类具备的功能,但是不提
供具体的实现。
区别:
1、在抽象类中可以写非抽象的方法,从而避免在子类中重复书写他们,
这样可以提高代码的复用性,这是抽象类的优势,接口中只能有抽象的方
法。
2、多继承:一个类只能继承一个直接父类,这个父类可以是具体的类也
可是抽象类,但是一个类可以实现多个接口。
3、抽象类可以有默认的方法实现,接口根本不存在方法的实现。
4、子类使用 extends 关键字来继承抽象类。如果子类不是抽象类的话,
它需要提供抽象类中所有声明方法的实现。子类使用关键字 implements
来实现接口。它需要提供接口中所有声明方法的实现。
5、构造器:抽象类可以有构造器,接口不能有构造器。
6、和普通 Java 类的区别:除了你不能实例化抽象类之外,抽象类和普通
Java 类没有任何区别,接口是完全不同的类型。
7、访问修饰符:抽象方法可以有 public、protected 和 default 修饰符,接
口方法默认修饰符是 public。你不可以使用其它修饰符。
8、main 方法:抽象方法可以有 main 方法并且我们可以运行它接口没有
main 方法,因此我们不能运行它。
9、速度:抽象类比接口速度要快,接口是稍微有点慢的,因为它需要时间
去寻找在类中实现的方法。
10、添加新方法:如果你往抽象类中添加新的方法,你可以给它提供默认
的实现。因此你不需要改变你现在的代码。如果你往接口中添加方法,那
么你必须改变实现该接口的类。
12、接口的意义?规范、扩展、回调。
13、父类的静态方法能否被子类重写?
不能。子类继承父类后,用相同的静态方法和非静态方法,这时非静态方法覆盖
父类中的方法(即方法重写),父类的该静态方法被隐藏(如果对象是父类则调
用该隐藏的方法),另外子类可继承父类的静态与非静态方法,至于方法重载我
觉得它其中一要素就是在同一类中,不能说父类中的什么方法与子类里的什么方
法是方法重载的体现。
14、抽象类的意义?
为其子类提供一个公共的类型,封装子类中的重复内容,定义抽象方法,子类虽
然有不同的实现 但是定义是一致的。
15、静态内部类、非静态内部类的理解?
静态内部类:只是为了降低包的深度,方便类的使用,静态内部类适用于包含在
类当中,但又不依赖与外在的类,不用使用外在类的非静态属性和方法,只是为
了方便管理类结构而定义。在创建静态内部类的时候,不需要外部类对象的引用。
非静态内部类:持有外部类的引用,可以自由使用外部类的所有变量和方法。
16、为什么复写 equals 方法的同时需要复写 hashcode 方法,前者相同后者是否相同,反过来
呢?为什么?
要考虑到类似 HashMap、HashTable、HashSet 的这种散列的数据类型的运用,
当我们重写 equals 时,是为了用自身的方式去判断两个自定义对象是否相等,
然而如果此时刚好需要我们用自定义的对象去充当 hashmap 的键值使用时,就
会出现我们认为的同一对象,却因为 hash 值不同而导致 hashmap 中存了两个对
象,从而才需要进行 hashcode 方法的覆盖。
17、equals 和 hashcode 的关系?
hashcode 和 equals 的约定关系如下:
1、如果两个对象相等,那么他们一定有相同的哈希值(hashcode)。2、如果两个对象的哈希值相等,那么这两个对象有可能相等也有可能不
相等。(需要再通过 equals 来判断)
18、java 为什么跨平台?
因为 Java 程序编译之后的代码不是能被硬件系统直接运行的代码,而是一种“中
间码”——字节码。然后不同的硬件平台上安装有不同的 Java 虚拟机(JVM),由
JVM 来把字节码再“翻译”成所对应的硬件平台能够执行的代码。因此对于 Java
编程者来说,不需要考虑硬件平台是什么。所以 Java 可以跨平台。
19、浮点数的精准计算
BigDecimal 类进行商业计算,Float 和 Double 只能用来做科学计算或者是工程
计算。
20、final,finally,finalize 的区别?
final 可以用来修饰类、方法、变量,分别有不同的意义,final 修饰的 class 代
表不可以继承扩展,final 的变量是不可以修改的,而 final 的方法也是不可以
重写的(override)。
finally 则是 Java 保证重点代码一定要被执行的一种机制。我们可以使用
try-finally 或者 try-catch-finally 来进行类似关闭 JDBC 连接、保证 unlock 锁
等动作。
finalize 是基础类 java.lang.Object 的一个方法,它的设计目的是保证对象在被
垃圾收集前完成特定资源的回收。finalize 机制现在已经不推荐使用,并且在
JDK 9 开始被标记为 deprecated。Java 平台目前在逐步使用
java.lang.ref.Cleaner 来替换掉原有的 finalize 实现。Cleaner 的实现利用了幻象引用(PhantomReference),这是一种常见的所谓 post-mortem 清理机制。
利用幻象引用和引用队列,我们可以保证对象被彻底销毁前做一些类似资源回收
的工作,比如关闭文件描述符(操作系统有限的资源),它比 finalize 更加轻量、
更加可靠。
21、静态内部类的设计意图
静态内部类与非静态内部类之间存在一个最大的区别:非静态内部类在编译完成
之后会隐含地保存着一个引用,该引用是指向创建它的外围内,但是静态内部类
却没有。
没有这个引用就意味着:
它的创建是不需要依赖于外围类的。 它不能使用任何外围类的非 static 成员变
量和方法。
22、Java 中对象的生命周期
在 Java 中,对象的生命周期包括以下几个阶段:
1.创建阶段(Created)
JVM 加载类的 class 文件 此时所有的 static 变量和 static 代码块将被执行 加载
完成后,对局部变量进行赋值(先父后子的顺序) 再执行 new 方法 调用构造
函数 一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到了
应用阶段。
2.应用阶段(In Use)
对象至少被一个强引用持有着。
3.不可见阶段(Invisible)
当一个对象处于不可见阶段时,说明程序本身不再持有该对象的任何强引用,虽
然该这些引用仍然是存在着的。 简单说就是程序的执行已经超出了该对象的作
用域了。4.不可达阶段(Unreachable)
对象处于不可达阶段是指该对象不再被任何强引用所持有。 与“不可见阶段”相
比,“不可见阶段”是指程序不再持有该对象的任何强引用,这种情况下,该对象
仍可能被 JVM 等系统下的某些已装载的静态变量或线程或 JNI 等强引用持有着,
这些特殊的强引用被称为”GC root”。存在着这些 GC root 会导致对象的内存泄
露情况,无法被回收。
5.收集阶段(Collected)
当垃圾回收器发现该对象已经处于“不可达阶段”并且垃圾回收器已经对该对象
的内存空间重新分配做好准备时,则对象进入了“收集阶段”。如果该对象已经重
写了 finalize()方法,则会去执行该方法的终端操作。
6.终结阶段(Finalized)
当对象执行完 finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。
在该阶段是等待垃圾回收器对该对象空间进行回收。
7.对象空间重分配阶段(De-allocated)
垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象彻底
消失了,称之为“对象空间重新分配阶段。
23、静态属性和静态方法是否可以被继承?是否可以被重写?以及原因?
结论:java 中静态属性和静态方法可以被继承,但是不可以被重写而是被隐藏。
原因:
1). 静态方法和属性是属于类的,调用的时候直接通过类名.方法名完成,不需要
继承机制即可以调用。如果子类里面定义了静态方法和属性,那么这时候父类的静态方法或属性称之为"隐藏"。如果你想要调用父类的静态方法和属性,直接通
过父类名.方法或变量名完成,至于是否继承一说,子类是有继承静态方法和属
性,但是跟实例方法和属性不太一样,存在"隐藏"的这种情况。
2). 多态之所以能够实现依赖于继承、接口和重写、重载(继承和重写最为关键)。
有了继承和重写就可以实现父类的引用指向不同子类的对象。重写的功能是:"
重写"后子类的优先级要高于父类的优先级,但是“隐藏”是没有这个优先级之分
的。
3). 静态属性、静态方法和非静态的属性都可以被继承和隐藏而不能被重写,因
此不能实现多态,不能实现父类的引用可以指向不同子类的对象。非静态方法可
以被继承和重写,因此可以实现多态。
24、object 类的 equal 和 hashcode 方法重写,为什么?
在 Java API 文档中关于 hashCode 方法有以下几点规定(原文来自 java 深入解
析一书):
1、在 java 应用程序执行期间,如果在 equals 方法比较中所用的信息没有被修
改,那么在同一个对象上多次调用 hashCode 方法时必须一致地返回相同的整
数。如果多次执行同一个应用时,不要求该整数必须相同。
2、如果两个对象通过调用 equals 方法是相等的,那么这两个对象调用 hashCode
方法必须返回相同的整数。
3、如果两个对象通过调用 equals 方法是不相等的,不要求这两个对象调用
hashCode 方法必须返回不同的整数。但是程序员应该意识到对不同的对象产生
不同的 hash 值可以提供哈希表的性能。
25、java 中==和 equals 和 hashCode 的区别?默认情况下也就是从超类 Object 继承而来的 equals 方法与‘==’是完全等价的,
比较的都是对象的内存地址,但我们可以重写 equals 方法,使其按照我们的需
求的方式进行比较,如 String 类重写了 equals 方法,使其比较的是字符的序列,
而不再是内存地址。在 java 的集合中,判断两个对象是否相等的规则是:
1.判断两个对象的 hashCode 是否相等。
2.判断两个对象用 equals 运算是否相等。
26、Java 的四种引用及使用场景?
强引用(FinalReference):在内存不足时不会被回收。平常用的最多的
对象,如新创建的对象。
软引用(SoftReference):在内存不足时会被回收。用于实现内存敏感的
高速缓存。
弱引用(WeakReferenc):只要 GC 回收器发现了它,就会将之回收。用
于 Map 数据结构中,引用占用内存空间较大的对象。
虚引用(PhantomReference):在回收之前,会被放入 ReferenceQueue,
JVM 不会自动将该 referent 字段值设置成 null。其它引用被 JVM 回收之
后才会被放入 ReferenceQueue 中。用于实现一个对象被回收之前做一些
清理工作。
27、类的加载过程,Person person = new Person();为例进行说明。
1).因为 new 用到了 Person.class,所以会先找到 Person.class 文件,并加载到内
存中;
2).执行该类中的 static 代码块,如果有的话,给 Person.class 类进行初始化;
3).在堆内存中开辟空间分配内存地址;4).在堆内存中建立对象的特有属性,并进行默认初始化;
5).对属性进行显示初始化;
6).对对象进行构造代码块初始化;
7).对对象进行与之对应的构造函数进行初始化;
8).将内存地址付给栈内存中的 p 变量。
28、JAVA 常量池
Interger 中的 128(-128~127)
a.当数值范围为-128~127 时:如果两个 new 出来的 Integer 对象,即使值相同,
通过“==”比较结果为 false,但两个对直接赋值,则通过“==”比较结果为“true,
这一点与 String 非常相似。
b.当数值不在-128~127 时,无论通过哪种方式,即使两对象的值相等,通过“==”
比较,其结果为 false;
c.当一个 Integer 对象直接与一个 int 基本数据类型通过“==”比较,其结果与第一
点相同;
d.Integer 对象的 hash 值为数值本身;
为什么是-128-127?
在 Integer 类中有一个静态内部类 IntegerCache,在 IntegrCache 类中有一个
Integer 数组,用以缓存当前数值范围为-128~127 时的 Integer 对象。29、在重写 equals 方法时,需要遵循哪些约定,具体介绍一下?
重写 equals 方法时需要遵循通用约定:自反性、对称性、传递性、一致性、非
空性
1)自反性
对于任何非 null 的引用值 x,x.equals(x)必须返回 true。---这一点基本上不会有啥
问题
2)对称性
对于任何非 null 的引用值 x 和 y,当且仅当 x.equals(y)为 true 时,y.equals(x)也
为 true。
3)传递性
对于任何非 null 的引用值 x、y、z。如果 x.equals(y)==true,y.equals(z)==true,
那么 x.equals(z)==true。
4) 一致性
对于任何非 null 的引用值 x 和 y,只要 equals 的比较操作在对象所用的信息没
有被修改,那么多次调用 x.equals(y)就会一致性地返回 true,或者一致性的返回
false。
5)非空性
所有比较的对象都不能为空。
30、深拷贝和浅拷贝的区别
31、Integer 类对 int 的优化第二节 Java 并发面试题
一、线程池相关 (⭐⭐⭐)
1、什么是线程池,如何使用?为什么要使用线程池?
答:线程池就是事先将多个线程对象放到一个容器中,使用的时候就不用 new
线程而是直接去池中拿线程即可,节 省了开辟子线程的时间,提高了代码执行
效率。
2、Java 中的线程池共有几种?
Java 有四种线程池:
第一种:newCachedThreadPool
不固定线程数量,且支持最大为 Integer.MAX_VALUE 的线程数量:
public static ExecutorService newCachedThreadPool() {
// 这个线程池 corePoolSize 为 0,maximumPoolSize 为 Integer.MAX_VALUE
// 意思也就是说来一个任务就创建一个 woker,回收时间是 60s
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
可缓存线程池:
1、线程数无限制。 2、有空闲线程则复用空闲线程,若无空闲线程则新建线程。
3、一定程序减少频繁创建/销毁线程,减少系统开销。
第二种:newFixedThreadPool一个固定线程数量的线程池:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory
threadFactory) {
// corePoolSize 跟 maximumPoolSize 值一样,同时传入一个无界阻塞队列
// 该线程池的线程会维持在指定线程数,不会进行回收
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
定长线程池:
1、可控制线程最大并发数(同时执行的线程数)。 2、超出的线程会在队列中
等待。
第三种:newSingleThreadExecutor
可以理解为线程数量为 1 的 FixedThreadPool:
public static ExecutorService newSingleThreadExecutor() {
// 线程池中只有一个线程进行任务执行,其他的都放入阻塞队列
// 外面包装的 FinalizableDelegatedExecutorService 类实现了 finalize 方法,在
JVM 垃圾回收的时候会关闭线程池
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}单线程化的线程池:
1、有且仅有一个工作线程执行任务。 2、所有任务按照指定顺序执行,即遵循
队列的入队出队规则。
第四种:newScheduledThreadPool。
支持定时以指定周期循环执行任务:
public static ScheduledExecutorService newScheduledThreadPool(int
corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
注意:前三种线程池是 ThreadPoolExecutor 不同配置的实例,最后一种是
ScheduledThreadPoolExecutor 的实例。
3、线程池原理?
从数据结构的角度来看,线程池主要使用了阻塞队列(BlockingQueue)和
HashSet 集合构成。 从任务提交的流程角度来看,对于使用线程池的外部来说,
线程池的机制是这样的:
1、如果正在运行的线程数 < coreSize,马上创建核心线程执行该 task,不排队等待;
2、如果正在运行的线程数 >= coreSize,把该 task 放入阻塞队列;
3、如果队列已满 && 正在运行的线程数 < maximumPoolSize,创建新的非核心线程执行该
task;
4、如果队列已满 && 正在运行的线程数 >= maximumPoolSize,线程池调用 handler 的 reject
方法拒绝本次提交。
理解记忆:1-2-3-4 对应(核心线程->阻塞队列->非核心线程->handler 拒绝提
交)。
线程池的线程复用:这里就需要深入到源码 addWorker():它是创建新线程的关键,也是线程复用的
关键入口。最终会执行到 runWoker,它取任务有两个方式:
firstTask:这是指定的第一个 runnable 可执行任务,它会在 Woker 这个
工作线程中运行执行任务 run。并且置空表示这个任务已经被执行。
getTask():这首先是一个死循环过程,工作线程循环直到能够取出
Runnable 对象或超时返回,这里的取的目标就是任务队列 workQueue,
对应刚才入队的操作,有入有出。
其实就是任务在并不只执行创建时指定的 firstTask 第一任务,还会从任务队列
的中通过 getTask()方法自己主动去取任务执行,而且是有/无时间限定的阻塞等
待,保证线程的存活。
信号量
semaphore 可用于进程间同步也可用于同一个进程间的线程同步。
可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之
前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放
信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。
4、线程池都有哪几种工作队列?
1、ArrayBlockingQueue
是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素
进行排序。
2、LinkedBlockingQueue
一个基于链表结构的阻塞队列,此队列按 FIFO (先进先出) 排序元素,吞吐
量通常要高于 ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()和 Executors.newSingleThreadExecutor 使用了
这个队列。
3、SynchronousQueue
一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,
否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQueue,静
态工厂方法 Executors.newCachedThreadPool 使用了这个队列。
4、PriorityBlockingQueue
一个具有优先级的无限阻塞队列。
5、怎么理解无界队列和有界队列?
有界队列
1.初始的 poolSize < corePoolSize,提交的 runnable 任务,会直接做为 new 一
个 Thread 的参数,立马执行 。 2.当提交的任务数超过了 corePoolSize,会将
当前的 runable 提交到一个 block queue 中。3.有界队列满了之后,如果 poolSize
< maximumPoolsize 时,会尝试 new 一个 Thread 的进行救急处理,立马执行
对应的 runnable 任务。 4.如果 3 中也无法处理了,就会走到第四步执行 reject
操作。
无界队列
与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败
的情况。当有新的任务到来,系统的线程数小于 corePoolSize 时,则新建线程
执行任务。当达到 corePoolSize 后,就不会继续增加,若后续仍有新的任务加
入,而没有空闲的线程资源,则任务直接进入队列等待。若任务创建和处理的速
度差异很大,无界队列会保持快速增长,直到耗尽系统内存。 当线程池的任务缓存队列已满并且线程池中的线程数目达到 maximumPoolSize,如果还有任务
到来就会采取任务拒绝策略。
6、多线程中的安全队列一般通过什么实现?
Java 提供的线程安全的 Queue 可以分为阻塞队列和非阻塞队列,其中阻塞队列
的典型例子是 BlockingQueue,非阻塞队列的典型例子是
ConcurrentLinkedQueue.
对于 BlockingQueue,想要实现阻塞功能,需要调用 put(e) take() 方法。而
ConcurrentLinkedQueue 是基于链接节点的、无界的、线程安全的非阻塞队列。
二、Synchronized、volatile、Lock(ReentrantLock)相关 (⭐⭐⭐)
1、synchronized 的原理?
synchronized 代码块是由一对儿 monitorenter/monitorexit 指令实现的,
Monitor 对象是同步的基本实现,而 synchronized 同步方法使用了
ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行
相应的同步调用。
在 Java 6 之前,Monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要
进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。
现代的(Oracle)JDK 中,JVM 对此进行了大刀阔斧地改进,提供了三种不同
的 Monitor 实现,也就是常说的三种不同的锁:偏斜锁(Biased Locking)、
轻量级锁和重量级锁,大大改进了其性能。
所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检
测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、
降级。当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作,在对象头上
的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不
涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周
期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销
(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark
Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升
级为重量级锁(可能会先进行自旋锁升级,如果失败再尝试重量级锁升级)。
我注意到有的观点认为 Java 不会进行锁降级。实际上据我所知,锁降级确实是
会发生的,当 JVM 进入安全点(SafePoint)的时候,会检查是否有闲置的
Monitor,然后试图进行降级。
2、Synchronized 优化后的锁机制简单介绍一下,包括自旋锁、偏向锁、轻量级锁、重量级锁?
自旋锁:
线程自旋说白了就是让 cpu 在做无用功,比如:可以执行几次 for 循环,可以执
行几条空的汇编指令,目的是占着 CPU 不放,等待获取锁的机会。如果旋的时
间过长会影响整体性能,时间过短又达不到延迟阻塞的目的。
偏向锁
偏向锁就是一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,
之后的多次调用则可以避免 CAS 操作,说白了就是置个变量,如果发现为 true
则无需再走各种加锁/解锁流程。
轻量级锁:
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当
第二个线程加入锁竞争用的时候,偏向锁就会升级为轻量级锁;重量级锁
重量锁在 JVM 中又叫对象监视器(Monitor),它很像 C 中的 Mutex,除了具备
Mutex(0|1)互斥的功能,它还负责实现了 Semaphore(信号量)的功能,也就是说
它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait 队列),前者负责做
互斥,后一个用于做线程同步。
3、谈谈对 Synchronized 关键字涉及到的类锁,方法锁,重入锁的理解?
synchronized 修饰静态方法获取的是类锁(类的字节码文件对象)。
synchronized 修饰普通方法或代码块获取的是对象锁。这种机制确保了同一时刻
对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处
于可执行状态,从而有效避免了类成员变量的访问冲突。
它俩是不冲突的,也就是说:获取了类锁的线程和获取了对象锁的线程是不冲突
的!
public class Widget {
// 锁住了
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
// 锁住了
public synchronized void doSomething() {System.out.println(toString() + ": calling doSomething");
super.doSomething();
}
}
因为锁的持有者是“线程”,而不是“调用”。
线程 A 已经是有了 LoggingWidget 实例对象的锁了,当再需要的时候可以继续
**“开锁”**进去的!
这就是内置锁的可重入性。
4、wait、sleep 的区别和 notify 运行过程。
wait、sleep 的区别
最大的不同是在等待时 wait 会释放锁,而 sleep 一直持有锁。wait 通常被用
于线程间交互,sleep 通常被用于暂停执行。
首先,要记住这个差别,“sleep 是 Thread 类的方法,wait 是 Object 类中
定义的方法”。尽管这两个方法都会影响线程的执行行为,但是本质上是
有区别的。
Thread.sleep 不会导致锁行为的改变,如果当前线程是拥有锁的,那么
Thread.sleep 不会让线程释放锁。如果能够帮助你记忆的话,可以简单认
为和锁相关的方法都定义在 Object 类中,因此调用 Thread.sleep 是不会
影响锁的相关行为。
Thread.sleep 和 Object.wait 都会暂停当前的线程,对于 CPU 资源来说,
不管是哪种方式暂停的线程,都表示它暂时不再需要 CPU 的执行时间。OS 会将执行时间分配给其它线程。区别是,调用 wait 后,需要别的线程
执行 notify/notifyAll 才能够重新获得 CPU 执行时间。
线程的状态参考 Thread.State 的定义。新创建的但是没有执行(还没有
调用 start())的线程处于“就绪”,或者说 Thread.State.NEW 状态。
Thread.State.BLOCKED(阻塞)表示线程正在获取锁时,因为锁不能获取
到而被迫暂停执行下面的指令,一直等到这个锁被别的线程释放。
BLOCKED 状态下线程,OS 调度机制需要决定下一个能够获取锁的线程是
哪个,这种情况下,就是产生锁的争用,无论如何这都是很耗时的操作。
notify 运行过程
当线程 A(消费者)调用 wait()方法后,线程 A 让出锁,自己进入等待状态,同
时加入锁对象的等待队列。 线程 B(生产者)获取锁后,调用 notify 方法通知
锁对象的等待队列,使得线程 A 从等待队列进入阻塞队列。 线程 A 进入阻塞队
列后,直至线程 B 释放锁后,线程 A 竞争得到锁继续从 wait()方法后执行。
5、
synchronized 关键字和 Lock 的区别你知道吗?为什么 Lock 的性能好一些?
类别
synchronized
Lock(底层实现主要是 Volatile + CAS)
存在
层次
Java 的关键字,在 jvm 层面上
是一个类
锁的
释放
1、已获取锁的线程执行完同步代码,释放锁 2、线程
执行发生异常,jvm 会让线程释放锁。
在 finally 中必须释放锁,不然容易造成线程死锁。
锁的
获取
假设 A 线程获得锁,B 线程等待。如果 A 线程阻塞,B
分情况而定,Lock 有多个锁获取的方式,大致就是Lock(ReentrantLock)的底层实现主要是 Volatile + CAS(乐观锁),而
Synchronized 是一种悲观锁,比较耗性能。但是在 JDK1.6 以后对 Synchronized
的锁机制进行了优化,加入了偏向锁、轻量级锁、自旋锁、重量级锁,在并发量
不大的情况下,性能可能优于 Lock 机制。所以建议一般请求并发量不大的情况
下使用 synchronized 关键字。
6、volatile 原理。
在《Java 并发编程:核心理论》一文中,我们已经提到可见性、有序性及原子性
问题,通常情况下我们可以通过 Synchronized 关键字来解决这些个问题,不过
如果对 Synchonized 原理有了解的话,应该知道 Synchronized 是一个较重量级
的操作,对系统的性能有比较大的影响,所以如果有其他解决方案,我们通常都
避免使用 Synchronized 来解决问题。
而 volatile 关键字就是 Java 中提供的另一种解决可见性有序性问题的方案。对于
原子性,需要强调一点,也是大家容易误解的一点:对 volatile 变量的单次读/
写操作可保证原子性的,如 long 和 double 类型变量,但是并不能保证 i++这种
操作的原子性,因为本质上 i++是读、写两次操作。
volatile 也是互斥同步的一种实现,不过它非常的轻量级。
volatile 的意义?
线程会一直等待。
可以尝试获得锁,线程可以不用一直等待
锁状
态
无法判断
可以判断
锁类
型
可重入 不可中断 非公平
可重入 可判断 可公平(两者皆可)
性能
少量同步
大量同步
防止 CPU 指令重排序
volatile 有两条关键的语义:
保证被 volatile 修饰的变量对所有线程都是可见的
禁止进行指令重排序
要理解 volatile 关键字,我们得先从 Java 的线程模型开始说起。如图所示:
Java 内存模型规定了所有字段(这些字段包括实例字段、静态字段等,不包括局
部变量、方法参数等,因为这些是线程私有的,并不存在竞争)都存在主内存中,
每个线程会 有自己的工作内存,工作内存里保存了线程所使用到的变量在主内
存里的副本拷贝,线程对变量的操作只能在工作内存里进行,而不能直接读写主
内存,当然不同内存之间也 无法直接访问对方的工作内存,也就是说主内存是
线程传值的媒介。
我们来理解第一句话:
保证被 volatile 修饰的变量对所有线程都是可见的
如何保证可见性?
被 volatile 修饰的变量在工作内存修改后会被强制写回主内存,其他线程在使用
时也会强制从主内存刷新,这样就保证了一致性。
关于“保证被 volatile 修饰的变量对所有线程都是可见的”,有种常见的错误理解:
由于 volatile 修饰的变量在各个线程里都是一致的,所以基于 volatile 变
量的运算在多线程并发的情况下是安全的。
这句话的前半部分是对的,后半部分却错了,因此它忘记考虑变量的操作是否具
有原子性这一问题。
举个例子:
private volatile int start = 0;
private void volatile Keyword() {
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
start++;
}
}
};
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(runnable);
thread.start();
}
Log.d(TAG, "start = " + start);
}这段代码启动了 10 个线程,每次 10 次自增,按道理最终结果应该是 100,但是
结果并非如此。
为什么会这样?
仔细看一下 start++,它其实并非一个原子操作,简单来看,它有两步:
1、取出 start 的值,因为有 volatile 的修饰,这时候的值是正确的。
2、自增,但是自增的时候,别的线程可能已经把 start 加大了,这种情况下就有
可能把较小的 start 写回主内存中。 所以 volatile 只能保证可见性,在不符合以
下场景下我们依然需要通过加锁来保证原子性:
运算结果并不依赖变量当前的值,或者只有单一线程修改变量的值。(要
么结果不依赖当前值,要么操作是原子性的,要么只要一个线程修改变量
的值)
变量不需要与其他状态变量共同参与不变约束 比方说我们会在线程里加
个 boolean 变量,来判断线程是否停止,这种情况就非常适合使用
volatile。
我们再来理解第二句话。
禁止进行指令重排序
什么是指令重排序?
指令重排序是指指令乱序执行,即在条件允许的情况下直接运行当前有能
力立即执行的后续指令,避开为获取一条指令所需数据而造成的等待,通
过乱序执行的技术提供执行效率。
指令重排序会在被 volatile 修饰的变量的赋值操作前,添加一个内存屏障,
指令重排序时不能把后面的指令重排序移到内存屏障之前的位置。
7、synchronized 和 volatile 关键字的作用和区别。Volatile
1)保证了不同线程对这个变量进行操作时的可见性即一个线程修改了某个变量
的值,这新值对其他线程来是立即可见的。
2)禁止进行指令重排序。
作用
volatile 本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,
需从主存中读取;synchronized 则是锁定当前变量,只有当前线程可以访问该变
量,其它线程被阻塞住。
区别
1.volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法、和类
级别的。
2.volatile 仅能实现变量的修改可见性,并不能保证原子性;synchronized 则可
以保证变量的修改可见性和原子性。
3.volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
4.volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译
器优化。
8、ReentrantLock 的内部实现。
ReentrantLock 实现的前提就是 AbstractQueuedSynchronizer,简称 AQS,是
java.util.concurrent 的核心,CountDownLatch、FutureTask、Semaphore、
ReentrantLock 等都有一个内部类是这个抽象类的子类。由于 AQS 是基于 FIFO
队列的实现,因此必然存在一个个节点,Node 就是一个节点,Node 有两种模
式:共享模式和独占模式。ReentrantLock 是基于 AQS 的,AQS 是 Java 并发包中众多同步组件的构建基础,它通过一个 int 类型的状态变量 state 和一个 FIFO
队列来完成共享资源的获取,线程的排队等待等。AQS 是个底层框架,采用模
板方法模式,它定义了通用的较为复杂的逻辑骨架,比如线程的排队,阻塞,唤
醒等,将这些复杂但实质通用的部分抽取出来,这些都是需要构建同步组件的使
用者无需关心的,使用者仅需重写一些简单的指定的方法即可(其实就是对于共
享变量 state 的一些简单的获取释放的操作)。AQS 的子类一般只需要重写
tryAcquire(int arg)和 tryRelease(int arg)两个方法即可。
ReentrantLock 的处理逻辑:
其内部定义了三个重要的静态内部类,Sync,NonFairSync,FairSync。Sync 作
为 ReentrantLock 中公用的同步组件,继承了 AQS(要利用 AQS 复杂的顶层逻
辑嘛,线程排队,阻塞,唤醒等等);NonFairSync 和 FairSync 则都继承 Sync,
调用 Sync 的公用逻辑,然后再在各自内部完成自己特定的逻辑(公平或非公平)。
接着说下这两者的 lock()方法实现原理:
NonFairSync(非公平可重入锁)
1.先获取 state 值,若为 0,意味着此时没有线程获取到资源,CAS 将其设置为 1,
设置成功则代表获取到排他锁了;
2.若 state 大于 0,肯定有线程已经抢占到资源了,此时再去判断是否就是自己
抢占的,是的话,state 累加,返回 true,重入成功,state 的值即是线程重入的
次数;
3.其他情况,则获取锁失败。
FairSync(公平可重入锁)
可以看到,公平锁的大致逻辑与非公平锁是一致的,不同的地方在于有
了!hasQueuedPredecessors()这个判断逻辑,即便 state 为 0,也不能贸然直接去获取,要先去看有没有还在排队的线程,若没有,才能尝试去获取,做后面的处
理。反之,返回 false,获取失败。
最后,说下 ReentrantLock 的 tryRelease()方法实现原理:
若 state 值为 0,表示当前线程已完全释放干净,返回 true,上层的 AQS 会意识
到资源已空出。若不为 0,则表示线程还占有资源,只不过将此次重入的资源的
释放了而已,返回 false。
ReentrantLock 是一种可重入的,可实现公平性的互斥锁,它的设计基于 AQS
框架,可重入和公平性的实现逻辑都不难理解,每重入一次,state 就加 1,当
然在释放的时候,也得一层一层释放。至于公平性,在尝试获取锁的时候多了一
个判断:是否有比自己申请早的线程在同步队列中等待,若有,去等待;若没有,
才允许去抢占。
9、ReentrantLock 、synchronized 和 volatile 比较?
synchronized 是互斥同步的一种实现。
synchronized:当某个线程访问被 synchronized 标记的方法或代码块时,这个
线程便获得了该对象的锁,其他线暂时无法访问这个方法,只有等待这个方法执
行完毕或代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这
个方法代码块。
前面我们已经说了 volatile 关键字,这里我们举个例子来综合分析 volatile 与
synchronized 关键字的使用。
举个例子:
public class Singleton {
// volatile 保证了:1 instance 在多线程并发的可见性 2 禁止 instance 在操作是的
指令重排序private volatile static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
// 第一次判空,保证不必要的同步
if (instance == null) {
// synchronized 对 Singleton 加全局锁,保证每次只要一个线程创建实例
synchronized (Singleton.class) {
// 第二次判空时为了在 null 的情况下创建实例
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这是一个经典的 DCL 单例。
它的字节码如下:可以看到被 synchronized 同步的代码块,会在前后分别加上 monitorenter 和
monitorexit,这两个字节码都需要指定加锁和解锁的对象。
关于加锁和解锁的对象:
synchronized 代码块 :同步代码块,作用范围是整个代码块,作用对象是调用
这个代码块的对象。
synchronized 方法 :同步方法,作用范围是整个方法,作用对象是调用这个方
法的对象。
synchronized 静态方法 :同步静态方法,作用范围是整个静态方法,作用对象
是调用这个类的所有对象。synchronized(this):作用范围是该对象中所有被 synchronized 标记的变量、方
法或代码块,作用对象是对象本身。
synchronized(ClassName.class) :作用范围是静态的方法或者静态变量,作用对
象是 Class 对象。
synchronized(this)添加的是对象锁,synchronized(ClassName.class)添加的是类
锁,它们的区别如下:
对象锁:Java 的所有对象都含有 1 个互斥锁,这个锁由 JVM 自动获取和
释放。线程进入 synchronized 方法的时候获取该对象的锁,当然如果已
经有线程获取了这个对象的锁那么当前线程会等待;
synchronized 方法正
常返回或者抛异常而终止,JVM 会自动释放对象锁。这里也体现了用
synchronized 来加锁的好处,方法抛异常的时候,锁仍然可以由 JVM 来
自动释放。
类锁:对象锁是用来控制实例方法之间的同步,类锁是来控制静态方法(或
静态变量互斥体)之间的同步。其实类锁只是一个概念上的东西,并不是
真实存在的,它只用来帮助我们理解锁定实例方法和静态方法的区别的。
我们都知道,java 类可能会有很多个对象,但是只有 1 个 Class 对象,也
就说类的不同实例之间共享该类的 Class 对象。Class 对象其实也仅仅是 1
个 java 对象,只不过有点特殊而已。由于每个 java 对象都有个互斥锁,
而类的静态方法是需要 Class 对象。所以所谓类锁,不过是 Class 对象的
锁而已。获取类的 Class 对象有好几种,最简单的就是 MyClass.class 的方
式。类锁和对象锁不是同一个东西,一个是类的 Class 对象的锁,一个是
类的实例的锁。也就是说:一个线程访问静态 sychronized 的时候,允许
另一个线程访问对象的实例 synchronized 方法。反过来也是成立的,为
他们需要的锁是不同的。
三、其它 (⭐⭐⭐)
1、多线程的使用场景?
使用多线程就一定效率高吗?有时候使用多线程并不是为了提高效率,而是使得
CPU 能同时处理多个事件。
为了不阻塞主线程,启动其他线程来做事情,比如 APP 中的耗时操作都不在
UI 线程中做。
实现更快的应用程序,即主线程专门监听用户请求,子线程用来处理用户请
求,以获得大的吞吐量.感觉这种情况,多线程的效率未必高。这种情况下
的多线程是为了不必等待,可以并行处理多条数据。比如 JavaWeb 的就
是主线程专门监听用户的 HTTP 请求,然启动子线程去处理用户的 HTTP
请求。
某种虽然优先级很低的服务,但是却要不定时去做。比如 Jvm 的垃圾回
收。
某种任务,虽然耗时,但是不消耗 CPU 的操作时间,开启个线程,效率
会有显著提高。比如读取文件,然后处理。磁盘 IO 是个很耗费时间,但
是不耗 CPU 计算的工作。所以可以一个线程读取数据,一个线程处理数
据。肯定比一个线程读取数据,然后处理效率高。因为两个线程的时候充
分利用了 CPU 等待磁盘 IO 的空闲时间。
2、CopyOnWriteArrayList 的了解。
Copy-On-Write 是什么?
在计算机中就是当你想要对一块内存进行修改时,我们不在原有内存块中进行写
操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向
原来内存指针指向新的内存,原来的内存就可以被回收掉。原理:
CopyOnWriteArrayList 这是一个 ArrayList 的线程安全的变体,
CopyOnWriteArrayList 底层实现添加的原理是先 copy 出一个容器(可以简称副
本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给了
之前那个旧的的容器地址,但是在添加这个数据的期间,其他线程如果要去读取
数据,仍然是读取到旧的容器里的数据。
优点和缺点:
优点:
1.据一致性完整,为什么?因为加锁了,并发数据不会乱。
2.解决了像 ArrayList、Vector 这种集合多线程遍历迭代问题,记住,Vector 虽然
线程安全,只不过是加了 synchronized 关键字,迭代问题完全没有解决!
缺点:
1.内存占有问题:很明显,两个数组同时驻扎在内存中,如果实际应用中,数据比
较多,而且比较大的情况下,占用内存会比较大,针对这个其实可以用
ConcurrentHashMap 来代替。
2.数据一致性:CopyOnWrite 容器只能保证数据的最终一致性,不能保证数据的
实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用
CopyOnWrite 容器。
使用场景:
1、读多写少(白名单,黑名单,商品类目的访问和更新场景),为什么?因为
写的时候会复制新集合。2、集合不大,为什么?因为写的时候会复制新集合。
3、实时性要求不高,为什么,因为有可能会读取到旧的集合数据。
3、ConcurrentHashMap 加锁机制是什么,详细说一下?
Java7 ConcurrentHashMap
ConcurrentHashMap 作为一种线程安全且高效的哈希表的解决方案,尤其其中
的"分段锁"的方案,相比 HashTable 的表锁在性能上的提升非常之大。HashTable
容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问
HashTable 的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于
锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间
就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是
ConcurrentHashMap 所使用的锁分段技术,首先将数据分成一段一段的存储,
然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其
他段的数据也能被其他线程访问。
ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承
ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,
这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
concurrencyLevel:并行级别、并发数、Segment 数。默认是 16,也就是说
ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以
同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。
这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩
容的。其中的每个 Segment 很像 HashMap,不过它要保证线程安全,所以处
理起来要麻烦些。
初始化槽: ensureSegmentConcurrentHashMap 初始化的时候会初始化第一个槽 segment[0],对于其他槽
来说,在插入第一个值的时候进行初始化。对于并发操作使用 CAS 进行控制。
Java8 ConcurrentHashMap
抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安
全性。结构上和 Java8 的 HashMap(数组+链表+红黑树) 基本上一样,不过
它要保证线程安全性,所以在源码上确实要复杂一些。1.8 在 1.7 的数据结构上
做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了
ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对
synchronized 优化是很到位的。
4、线程死锁的 4 个条件?
死锁是如何发生的,如何避免死锁?
当线程 A 持有独占锁 a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,
并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的
锁,而发生的阻塞现象,我们称为死锁。
public class DeadLockDemo {
public static void main(String[] args) {
// 线程 a
Thread td1 = new Thread(new Runnable() {
public void run() {
DeadLockDemo.method1();
}
});// 线程 b
Thread td2 = new Thread(new Runnable() {
public void run() {
DeadLockDemo.method2();
}
});
td1.start();
td2.start();
}
public static void method1() {
synchronized (String.class) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 a 尝试获取 integer.class");
synchronized (Integer.class) {
}
}
}public static void method2() {
synchronized (Integer.class) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 b 尝试获取 String.class");
synchronized (String.class) {
}
}
}
}
造成死锁的四个条件:
互斥条件:一个资源每次只能被一个线程使用。
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不
放。
不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
在并发程序中,避免了逻辑中出现数个线程互相持有对方线程所需要的独占锁的
的情况,就可以避免死锁,如下所示:
public class BreakDeadLockDemo {
public static void main(String[] args) {
// 线程 a
Thread td1 = new Thread(new Runnable() {public void run() {
DeadLockDemo2.method1();
}
});
// 线程 b
Thread td2 = new Thread(new Runnable() {
public void run() {
DeadLockDemo2.method2();
}
});
td1.start();
td2.start();
}
public static void method1() {
synchronized (String.class) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 a 尝试获取 integer.class");
synchronized (Integer.class) {
System.out.println("线程 a 获取到 integer.class");}
}
}
public static void method2() {
// 不再获取线程 a 需要的 Integer.class 锁。
synchronized (String.class) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 b 尝试获取 Integer.class");
synchronized (Integer.class) {
System.out.println("线程 b 获取到 Integer.class");
}
}
}
}
5、CAS 介绍?
UnsafeUnsafe 是 CAS 的核心类。因为 Java 无法直接访问底层操作系统,而是通过本地
(native)方法来访问。不过尽管如此,JVM 还是开了一个后门,JDK 中有一个
类 Unsafe,它提供了硬件级别的原子操作。
CAS
CAS,Compare and Swap 即比较并交换,设计并发算法时常用到的一种技术,
java.util.concurrent 包全完建立在 CAS 之上,没有 CAS 也就没有此包,可见 CAS
的重要性。当前的处理器基本都支持 CAS,只不过不同的厂家的实现不一样罢了。
并且 CAS 也是通过 Unsafe 实现的,由于 CAS 都是硬件级别的操作,因此效率
会比普通加锁高一些。
CAS 的缺点
CAS 看起来很美,但这种操作显然无法涵盖并发下的所有场景,并且 CAS 从语
义上来说也不是完美的,存在这样一个逻辑漏洞:如果一个变量 V 初次读取的
时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它
的值没有被其他线程修改过了吗?如果在这段期间它的值曾经被改成了 B,然后
又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个漏洞称为 CAS 操
作的"ABA"问题。java.util.concurrent 包为了解决这个问题,提供了一个带有标
记的原子引用类"AtomicStampedReference",它可以通过控制变量值的版本来
保证 CAS 的正确性。不过目前来说这个类比较"鸡肋",大部分情况下 ABA 问题
并不会影响程序并发的正确性,如果需要解决 ABA 问题,使用传统的互斥同步
可能回避原子类更加高效。
6、进程和线程的区别?
简而言之,一个程序至少有一个进程,一个进程至少有一个线程。1、线程的划分尺度小于进程,使得多线程程序的并发性高。
2、进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而
极大地提高了程序的运行效率。
3、线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序
运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必
须依存在应用程序中,由应用程序提供多个线程执行控制。
4、从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部
分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来
实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
5、进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,
进程是系统进行资源分配和调度的一个独立单位。线程是进程的一个实体,
是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.
线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如
程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共
享进程所拥有的全部资源。
6、一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可
以并发执行。
7、进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它
进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆
栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整
个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,
耗费资源较大,效率要差一些。
7、什么导致线程阻塞?
线程的阻塞为了解决对共享存储区的访问冲突,Java 引入了同步机制,现在让我们来考察
多个线程对共享资源的访问,显然同步机制已经不够了,因为在任意时刻所要求
的资源不一定已经准备好了被访问,反过来,同一时刻准备好了的资源也可能不
止一个。为了解决这种情况下的访问控制问题,Java 引入了对阻塞机制的支持.
阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪),学过操
作系统的同学对它一定已经很熟悉了。Java 提供了大量方法来支持阻塞,下面
让我们逐一分析。
sleep() 方法:sleep() 允许 指定以毫秒为单位的一段时间作为参数,它使得线
程在指定的时间内进入阻塞状态,不能得到 CPU 时间,指定的时间一过,线程
重新进入可执行状态。 典型地,sleep() 被用在等待某个资源就绪的情形:测试
发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止。
suspend() 和 resume() 方法:两个方法配套使用,suspend()使得线程进入阻塞
状态,并且不会自动恢复,必须其对应的 resume() 被调用,才能使得线程重新
进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生
的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结
果后,调用 resume() 使其恢复。
yield() 方法:yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,
即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果
等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。
wait() 和 notify() 方法:两个方法配套使用,wait() 使得线程进入阻塞状态,它
有两种形式,一种允许指定以毫秒为单位的一段时间作为参数,另一种没有参数,
前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,
后者则必须对应的 notify() 被调用。初看起来它们与 suspend() 和 resume()
方法对没有什么分别,但是事实上它们是截然不同的。区别的核心在于,前面叙述的所有方法,阻塞时都不会释放占用的锁(如果占用了的话),而这一对方法
则相反。
上述的核心区别导致了一系列的细节上的区别。
首先,前面叙述的所有方法都隶属于 Thread 类,但是这一对却直接隶属于
Object 类,也就是说,所有对象都拥有这一对方法。初看起来这十分不可思议,
但是实际上却是很自然的,因为这一对方法阻塞时要释放占用的锁,而锁是任何
对象都具有的,调用任意对象的 wait() 方法导致线程阻塞,并且该对象上的锁
被释放。而调用 任意对象的 notify()方法则导致因调用该对象的 wait() 方法而
阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。
其次,前面叙述的所有方法都可在任何位置调用,但是这一对方法却必须在
synchronized 方法或块中调用,理由也很简单,只有在 synchronized 方法或块
中当前线程才占有锁,才有锁可以释放。同样的道理,调用这一对方法的对象上
的锁必须为当前线程所拥有,这样才有锁可以释放。因此,这一对方法调用必须
放置在这样的 synchronized 方法或块中,该方法或块的上锁对象就是调用这一
对方法的对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现
IllegalMonitorStateException 异常。
wait() 和 notify() 方法的上述特性决定了它们经常和 synchronized 方法或块一
起使用,将它们和操作系统的进程间通信机制作一个比较就会发现它们的相似
性:synchronized 方法或块提供了类似于操作系统原语的功能,它们的执行不会
受到多线程机制的干扰,而这一对方法则相当于 block 和 wakeup 原语(这一
对方法均声明为 synchronized)。它们的结合使得我们可以实现操作系统上一
系列精妙的进程间通信的算法(如信号量算法),并用于解决各种复杂的线程间
通信问题。(此外,线程间通信的方式还有多个线程通过 synchronized 关键字
这种方式来实现线程间的通信、while 轮询、使用 java.io.PipedInputStream 和
java.io.PipedOutputStream 进行通信的管道通信)。
关于 wait() 和 notify() 方法最后再说明两点:第一:调用 notify() 方法导致解除阻塞的线程是从调用该对象的 wait() 方法而
阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要
特别小心,避免因这种不确定性而产生问题。
第二:除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别
在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程
一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。
谈到阻塞,就不能不谈一谈死锁,略一分析就能发现,suspend() 方法和不指定
超时期限的 wait() 方法的调用都可能产生死锁。遗憾的是,Java 并不在语言级
别上支持死锁的避免,我们在编程中必须小心地避免死锁。
以上我们对 Java 中实现线程阻塞的各种方法作了一番分析,我们重点分析了
wait() 和 notify() 方法,因为它们的功能最强大,使用也最灵活,但是这也导致
了它们的效率较低,较容易出错。实际使用中我们应该灵活使用各种方法,以便
更好地达到我们的目的。
8、线程的生命周期
线程状态流程图 NEW:创建状态,线程创建之后,但是还未启动。
RUNNABLE:运行状态,处于运行状态的线程,但有可能处于等待状态,
例如等待 CPU、IO 等。
WAITING:等待状态,一般是调用了 wait()、join()、LockSupport.spark()
等方法。
TIMED_WAITING:超时等待状态,也就是带时间的等待状态。一般是调
用 了 wait(time) 、 join(time) 、 LockSupport.sparkNanos() 、
LockSupport.sparkUnit()等方法。
BLOCKED:阻塞状态,等待锁的释放,例如调用了 synchronized 增加了
锁。
TERMINATED:终止状态,一般是线程完成任务后退出或者异常终止。NEW、WAITING、TIMED_WAITING 都比较好理解,我们重点说一说 RUNNABLE
运行态和 BLOCKED 阻塞态。
线程进入 RUNNABLE 运行态一般分为五种情况:
线程调用 sleep(time)后结束了休眠时间
线程调用的阻塞 IO 已经返回,阻塞方法执行完毕
线程成功的获取了资源锁
线程正在等待某个通知,成功的获得了其他线程发出的通知
线程处于挂起状态,然后调用了 resume()恢复方法,解除了挂起。
线程进入 BLOCKED 阻塞态一般也分为五种情况:
线程调用 sleep()方法主动放弃占有的资源
线程调用了阻塞式 IO 的方法,在该方法返回前,该线程被阻塞。
线程视图获得一个资源锁,但是该资源锁正被其他线程锁持有。
线程正在等待某个通知
线程调度器调用 suspend()方法将该线程挂起
我们再来看看和线程状态相关的一些方法。
sleep()方法让当前正在执行的线程在指定时间内暂停执行,正在执行的线
程可以通过 Thread.currentThread()方法获取。
yield()方法放弃线程持有的 CPU 资源,将其让给其他任务去占用 CPU 执
行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得 CPU 时间
片。wait()方法是当前执行代码的线程进行等待,将当前线程放入预执行队列,
并在 wait()所在的代码处停止执行,直到接到通知或者被中断为止。该方
法可以使得调用该方法的线程释放共享资源的锁,然后从运行状态退出,
进入等待队列,直到再次被唤醒。该方法只能在同步代码块里调用,否则
会抛出 IllegalMonitorStateException 异常。wait(long millis)方法等待某
一段时间内是否有线程对锁进行唤醒,如果超过了这个时间则自动唤醒。
notify()方法用来通知那些可能等待该对象的对象锁的其他线程,该方法
可以随机唤醒等待队列中等同一共享资源的一个线程,并使该线程退出等
待队列,进入可运行状态。
notifyAll()方法可以使所有正在等待队列中等待同一共享资源的全部线程
从等待状态退出,进入可运行状态,一般会是优先级高的线程先执行,但
是根据虚拟机的实现不同,也有可能是随机执行。
join()方法可以让调用它的线程正常执行完成后,再去执行该线程后面的
代码,它具有让线程排队的作用。
9、乐观锁与悲观锁。
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数
据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每
次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。Java
中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,
但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版
本号机制和 CAS 算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐
量。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观
锁的一种实现方式 CAS 实现的。使用场景
乐观锁适用于写比较少的情况下(多读场景),而一般多写的场景下用悲观锁就
比较合适。
乐观锁常见的两种实现方式
1、版本号机制
一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数,
当数据被修改时,version 值会加 1。当线程 A 要更新数据值时,在读取数据的
同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数
据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
2、CAS 算法
即 compare and swap(比较与交换),是一种有名的无锁算法。CAS 有 3 个操
作数,内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值
V 相同时,将内存值 V 修改为 B,否则什么都不做。 一般情况下是一个自旋操
作,即不断的重试。
乐观锁的缺点
1、ABA 问题
如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然
是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,
因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误
认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。JDK 1.5 以后的 AtomicStampedReference 类一定程度上解决了这个问题,其
中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前
标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设
置为给定的更新值。
2、自旋 CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会
给 CPU 带来非常大的执行开销。
3、CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但
是从 JDK 1.5 开始,提供了 AtomicReference 类来保证引用对象之间的原子性,
你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者
利用 AtomicReference 类把多个共享变量合并成一个共享变量来操作。
10、run()和 start()方法区别?
1.start()方法来启动线程,真正实现了多线程运行,这时无需等待 run 方法体代
码执行完毕而直接继续执行下面的代码:
通过调用Thread类的 start()方法来启动一个线程,这时此线程是处于就绪状态,
并没有运行。 然后通过此 Thread 类调用方法 run()来完成其运行操作的, 这里
方法 run()称为线程体, 它包含了要执行的这个线程的内容, Run 方法运行结
束, 此线程终止, 而 CPU 再运行其它线程,在 Android 中一般是主线程。
2.run()方法当作普通方法的方式调用,程序还是要顺序执行,还是要等待 run 方
法体执行完毕后才可继续执行下面的代码:
而如果直接用 Run 方法, 这只是调用一个方法而已, 程序中依然只有主线程
--这一个线程,其程序执行路径还是只有一条,这样就没有达到写线程的目的。
11、多线程断点续传原理。在本地下载过程中要使用数据库实时存储到底存储到文件的哪个位置了,这样点
击开始继续传递时,才能通过 HTTP 的 GET 请求中的
setRequestProperty("Range","bytes=startIndex-endIndex");方法可以告诉服务
器,数据从哪里开始,到哪里结束。同时在本地的文件写入时,RandomAccessFile
的 seek()方法也支持在文件中的任意位置进行写入操作。同时通过广播或事件总
线机制将子线程的进度告诉 Activity 的进度条。关于断线续传的 HTTP 状态码是
206,即 HttpStatus.SC_PARTIAL_CONTENT。
12、怎么安全停止一个线程任务?原理是什么?线程池里有类似机制吗?
终止线程
1、使用 violate boolean 变量退出标志,使线程正常退出,也就是当 run 方法完
成后线程终止。(推荐)
2、使用 interrupt()方法中断线程,但是线程不一定会终止。
3、使用 stop 方法强行终止线程。不安全主要是:thread.stop()调用之后,创建
子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的
所有锁。
终止线程池
ExecutorService 线程池就提供了 shutdown 和 shutdownNow 这样的生命周期方
法来关闭线程池自身以及它拥有的所有线程。
1、shutdown 关闭线程池
线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。
2、shutdownNow 关闭线程池并中断任务终止等待执行的线程,并返回它们的列表。试图停止所有正在执行的线程,试图
终止的方法是调用 Thread.interrupt(),但是大家知道,如果线程中没有 sleep 、
wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,
ShutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有
正在执行的任务都执行完成了才能退出。
13、堆内存,栈内存理解,栈如何转换成堆?
在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈
内存中分配。
堆内存用于存放由 new 创建的对象和数组。JVM 里的“堆”(heap)特指
用于存放 Java 对象的内存区域。所以根据这个定义,Java 对象全部都在
堆上。JVM 的堆被同一个 JVM 实例中的所有 Java 线程共享。它通常由某
种自动内存管理机制所管理,这种机制通常叫做“垃圾回收”(garbage
collection,GC)。
堆主要用来存放对象的,栈主要是用来执行程序的。
实际上,栈中的变量指向堆内存中的变量,这就是 Java 中的指针!
14、如何控制某个方法允许并发访问线程的个数;
15、多进程开发以及多进程应用场景;
16、Java 的线程模型;
17、死锁的概念,怎么避免死锁?
18、如何保证多线程读写文件的安全?
19、线程如何关闭,以及如何防止线程的内存泄漏?
20、为什么要有线程,而不是仅仅用进程?
21、多个线程如何同时请求,返回的结果如何等待所有线程数据完成后合成一个数据?
22、线程如何关闭?23、数据一致性如何保证?
24、两个进程同时要求写或者读,能不能实现?如何防止进程的同步?
25、谈谈对多线程的理解并举例说明
26、线程的状态和优先级。
27、ThreadLocal 的使用
28、Java 中的并发工具(CountDownLatch,CyclicBarrier 等)
29、进程线程在操作系统中的实现
30、双线程通过线程同步的方式打印 12121212.......
31、java 线程,场景实现,多个线程如何同时请求,返回的结果如何等待所有线程数据完成后
合成一个数据
32、服务器只提供数据接收接口,在多线程或多进程条件下,如何保证数据的有序到达?
33、单机上一个线程池正在处理服务,如果忽然断电了怎么办(正在处理和阻塞队列里的请求
怎么处理)?
第三节 Java 虚拟机面试题 (⭐⭐⭐)
1、JVM 内存区域。JVM 基本构成
从上图可知,JVM 主要包括四个部分:
1.类加载器(ClassLoader):在 JVM 启动时或者在类运行将需要的 class 加载到
JVM 中。(下图表示了从 java 源文件到 JVM 的整个过程,可配合理解。2.执行引擎:负责执行 class 文件中包含的字节码指令;
3.内存区(也叫运行时数据区):是在 JVM 运行的时候操作所分配的内存区。
运行时内存区主要可以划分为 5 个区域,如图:方法区(MethodArea):用于存储类结构信息的地方,包括常量池、静态常量、
构造函数等。虽然 JVM 规范把方法区描述为堆的一个辑部分, 但它却有个别名
non-heap(非堆),所以大家不要搞混淆了。方法区还包含一个运行时常量池。
java 堆(Heap):存储 java 实例或者对象的地方。这块是 GC 的主要区域。从存储
的内容我们可以很容易知道,方法和堆是被所有 java 线程共享的。
java 栈(Stack):java 栈总是和线程关联在一起,每当创一个线程时,JVM 就会为
这个线程创建一个对应的 java 栈在这个 java 栈中,其中又会包含多个栈帧,每运
行一个方法就建一个栈帧,用于存储局部变量表、操作栈、方法返回等。每一个
方法从调用直至执行完成的过程,就对应一栈帧在 java 栈中入栈到出栈的过程。
所以 java 栈是现成有的。程序计数器(PCRegister):用于保存当前线程执行的内存地址。由于 JVM 程序是
多线程执行的(线程轮流切换),所以为了保证程切换回来后,还能恢复到原先
状态,就需要一个独立计数器,记录之前中断的地方,可见程序计数器也是线程
私有的。
本地方法栈(Native MethodStack):和 java 栈的作用差不多,只不过是为 JVM 使
用到 native 方法服务的。
4.本地方法接口:主要是调用 C 或 C++实现的本地方法及回调结果。
开线程影响哪块内存?
每当有线程被创建的时候,JVM 就需要为其在内存中分配虚拟机栈和本地方法
栈来记录调用方法的内容,分配程序计数器记录指令执行的位置,这样的内存消
耗就是创建线程的内存代价。
2、JVM 的内存模型的理解?
Java内存模型即Java Memory Model,简称JMM。
JMM定义了Java 虚拟机(JVM)
在计算机内存(RAM)中的工作方式。JVM 是整个计算机虚拟模型,所以 JMM 是
隶属于 JVM 的。
Java 线程之间的通信总是隐式进行,并且采用的是共享内存模型。这里提到的共
享内存模型指的就是 Java 内存模型(简称 JMM),JMM 决定一个线程对共享变量
的写入何时对另一个线程可见。从抽象的角度来看,JMM 定义了线程和主内存
之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每
个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以
读/写共享变量的副本。本地内存是 JMM 的一个抽象概念,并不真实存在。它涵
盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。总之,JMM 就是一组规则,这组规则意在解决在并发编程可能出现的线程安全
问题,并提供了内置解决方案(happen-before 原则)及其外部可使用的同步手
段(synchronized/volatile 等),确保了程序执行在多线程环境中的应有的原子性,
可视性及其有序性。
需要更全面理解建议阅读以下文章:
全面理解 Java 内存模型(JMM)及 volatile 关键字
全面理解 Java 内存模型
3、描述一下 GC 的原理和回收策略?
提到垃圾回收,我们可以先思考一下,如果我们去做垃圾回收需要解决哪些问
题?
一般说来,我们要解决三个问题:
1、回收哪些内存?
2、什么时候回收?
3、如何回收?
这些问题分别对应着引用管理和回收策略等方案。
提到引用,我们都知道 Java 中有四种引用类型:
强引用:代码中普遍存在的,只要强引用还存在,垃圾收集器就不会回收
掉被引用的对象。
软引用:SoftReference,用来描述还有用但是非必须的对象,当内存不足
的时候会回收这类对象。
弱引用:WeakReference,用来描述非必须对象,弱引用的对象只能生存
到下一次 GC 发生时,当 GC 发生时,无论内存是否足够,都会回收该对
象。
虚引用:PhantomReference,一个对象是否有虚引用的存在,完全不会
对其生存时间产生影响,也无法通过虚引用取得一个对象的引用,它存在
的唯一目的是在这个对象被回收时可以收到一个系统通知。
不同的引用类型,在做 GC 时会区别对待,我们平时生成的 Java 对象,默认都
是强引用,也就是说只要强引用还在,GC 就不会回收,那么如何判断强引用是
否存在呢?
一个简单的思路就是:引用计数法,有对这个对象的引用就+1,不再引用就-1,
但是这种方式看起来简单美好,但它却不能解决循环引用计数的问题。
因此可达性分析算法登上历史舞台,用它来判断对象的引用是否存在。
可达性分析算法通过一系列称为 GCRoots 的对象作为起始点,从这些节点从上
向下搜索,所走过的路径称为引用链,当一个对象没有任何引用链与 GCRoots
连接时就说明此对象不可用,也就是对象不可达。
GC Roots 对象通常包括:
虚拟机栈中引用的对象(栈帧中的本地变量表)
方法中类的静态属性引用的对象
方法区中常量引用的对象
Native 方法引用的对象
可达性分析算法整个流程如下所示:
第一次标记:对象在经过可达性分析后发现没有与 GC Roots 有引用链,则进行
第一次标记并进行一次筛选,筛选条件是:该对象是否有必要执行 finalize()方法。
没有覆盖 finalize()方法或者 finalize()方法已经被执行过都会被认为没有必要执行。 如果有必要执行:则该对象会被放在一个 F-Queue 队列,并稍后在由虚拟
机建立的低优先级 Finalizer 线程中触发该对象的 finalize()方法,但不保证一定等
待它执行结束,因为如果这个对象的 finalize()方法发生了死循环或者执行时间较
长的情况,会阻塞 F-Queue 队列里的其他对象,影响 GC。
第二次标记:GC 对 F-Queue 队列里的对象进行第二次标记,如果在第二次标记
时该对象又成功被引用,则会被移除即将回收的集合,否则会被回收。
总之,JVM 在做垃圾回收的时候,会检查堆中的所有对象否会被这些根集对象
引用,不能够被引用的对象就会被圾收集器回收。一般回收算法也有如下几种:
1).标记-清除(Mark-sweep)
标记-清除算法采用从根集合进行扫描,对存活的对象进行标记,标记完毕后,
再扫描整个空间中未被标记的对象,进行回收。标记-清除算法不需要进行对象
的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,
但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
2).标记-整理(Mark-Compact)
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不
同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移
动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了
对象的移动,因此成本更高,但是却解决了内存碎片的问题。该垃圾回收算法适
用于对象存活率高的场景(老年代)。
3).复制(Copying)
复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已
使用过的内存空间一次清理掉。这种算法适用于对象存活率低的场景,比如新生代。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存
碎片等复杂情况。
4).分代收集算法
不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中
不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的
执行效率。当代商用虚拟机使用的都是分代收集算法:新生代对象存活率低,就
采用复制算法;老年代存活率高,就用标记清除算法或者标记整理算法。Java
堆内存一般可以分为新生代、老年代和永久代三个模块:
新生代:
1.所有新生成的对象首先都是放在新生代的。新生代的目标就是尽可能快速的收
集掉那些生命周期短的对象。
2.新生代内存按照 8:1:1 的比例分为一个 eden 区和两个
survivor(survivor0,survivor1)区。大部分对象在 Eden 区中生成。回收时先将 eden
区存活对象复制到一个 survivor0 区,然后清空 eden 区,当这个 survivor0 区也
存放满了时,则将 eden 区和 survivor0 区存活对象复制到另一个 survivor1 区,
然后清空 eden 和这个 survivor0 区,此时 survivor0 区是空的,然后将 survivor0
区和 survivor1 区交换,即保持 survivor1 区为空, 如此往复。
3.当 survivor1 区不足以存放 eden 和 survivor0 的存活对象时,就将存活对象直
接存放到老年代。若是老年代也满了就会触发一次 Full GC,也就是新生代、老
年代都进行回收。
4.新生代发生的 GC 也叫做 Minor GC,MinorGC 发生频率比较高(不一定等 Eden
区满了才触发)。老年代:
1.在老年代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到老年代中。因
此,可以认为老年代中存放的都是一些生命周期较长的对象。
2.内存比新生代也大很多(大概比例是 1:2),当老年代内存满时触发 Major GC,
即 Full GC。Full GC 发生频率比较低,老年代对象存活时间比较长。
永久代:
永久代主要存放静态文件,如 Java 类、方法等。永久代对垃圾回收没有显著影
响,但是有些应用可能动态生成或者调用一些 class,例如使用反射、动态代理、
CGLib 等 bytecode 框架时,在这种时候需要设置一个比较大的永久代空间来存
放这些运行过程中新增的类。
垃圾收集器
垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现:
Serial 收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,
优点是简单高效;
Serial Old 收集器 (标记-整理算法): 老年代单线程收集器,Serial 收集器
的老年代版本;
ParNew 收集器 (复制算法): 新生代收并行集器,实际上是 Serial 收集器
的多线程版本,在多核 CPU 环境下有着比 Serial 更好的表现;
CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行
收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿
的特点,追求最短 GC 回收停顿时间。Parallel Old 收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,
Parallel Scavenge 收集器的老年代版本;
Parallel Scavenge 收集器 (复制算法): 新生代并行收集器,追求高吞吐量,
高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC 线程时间),
高吞吐量可以高效率的利用 CPU 时间,尽快完成程序的运算任务,适合
后台应用等对交互相应要求不高的场景;
G1(Garbage First)收集器 (标记-整理算法): Java 堆并行收集器,G1 收
集器是 JDK1.7 提供的一个新收集器,G1 收集器基于“标记-整理”算法实
现,也就是说不会产生内存碎片。此外,G1 收集器不同于之前的收集器
的一个重要特点是:G1 回收的范围是整个 Java 堆(包括新生代,老年代),
而前六种收集器回收的范围仅限于新生代或老年代。
内存分配和回收策略
JAVA 自动内存管理:给对象分配内存 以及 回收分配给对象的内存。
1、对象优先在 Eden 分配,当 Eden 区没有足够空间进行分配时,虚拟机将发起
一次 MinorGC。
2、大对象直接进入老年代。如很长的字符串以及数组。很长的字符串以及数组。
3、长期存活的对象将进入老年代。当对象在新生代中经历过一定次数(默认为
15)的 Minor GC 后,就会被晋升到老年代中。
4、动态对象年龄判定。为了更好地适应不同程序的内存状况,虚拟机并不是永
远地要求对象年龄必须达到了 MaxTenuringThreshold 才能晋升老年代,如果在Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大
于或等于该年龄的对象就可以直接进入老年代,无须等到
MaxTenuringThreshold 中要求的年龄。
需要更全面的理解请点击这里
4、类的加载器,双亲机制,Android 的类加载器。
类的加载器
大家都知道,一个 Java 程序都是由若干个.class 文件组织而成的一个完整的 Java
应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关
功能,而这些功能都被封装在不同的 class 文件当中,所以经常要从这个 class
文件中要调用另外一个 class 文件中的方法,如果另外一个文件不存在的话,则
会引发系统异常。
而程序在启动的时候,并不会一次性加载程序所要用到的 class 文件,而是根据
程序的需要,通过 Java 的类加载制(ClassLoader)来动态加载某个 class 文件
到内存当的,从而只有 class 文件被载入到了内存之后,才能被其它 class 文件
所引用。所以 ClassLoader 就是用来动态加载 class 件到内存当中用的。
双亲机制
类的加载就是虚拟机通过一个类的全限定名来获取描述此类的二进制字节流,而
完成这个加载动作的就是类加载器。
类和类加载器息息相关,判定两个类是否相等,只有在这两个类被同一个类加载
器加载的情况下才有意义,否则即便是两个类来自同一个 Class 文件,被不同类
加载器加载,它们也是不相等的。
注:这里的相等性保函 Class 对象的 equals()方法、isAssignableFrom()方法、
isInstance()方法的返回结果以及 Instance 关键字对对象所属关系的判定结果等。
类加载器可以分为三类:启动类加载器(Bootstrap ClassLoader):负责加载<JAVA_HOME>\lib
目录下或者被-Xbootclasspath 参数所指定的路径的,并且是被虚拟机所
识别的库到内存中。
扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext
目录下或者被 java.ext.dirs 系统变量所指定的路径的所有类库到内存中。
应用类加载器(Application ClassLoader):负责加载用户类路径上的指
定类库,如果应用程序中没有实现自己的类加载器,一般就是这个类加载
器去加载应用程序中的类库。
1、原理介绍
ClassLoader 使用的是双亲委托模型来搜索类的,每个 ClassLoader 实例都有一
个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类
加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它
lassLoader 实例的的父类加载器。
当一个 ClassLoader 实例需要加载某个类时,它会在试图搜索某个类之前,先把
这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶
层的类加载器 Bootstrap ClassLoader 试图加载,如果没加载到,则把任务转交
给 Extension ClassLoader 试图加载,如果也没加载到,则转交给 App ClassLoader
进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的
文件系统或网络等待 URL 中加载该类。
如果它们都没有加载到这个类时,则抛出 ClassNotFoundException 异常。否则
将这个找到的类生成一个类的定义,将它加载到内存当中,最后返回这个类在内
存中的 Class 实例对象。
类加载机制:类的加载指的是将类的.class 文件中的二进制数据读入到内存中,将其放在运行
时数据区的方法去内,然后在堆区创建一个 java.lang.Class 对象,用来封装在方
法区内的数据结构。类的加载最终是在堆区内的 Class 对象,Class 对象封装了
类在方法区内的数据结构,并且向 Java 程序员提供了访问方法区内的数据结构
的接口。
类加载有三种方式:
1)命令行启动应用时候由 JVM 初始化加载
2)通过 Class.forName()方法动态加载
3)通过 ClassLoader.loadClass()方法动态加载
这么多类加载器,那么当类在加载的时候会使用哪个加载器呢?
这个时候就要提到类加载器的双亲委派模型,流程图如下所示:
双亲委派模型的整个工作流程非常的简单,如下所示:
如果一个类加载器收到了加载类的请求,它不会自己立去加载类,它会先去请求
父类加载器,每个层次的类加器都是如此。层层传递,直到传递到最高层的类加
载器只有当 父类加载器反馈自己无法加载这个类,才会有当子类加载器去加载
该类。
2、为什么要使用双亲委托这种模型呢?
因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要让子
ClassLoader 再加载一次。
考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时
使用自定义的 String 来动态替代 java 核心 api 中定义的类型,这样会存在非常
大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为 String 已经在启动时就被引导类加载器(BootstrcpClassLoader)加载,所以用户自定义的
ClassLoader 永远也无法加载一个自己写的 String,除非你改变 JDK 中
ClassLoader 搜索类的默认算法。
3、但是 JVM 在搜索类的时候,又是如何判定两个 class 是相同的呢?
JVM 在判定两个 class 是否相同时,不仅要判断两个类名否相同,而且要判断是
否由同一个类加载器实例加载的。
只有两者同时满足的情况下,JVM 才认为这两个 class 是相同的。就算两个 class
是同一份 class 字节码,如果被两个不同的 ClassLoader 实例所加载,JVM 也会
认为它们是两个不同 class。
比如网络上的一个 Java 类 org.classloader.simple.NetClassLoaderSimple,javac
编译之后生成字节码文件 NetClasLoaderSimple.class,ClassLoaderA 和
ClassLoaderB 这个类加载器并读取了 NetClassLoaderSimple.class 文件并分别定
义出了 java.lang.Class 实例来表示这个类,对 JVM 来说,它们是两个不同的实
例对象,但它们确实是一份字节码文件,如果试图将这个 Class 实例生成具体的
对象进行转换时,就会抛运行时异常 java.lang.ClassCastException,提示这是两
个不同的类型。
Android 类加载器
对于 Android 而言,最终的 apk 文件包含的是 dex 类型的文件,dex 文件是将
class 文件重新打包,打包的规则又不是简单地压缩,而是完全对 class 文件内部
的各种函数表进行优化,产生一个新的文件,即 dex 文件。因此加载某种特殊的
Class 文件就需要特殊的类加载器 DexClassLoader。可以动态加载 Jar 通过 URLClassLoader
1.ClassLoader 隔离问题:JVM 识别一个类是由 ClassLoaderid + PackageName
+ ClassName。
2.加载不同 Jar 包中的公共类:
让父 ClassLoader 加载公共的 Jar,子 ClassLoade 加载包含公共 Jar 的 Jar,
此时子 ClassLoader 在加载 Jar 的时候会先去父 ClassLoader 中找。(只适
用 Java)
重写加载包含公共 Jar 的 Jar 的 ClassLoader,在 loClass 中找到已经加载
过公共 Jar 的 ClassLoader,是把父 ClassLoader 替换掉。(只适用 Java)
在生成包含公共 Jar 的 Jar 时候把公共 Jar 去掉。
5、JVM 跟 Art、Dalvik 对比?
6、GC 收集器简介?以及它的内存划分怎么样的?
(1)简介:
Garbage-First(G1,垃圾优先)收集器是服务类型的收集器,目标是多处理器
机器、大内存机器。它高度符合垃圾收集暂停时间的目标,同时实现高吞吐量。
Oracle JDK 7 update 4 以及更新发布版完全支持 G1 垃圾收集器
(2)G1 的内存划分方式:它是将堆内存被划分为多个大小相等的 heap 区,每个 heap 区都是逻辑上连续
的一段内存(virtual memory). 其中一部分区域被当成老一代收集器相同的角色
(eden, survivor, old), 但每个角色的区域个数都不是固定的。这在内存使用上提
供了更多的灵活性
7、Java 的虚拟机 JVM 的两个内存:栈内存和堆内存的区别是什么?
Java 把内存划分成两种:一种是栈内存,一种是堆内存。两者的区别是:
1)栈内存:在函数中定义的一些基本类型的变量和对象的引用变量都在函数的
栈内存中分配。 当在一段代码块定义一个变量时,Java 就在栈中为这个变量分
配内存空间,当超过变量的作用域后,Java 会自动释放掉为该变量所分配的内存
空间,该内存空间可以立即被另作他用。
2)堆内存:堆内存用来存放由 new 创建的对象和数组。在堆中分配的内存,由
Java 虚拟机的自动垃圾回收器来管理。
8、JVM 调优的常见命令行工具有哪些?JVM 常见的调优参数有哪些?
(1)JVM 调优的常见命令工具包括:
1)jps 命令用于查询正在运行的 JVM 进程,
2)jstat 可以实时显示本地或远程 JVM 进程中类装载、内存、垃圾收集、JIT 编
译等数据
3)jinfo 用于查询当前运行这的 JVM 属性和参数的值。
4)jmap 用于显示当前 Java 堆和永久代的详细信息5)jhat 用于分析使用 jmap 生成的 dump 文件,是 JDK 自带的工具
6)jstack 用于生成当前 JVM 的所有线程快照,线程快照是虚拟机每一条线程正
在执行的方法,目的是定位线程出现长时间停顿的原因。
(2)JVM 常见的调优参数包括:
-Xmx
指定 java 程序的最大堆内存, 使用 java -Xmx5000M -version 判断当前系统
能分配的最大堆内存
-Xms
指定最小堆内存, 通常设置成跟最大堆内存一样,减少 GC
-Xmn
设置年轻代大小。整个堆大小=年轻代大小 + 年老代大小。所以增大年轻
代后,将会减小年老代大小。此值对系统性能影响较大,Sun 官方推荐配置为
个堆的 3/8。
-Xss
指定线程的最大栈空间, 此参数决定了 java 函数调用的深度, 值越大调用深
度越深, 若值太小则容易出栈溢出错误(StackOverflowError)
-XX:PermSize指定方法区(永久区)的初始值,默认是物理内存的 1/64, 在 Java8 永久区移
除, 代之的是元数据区, 由-XX:
|