Java面试题整理(转)
Java面试题整理
计算机网络
1.停止等待协议
停止等待协议是最基本的数据链路层协议,它的工作原理是这样的。
在发送端,每发送完一帧就停止发送,等待接收端的确认,如果收到确认就发送下一帧。
在接收端,每收到一个无差错的帧,就把这个帧交付上层并向发送端发送确认。若该帧有差错,就丢弃,其他什么也不做。
其他细节:
停止等待协议为了可靠交付,需要对帧进行编号,由于每次只发送一帧,所有停止等待协议使用1个比特编号,编号0和1
停止等待协议会出现死锁现象(A等待B的确认),解决办法,启动超时计时器,超时计时器有一个重传时间。重传时间一般选择略大于“正常情况下从发完数据帧到收到确认帧所需的平均时间”。
2.滑动窗口协议
再说滑动窗口之前,先说下连续ARQ,连续ARQ又称Go-back-N ARQ,意思是当出现差错必须重传时,要向回走N个帧,然后再开始重传,也就意味着只要有一帧出现差错,即使已经正确的帧也需要重传,白白浪费时间,增大开销。为此,应该对发送出去但未被确认的帧的数目加以限制,这就是滑动窗口协议。滑动窗口指收发两端分别维护一个发送窗口和接收窗口,发送窗口有一个窗口值Wt,窗口值Wt代表在没有收到对方确认的情况下最多可以发送的帧的数目。当发送的帧的序号被接收窗口正确收下后,接收端向前滑动并向发送端发去确认,发送端收到确认后,发送窗口向前滑动。收发两端按规律向前推进。
连续ARQ和选择重传ARQ均是窗口大于1的滑动窗口协议,而停止等待协议相当于收发两端窗口等于1.
滑动窗口指接收和发送两端的窗口按规律不断向前推进,是一种流量控制的策略。
3.Http1.0和Http1.1的区别
1.HTTP/1.0协议使用非持久连接,即在非持久连接下,一个tcp连接只传输一个Web对象。
2.HTTP/1.1默认使用持久连接(然而,HTTP/1.1协议的客户机和服务器可以配置成使用非持久连接)。在持久连接下,不必为每个Web对象的传送建立一个新的连接,一个连接中可以传输多个对象。
4.Post和Get的区别
1.安全性上说:get的方式是把数据在地址栏中明文的形式发送,URL中可见,POST方式对用户是透明的,安全性更高。
2.数据量说:Get传送的数据量较小,一般不能大于2KB,POST传送的数据量更大。
3.适用范围说:查询用Get,数据添加、修改和删除建议Post
5.TCP/IP体系各层功能及协议
TCP/IP体系共有四个层次,分别为网络接口层Host-to-Network Layer, 网际层 Internet Layer, 传输层Transport Layer,应用层Application Layer。
网络接口层 -> 接收和发送数据报
主要负责将数据发送到网络传输介质上以及从网络上接收TCP/IP数据报,相当于OSI参考模型的物理层和数据链路层。在实际中,先后流行的以太网、令牌环网、ATM、帧中继等都可视为其底层协议。它将发送的信息组装成帧并通过物理层向选定网络发送,或者从网络上接收物理帧,将去除控制信息后的IP数据报交给网络层。
网际层 -> 数据报封装和路由寻址
网际层主要功能是寻址和对数据报的封装以及路由选择功能。这些功能大部分通过IP协议完成,并通过地址解析协议ARP、逆地址解析协议RARP、因特网控制报文协议ICMP、因特网组管理协议IGMP从旁协助。所以IP协议是网络层的核心。
网际协议IP:IP协议是一个无连接的协议,主要负责将数据报从源结点转发到目的结点。也就是说IP协议通过对数据报中源地址和目的地址进行分析,然后进行路由选择,最后再转发到目的地。需要注意的是:IP协议只负责对数据进行转发,并不对数据进行检查,也就是说,它不负责数据的可靠性,这样设计的主要目的是提高IP协议传送和转发数据的效率。
ARP:该协议负责将IP地址解析转换为计算机的物理地址。
虽然我们使用IP地址进行通信,但IP地址只是主机在抽象的网络层中的地址。最终要传到数据链路层封装成MAC帧才能发送到实际的网络。因此不管使用什么协议最终需要的还是硬件地址。
每个主机拥有一个ARP高速缓存(存放所在局域网内主机和路由器的IP地址到硬件地址的映射表)
举例:A发送B
(1).A在自己的ARP高速缓存中查到B的MAC地址,写入MAC帧发往此B
(2)没查到,A向本局域网广播ARP请求分组,内容包括自己的地址映射和B的IP地址
(3)B发送ARP响应分组,内容为自己的IP地址到物理地址的映射,同时将A的映射写入自己的ARP高速缓存(单播的方式)
注:ARP Cache映射项目具有一个生存时间。
RARP:将计算机物理地址转换为IP地址
ICMP:该协议主要负责发送和传递包含控制信息的数据报,这些控制信息包括了哪台计算机出现了什么错误,网络路由出现了什么错误等内容。
传输层 -> 应用进程间端到端的通信
传输层主要负责应用进程间“端到端”的通信,即从某个应用进程传输到另一个应用进程,它与OSI参考模型的传输层功能类似。
传输层在某个时刻可能要同时为多个不同的应用进程服务,因此传输层在每个分组中必须增加用于识别应用进程的标识,即端口。
TCP/IP体系的传输层主要包含两个主要协议,即传输控制协议TCP和用户数据报协议UDP。TCP协议是一种可靠的、面向连接的协议,保证收发两端有可靠的字节流传输,进行了流量控制,协调双方的发送和接收速度,达到正确传输的目的。
UDP是一种不可靠的、无连接的协议,其特点是协议简单、额外开销小、效率较高,不能保证可靠传输。
传输层提供应用进程间的逻辑通信。它使应用进程看见的就好像是在两个运输层实体间一条端到端的逻辑通信信道。
当运输层采用TCP时,尽管下面的网络是不可靠的,但这种逻辑通信信道相当于一条全双工的可靠信道。可以做到报文的无差错、按序、无丢失、无重复。
注:单单面向连接只是可靠的必要条件,不充分。还需要其他措施,如确认重传,按序接收,无丢失无重复。
熟知端口:
20 FTP数据连接
21 FTP控制连接
22 SSH
23 TELNET
25 SMTP
53 DNS
69 TFTP
80 HTTP
161 SNMP
UDP重要
UDP的优点:
1.发送之前无需建立连接,减小了开销和发送数据的时延
2.UDP不使用拥塞控制,不使用可靠交付,因此主机不需要维护复杂的参数表、连接状态表
3.UDP用户数据报只有8个字节的首部开销,而TCP要20字节。
4.由于没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(IP电话等实时应用要求源主机以恒定的速率发送数据)
Table,使用TCP和UDP的应用
应用 | 应用层协议 | 运输层协议 |
---|---|---|
名字转换 | DNS | UDP |
文件传送 | TFTP | UDP |
路由选择协议 | RIP | UDP |
IP地址配置 | BOOTTP,DHCP | UDP |
网络管理 | SNMP | UDP |
远程文件服务器 | NFS | UDP |
IP电话 | 专用协议 | UDP |
流式多媒体通信 | 专用协议 | UDP |
电子邮件 | SMTP | TCP |
远程终端接入 | TELNET | TCP |
万维网 | HTTP | TCP |
文件传送 | FTP | TCP |
注:TFTP:Trivial File Transfer Protocol
UDP的过程:
1.服务器进程运行着,等待TFTP客户进程的服务请求。客户端TFTP进程启动时,向操作系统申请一个临时端口号,然后操作系统为该进程创建2个队列,
入队列和出队列。只要进程在执行,2个队列一直存在。
2.客户进程将报文发送到出队列中。UDP按报文在队列的先后顺序发送。在传送到IP层前给报文加上UDP首部,其中目的端口后为69。然后发给IP层。
出队列若溢出,则操作系统通知应用层TFTP客户进程暂停发送。
3.客户端收到来自IP层的报文时,UDP检查报文中目的端口号是否正确,若正确,放入入队列队尾,客户进程按先后顺序一一取走。若不正确,UDP丢弃该报文,并请ICMP发送”端口不可达“差错报文给服务器端。入队列可能会溢出,若溢出,UDP丢弃该报文,不通知对方。
服务器端类似。
UDP首部:源端口 - 目的端口 - 长度 - 检验和,每个字段22字节。
IP数据报检验和只检验IP数据报的首部,而UDP的检验和将首部和数据部分一起都检验。
TCP重要
细节:
TCP报文段是面向字节的数据流。
TCP首部:20字节固定首部
确认比特ACK,ACK=1 确认号字段才有效;同步比特SYN:SYN=1 ACK=0表示一个连接请求报文段;终止比特FIN,FIN=1时要求释放连接。
窗口:将TCP收发两端记为A和B,A根据TCP缓存空间的大小确定自己的接收窗口大小。并在A发送给B的窗口字段写入该值。作为B的发送窗口的上限。意味着B在未收到A的确认情况下,最多发送的字节数。
选项:最大报文段长度MSS,MSS告诉对方TCP:我的缓存所能接收的报文段的数据字段的最大长度是MSS个字节。若主机未填写,默认为536字节。
TCP的可靠是使用了序号和确认。当TCP发送一个报文时,在自己的重传队列中存放一个副本。若收到副本,删除副本。
TCP使用捎带确认。
TCP报文段的发送时机:1.维持一个变量等于MSS,发送缓存达到MSS就发送 2.发送端应用进程指明要发送,即TCP支持的PUSH操作。3.设定计时器
TCP的拥塞控制:TCP使用慢开始和拥塞避免算法进行拥塞控制
慢开始和拥塞避免
接收端根据自身资源情况控制发送端发送窗口的大小。
每个TCP连接需要维持一下2个状态变量:
接收端窗口rwnd(receiver window):接收端根据目前接收缓存大小设置的窗口值,是来自接收端的流量控制
拥塞窗口cwnd(congestion window):是发送端根据自己估计的网络拥塞程度设置的窗口值,是来自发送端的流量控制
发送端的窗口上限值=Min(rwnd, cwnd)
慢开始算法原理:主机刚开始发送数据时,如果立即将较大的发送窗口的全部字节注入网络,由于不清楚网络状况,可能会引起拥塞。通常的做法是将cwnd设置为1个MSS,每收到一个确认,将cwnd+1,由小到大逐步增大cwnd,使分组注入网络的速率更加合理。为了防止拥塞窗口增长引起网络拥塞,还需设置一个状态变量ssthresh,即慢开始门限。
慢开始门限:ssthresh,当cwnd < ssthresh,执行慢开始算法;cwnd > ssthresh,改用拥塞避免算法。 cwnd = ssthresh时,都可以。
拥塞避免算法使发送端的拥塞窗口每经过一个RTT增加一个MSS(而不管在此期间收到多少ACK),这样,拥塞窗口cwnd按线性规律增长,拥塞窗口此时比慢开始增长速率缓慢很多。这一过程称为加法增大,目的在于使拥塞窗口缓慢增长,防止网络过早拥塞。
无论是慢开始还是拥塞避免,只要发送端发现网络出现拥塞(根据是没有按时收到ACK或者收到重复ACK),就将慢开始门限ssthresh设置为拥塞窗口值的一半并将拥塞窗口cwnd置为1,重新执行慢开始算法。这一过程称为乘法减小。目的在于迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕。
上述TCP确认都是通过捎带确认执行的。
快重传和快恢复
上述的慢开始和拥塞避免算法是早期TCP使用的拥塞控制算法。因为有时TCP连接会在重传时会因等待重传计时器的超时时间而空闲。为此在快重传中:只要发送端一连收到三个重复的ACK,即可断定分组丢失,不必等待重传计数器,立即重传丢失的报文。
与快重传搭配使用的还有快恢复:当不使用快恢复时,发送端若发现网络拥塞就将拥塞窗口降为1,然后执行慢开始算法,这样的缺点是网络不能很快恢复到正常状态。快恢复是指当发送端收到3个重复的ACK时,执行乘法减小,ssthresh变为拥塞窗口值的一半。但是cwnd不是置为1,而是ssthresh+3xMSS。若收到的重复ACK
为n(n > 3),则cwnd=ssthresh+n*MSS.这样做的理由是基于发送端已经收到3个重复的ACK,它表明已经有3个分组离开了网络,它们不在消耗网络的资源。
注意的是:在使用快恢复算法时,慢开始算法只在TCP连接建立时使用。
TCP的重传机制
每发送一个报文段,就对这个报文段设置一次计时器。新的重传时间=γ*旧的重传时间。
TCP连接建立和释放的过程
SYN置1和FIN的报文段要消耗一个序号。
客户端连接状态变迁:CLOSED -> 主动打开,发送SYN=1 -> SYN_SENT -> 收到服务器的SYN=1和ACK时,发送三次握手的最后一个ACK
-> ESTABLISHED -> 数据传送 -> 主动关闭 -> 发送FIN=1,等待确认ACK的到达 -> FIN_WAIT_1 -> 收到确认ACK后 -> FIN_WAIT_2
->收到服务器发送的FIN=1报文,响应,发送四次挥手的的最后一个确认ACK -> 进入TIME_WAIT状态
-> 经过2倍报文寿命,TCP删除连接记录 -> 回到CLOSED状态
服务器端连接状态变迁:CLOSED -> 被动打开 -> LISTEN -> 收到SYN=1的报文,发送SYN=1和确认ACK -> 进入SYN_RCVD -> 收到三次握手
的最后一个确认ACK -> ESTABLISHED -> 数据传送 -> 数据传送完毕,收到FIN=1 -> 发送确认ACK并进入CLOSED_WAIT -> 发送FIN=1给客户端 -> LAST_ACK
-> 收到客户端四次挥手的最后一个确认ACK -> 删除连接记录 -> 回到CLOSED状态
应用层
应用层位于TCP/IP体系结构的最高一层,也是直接为应用进程服务的一层,即当不同的应用进程数据交换时,就去调用应用层的不同协议实体,让这些实体去调用传输层的TCP或者UDP来进行网络传输。具体的应用层协议有,SMTP 25、DNS 53、HTTP 80、FTP 20数据端口 21控制端口、TFTP 69、TELNET 23、SNMP 161等
6.网络的划分
按网络拓扑结构:总线、星型、环型、树型、网状结构和混合型。
按覆盖范围:局域网、城域网、广域网
按传播方式:广播网络和点对点网络
广播式网络是指网络中的计算机使用一个共享信道进行数据传播,网络中的所有结点都能收到某一结点发出的数据信息。
单播:一对一的发送形式。
组播:采用一对一组的发送形式,将数据发送给网络中的某一组主机。
广播:采用一对所有,将数据发送给网络所有目的结点。
点对点网络中两个结点间的通信方式是点对点的。如果两台计算机之间没有直连的线路,则需要中间结点的接收、存储、转发直至目的结点。
7.TCP的三次握手和四次挥手的过程
连接建立(三次握手):首先Client端发送连接请求报文SYN并进入SYN_SENT状态,Server收到后发送ACK+SYN报文,并为这次连接分配资源。Client端接收到Server端的SYN+ACK后发送三次握手的最后一个ACK,并分配资源,连接建立。
连接释放(四次挥手):假设Client端发起断开连接请求,首先发送FIN=1,等待确认ACK的到达 -> FIN_WAIT_1 -> 收到确认Server端的确认ACK后时 -> FIN_WAIT_2
->收到服务器发送的FIN=1报文,响应,发送四次挥手的的最后一个确认ACK ->进入TIME_WAIT状态
-> 经过2倍报文寿命,TCP删除连接记录 -> 回到CLOSED状态
8.为什么连接建立是三次握手,而连接释放要四次挥手?
因为当Server端收到Client端发送的SYN连接请求报文后,可以直接发送SYN+ACK报文,其中ACK用来应答,SYN用来同步。但是关闭连接时,当Server端收到FIN报文后,并不会立即关闭socket,所以先回复一个ACK,告诉Client端“你的FIN我收到了”,只有等Server端的所有报文发送完了,Server端才发送FIN报文,因此不能一起发送,故需要四次挥手。
9.为什么TIME_WAIT状态需要2MSL(最大报文段生存时间)才能返回Closed状态?
这是因为虽然双方都同意关闭连接了,而且四次挥手的报文也都协调发送完毕。但是我们必须假想网络是不可靠的,无法保证最后发送的ACK报文一定被对方收到,因此处于LAST_ACK状态下的
Server端可能会因未收到ACK而重发FIN,所以TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。
10.Http报文格式
Http请求报文格式:1.请求行 2.Http头 3.报文主体
请求行由三部分组成,分别是请求方法,请求地址,Http版本
Http头:有三种,分别为请求头(request header),普通头(General Header)和实体头(entity header)。Get方法没有实体头。
报文主体:只在POST方法请求中存在。
Http响应报文:1.状态行 2.Http头 3.返回内容
状态行:第一部分为Http版本,第二部分为响应状态码 第三部分为状态码的描述
其中第三部分为状态码的描述,信息类100-199 响应成功200-299 重定向类300-399 客户端错误400-499 服务器端错误500-599
Http头:响应头(Response Header),普通头(General Header)和实体头(Entity Header)
返回内容:即Http请求的信息,可以是HTML也可以是图片等等。
11.Http和Https的区别
Https即Secure Hypertext Transfer Protocol,即安全超文本传输协议,它是一个安全通信信道,基于Http开发,用于在客户机和服务器间交换信息。它使用安全套接字层SSL进行信息交换,是Http的安全版。
Https协议需要到CA申请证书,一般免费证书很少,需要交费。
Http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
http是80端口,https是443端口
12.浏览器输入一个URL的过程
浏览器向DNS服务器请求解析该URL中的域名所对应的IP地址
解析出IP地址后,根据IP地址和默认端口80和服务器建立TCP连接
浏览器发出Http请求,该请求报文作为TCP三次握手的第三个报文的数据发送给服务器
服务器做出响应,把对应的请求资源发送给浏览器
释放TCP连接
浏览器解析并显示内容
13.中间人攻击
中间人获取server发给client的公钥,自己伪造一对公私钥,然后伪造自己让client以为它是server,然后将伪造的公钥发给client,并拦截client发给server的密文,用伪造的私钥即可得到client发出去的内容,最后用真实的公钥对内容加密发给server。
解决办法:数字证书,证书链,可信任的中间人
14.差错检测
误码率:传输错误的比特与传输总比特数的比率
CRC是检错方法并不能纠错,FCS(Frame Check Sequence)是冗余码。
计算冗余码(余数R)的方法:先补0(n个)再对生成多项式取模。
CRC只能表示以接近1的概率认为它没有差错。但不能做到可靠传输。可靠传输还需要确认和重传机制。
生成多项式P(X):CRC-16,CRC-CCITT,CRC-32
15.数据链路层的协议
停止等待协议 - 连续ARQ - 选择重传ARQ - PPP - 以太网协议- 帧中继 - ATM - HDLC
16.截断二进制指数退避算法
是以太网用于解决当发生碰撞时就停止发送然后重发再碰撞这一问题。
截断二进制指数退避算法:基本退避时间为2τ k=min{重传次数,10} r=random(0~2^k-1) 重传所需时延为r倍的基本退避时间
JavaSE基础
1.equals与==的区别
区别1.==是一个运算符 equals是Object类的方法
区别2.用于基本类型的变量比较时:==用于比较值是否相等,equals不能直接用于基本数据类型的比较,需要转换为其对应的包装类型。
b.用于引用类型的比较时。==和equals都是比较栈内存中的地址是否相等 。相等为true 否则为false。但是通常会重写equals方法去实现对象内容的比较。
2.Object有哪些公用方法?
clone equals hashcode wait notify notifyall finalize toString getClass
除了clone和finalize其他均为公共方法。
3.Java中的四种引用
StrongReference:JVM默认,只要StrongReference存在,GC将不会回收被引用的对象
SoftReference:尽可能长时间保留引用,直到JVM内存不足,适合某些缓存应用
WeakReference:尽可能长时间的存活于JVM内直到下一次GC发生前
PhantomReference:它是最弱的一种引用关系,也无法通过PhantomReference取得对象的实例。仅用来当该对象被回收时收到一个通知
4.Hashcode的作用
HashCode是Object类的一个方法。hashCode() 方法是相应对象整型的 hash 值。它常用于基于 hash 的集合类,如 Hashtable、HashMap、LinkedHashMap等等。它与 equals() 方法关系特别紧密。根据 Java 规范,两个使用 equals() 方法来判断相等的对象,必须具有相同的 hash code。也就是说,
如果调用equals方法得到的结果为true,则两个对象的hashcode值必定相等
如果equals方法得到的结果为false,则两个对象的hashcode值不一定不同
如果两个对象的hashcode值不等,则equals方法得到的结果必定为false
如果两个对象的hashcode值相等,则equals方法得到的结果未知
注意的一点是:在重写equals方法的同时,必须重写hashCode方法。让equals方法和hashCode方法在逻辑上保持一致性。
为什么要重写:不重写可能会违反hashcode方法的约定,即“相等的对象必须具有相等的hashcode值”,导致对基于 hash 的集合类无法正常工作,比如说HashMap,它是使用Key对象的hashCode()和equals()方法去决定key-value对的。如果只重写equals方法,将某个对象用put方法放入散列桶中,get时可能从另外一个散列桶获取,因为两个方法的传入参数的equals虽然相同,但hashcode可能不同。
5.String、StringBuffer与StringBuilder的区别
第一点:可变和适用范围。String对象是不可变的,而StringBuffer和StringBuilder是可变字符序列。每次对String的操作相当于生成一个新的String对象,而对StringBuffer和StringBuilder的操作是对对象本身的操作,而不会生成新的对象,所以对于频繁改变内容的字符串避免使用String,因为频繁的生成对象将会对系统性能产生影响。
第二点:线程安全。String由于有final修饰,是immutable的,安全性是简单而纯粹的。StringBuilder和StringBuffer的区别在于StringBuilder不保证同步,也就是说如果需要线程安全需要使用StringBuffer,不需要同步的StringBuilder效率更高。
6.并发和并行
“并行”是指无论从微观还是宏观,二者都是一起执行的,也就是同一时刻执行
而“并发”在微观上不是同时执行的。是在同一时间间隔交替轮流执行
7.switch能否用String做参数
Java1.7开始支持,但实际这是一颗Java语法糖。除此之外,byte,short,long,枚举,boolean均可用于switch,只有浮点型不可以。
8.Java语法糖
- Java7的switch用字符串 - 实际为hashcode方法
- 伪泛型 - 实际为List原始类型
- 自动装箱拆箱 - 实际为Integer.valueOf和Integer.intValue
- foreach遍历 - 实际为Iterator迭代器实现
- 条件编译
- enum枚举类、内部类
- 可变参数 - 实际为数组实现
- 断言语言
- try语句中定义和关闭资源
9.try catch finally问题
1)不管有木有出现异常,finally块中代码都会执行
2)当try和catch中有return时,finally仍然会执行
3)finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,仍然是之前保存的值),所以函数返回值是在finally执行前确定的
4)finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值
10.受检查异常和运行时异常
1.粉红色的是受检查的异常(checked exceptions),其必须被try…catch语句块所捕获,或者在方法签名里通过throws子句声明。受检查的异常必须在编译时被捕捉处理,命名为Checked Exception是因为Java编译器要进行检查,Java虚拟机也要进行检查,以确保这个规则得到遵守。
2.绿色的异常是运行时异常(runtime exceptions),需要程序员自己分析代码决定是否捕获和处理,比如空指针,被0除…
3.而声明为Error的,则属于严重错误,如系统崩溃、虚拟机错误、动态链接失败等,这些错误无法恢复或者不可能捕捉,将导致应用程序中断,Error不需要捕捉。
11.封装、继承、多态
封装:
1.概念:就是把对象的属性和操作(或服务)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节。
2.好处:
(1)隐藏内部实现细节。
继承:
1.概念:继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力
2.好处:提高代码的复用,缩短开发周期。
多态:
1.概念:多态(Polymorphism)按字面的意思就是“多种状态,即同一个实体同时具有多种形式。一般表现形式是程序在运行的过程中,同一种类型在不同的条件下表现不同的结果。多态也称为动态绑定,一般是在运行时刻才能确定方法的具体执行对象。
2.好处:
1)将接口和实现分开,改善代码的组织结构和可读性,还能创建可拓展的程序。
2)消除类型之间的耦合关系。允许将多个类型视为同一个类型。
3)一个多态方法的调用允许有多种表现形式
12.抽象类与接口
1.一个子类只能继承一个抽象类,但能实现多个接口
2.抽象类可以有构造方法,接口没有构造方法
3.抽象类可以有普通成员变量,接口没有普通成员变量
4.抽象类和接口都可有静态成员变量,抽象类中静态成员变量访问类型任意,接口只能public static final(默认)
5.抽象类可以没有抽象方法,抽象类可以有普通方法,接口中都是抽象方法
6.抽象类可以有静态方法,接口不能有静态方法
7.抽象类中的方法可以是public、protected和默认;接口方法只有public abstract
13.静态内部类和普通内部类
静态内部类不需要有指向外部类的引用。但非静态内部类需要持有对外部类的引用。非静态内部类能够访问外部类的静态和非静态成员。静态类不能访问外部类的非静态成员。他只能访问外部类的静态成员。
集合框架
1.List集合和Set集合
List接口:
List中元素存取是有序的、可重复的;Set集合中元素是无序的,不可重复的。
CopyOnWriteArrayList:COW的策略,即写时复制的策略。适用于读多写少的并发场景
Set接口:
Set集合元素存取无序,且元素不可重复。
HashSet不保证迭代顺序,线程不安全;LinkedHashSet是Set接口的哈希表和链接列表的实现,保证迭代顺序,线程不安全。
TreeSet:可以对Set集合中的元素排序,元素以二叉树形式存放,线程不安全。
2.ArrayList、LinkedList、Vector的区别
首先它们均是List接口的实现。
ArrayList、LinkedList的区别
1.随机存取:ArrayList是基于可变大小的数组实现,LinkedList是链接列表的实现。这也就决定了对于随机访问的get和set的操作,ArrayList要优于LinkedList,因为LinkedList要移动指针。
2.插入和删除:LinkedList要好一些,因为ArrayList要移动数据,更新索引。
3.内存消耗:LinkedList需要更多的内存,因为需要维护指向后继结点的指针。
Vector从Java 1.0起就存在,在1.2时改为实现List接口,功能与ArrayList类似,但是Vector具备线程安全。
3.Map集合
Hashtable:基于Dictionary类,线程安全,速度快。底层是哈希表数据结构。是同步的。
不允许null作为键,null作为值。
Properties:Hashtable的子类。用于配置文件的定义和操作,使用频率非常高,同时键和值都是字符串。
HashMap:线程不安全,底层是数组加链表实现的哈希表。允许null作为键,null作为值。HashMap去掉了contains方法。
注意:HashMap不保证元素的迭代顺序。如果需要元素存取有序,请使用LinkedHashMap
TreeMap:可以用来对Map集合中的键进行排序。
ConcurrentHashMap:是JUC包下的一个并发集合。
为什么使用ConcurrentHashMap而不是HashMap或Hashtable?
HashMap的缺点:主要是多线程同时put时,如果同时触发了rehash操作,会导致HashMap中的链表中出现循环节点,进而使得后面get的时候,会死循环,CPU达到100%,所以在并发情况下不能使用HashMap。让HashMap同步:Map m = Collections.synchronizeMap(hashMap);而Hashtable虽然是同步的,使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。
ConcurrentHashMap的原理:
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因在于所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap的结构:
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,当对某个HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
ConcurrentHashMap的构造、get、put操作:
构造函数:传入参数分别为 1、初始容量,默认16 2、装载因子 装载因子用于rehash的判定,就是当ConcurrentHashMap中的元素大于装载因子*最大容量时进行扩容,默认0.75 3、并发级别 这个值用来确定Segment的个数,Segment的个数是大于等于concurrencyLevel的第一个2的n次方的数。比如,如果concurrencyLevel为12,13,14,15,16这些数,则Segment的数目为16(2的4次方)。默认值为static final int DEFAULT_CONCURRENCY_LEVEL = 16;。理想情况下ConcurrentHashMap的真正的并发访问量能够达到concurrencyLevel,因为有concurrencyLevel个Segment,假如有concurrencyLevel个线程需要访问Map,并且需要访问的数据都恰好分别落在不同的Segment中,则这些线程能够无竞争地*访问(因为他们不需要竞争同一把锁),达到同时访问的效果。这也是为什么这个参数起名为“并发级别”的原因。默认16.
初始化的一些动作:
初始化segments数组(根据并发级别得到数组大小ssize),默认16
初始化segmentShift和segmentMask(这两个全局变量在定位segment时的哈希算法里需要使用),默认情况下segmentShift为28,segmentMask为15
初始化每个Segment,这一步会确定Segment里HashEntry数组的长度.
put操作:
1、判断value是否为null,如果为null,直接抛出异常。
2、key通过一次hash运算得到一个hash值。将得到hash值向右按位移动segmentShift位,然后再与segmentMask做&运算得到segment的索引j。即segmentFor方法
3、使用Unsafe的方式从Segment数组中获取该索引对应的Segment对象。向这个Segment对象中put值,这个put操作也基本是一样的步骤(通过&运算获取HashEntry的索引,然后set)。
get操作:
1、和put操作一样,先通过key进行hash确定应该去哪个Segment中取数据。
2、使用Unsafe获取对应的Segment,然后再进行一次&运算得到HashEntry链表的位置,然后从链表头开始遍历整个链表(因为Hash可能会有碰撞,所以用一个链表保存),如果找到对应的key,则返回对应的value值,如果链表遍历完都没有找到对应的key,则说明Map中不包含该key,返回null。
定位Segment的hash算法:(hash >>> segmentShift) & segmentMask
定位HashEntry所使用的hash算法:int index = hash & (tab.length - 1);
注:tab为HashEntry数组
4.Collection 和 Collections的区别
Collection是集合类的上级接口,子接口主要有Set 和List、Queue
Collections是针对集合类的一个帮助类,提供了操作集合的工具方法:一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。
5.Map、Set、List、Queue、Stack的特点与用法
Set集合类似于一个罐子,”丢进”Set集合里的多个对象之间没有明显的顺序。 List集合代表元素有序、可重复的集合,集合中每个元素都有其对应的顺序索引。 Stack是Vector提供的一个子类,用于模拟”栈”这种数据结构(LIFO后进先出) Queue用于模拟”队列”这种数据结构(先进先出 FIFO)。 Map用于保存具有”映射关系”的数据,因此Map集合里保存着两组值。
6.HashMap的工作原理
HashMap维护了一个Entry数组,Entry内部类有key,value,hash和next是个字段,其中next也是一个Entry类型。可以将Entry数组理解为一个个的散列桶。每一个桶实际上是一个单链表。当执行put操作时,会根据key的hashcode定位到相应的桶。遍历单链表检查该key是否已经存在,如果存在,覆盖该value,反之,新建一个新的Entry,并放在单链表的头部。当通过传递key调用get方法时,它再次使用key.hashCode()来找到相应的散列桶,然后使用key.equals()方法找出单链表中正确的Entry,然后返回它的值。
7.HashMap和Hashtable的区别
- [x] Hashtable是基于陈旧的Dictionary的Map接口的实现,而HashMap是基于哈希表的Map接口的实现
- [x] 从方法上看,HashMap去掉了Hashtable的contains方法
- [x] HashTable是同步的(线程安全),而HashMap线程不安全
- [x] HashMap允许空键值,而Hashtable不允许
- [x] HashMap的iterator迭代器执行快速失败机制,也就是说在迭代过程中修改集合结构,除非调用迭代器自身的remove方法,否则以其他任何方式的修改都将抛出并发修改异常。如果寻求迭代的时候修改Map,可以使用ConcurrentHashMap。而Hashtable返回的Enumeration不是快速失败的。
8.Map的实现类的介绍
HashMap基于散列表来的实现,即使用hashCode()进行快速查询元素的位置,显著提高性能。插入和查询“键值对”的开销是固定的。可以通过设置容量和装载因子,以调整容器的性能。
LinkedHashMap, 类似于HashMap,但是迭代遍历它时,保证迭代的顺序是其插入的次序,因为它使用链表维护内部次序。此外可以在构造器中设定LinkedHashMap,使之采用LRU算法。使没有被访问过的元素或较少访问的元素出现在前面,访问过的或访问多的出现在后面。这对于需要定期清理元素以节省空间的程序员来说,此功能使得程序员很容易得以实现。
TreeMap, 是基于红黑树的实现。同时TreeMap实现了SortedMap接口,该接口可以确保键处于排序状态。所以查看“键”和“键值对”时,所有得到的结果都是经过排序的,次序由自然排序或提供的Comparator决定。SortedMap接口拥有其他额外的功能,如:返回当前Map使用的Comparator比较强,firstKey(),lastKey(),headMap(toKey),tailMap(fromKey)以及可以返回一个子树的subMap()方法等。
WeakHashMap,表示弱键映射,WeakHashMap 的工作与正常的 HashMap 类似,但是使用弱引用作为 key,意思就是当 key 对象没有任何引用时,key/value 将会被回收。
ConcurrentHashMap, 在HashMap基础上分段锁机制实现的线程安全的HashMap。
IdentityHashMap 使用==代替equals() 对“键”进行比较的散列映射。专为解决特殊问题而设计。
HashTable:基于Dictionary类的Map接口的实现,它是线程安全的。
9.LinkedList 和 PriorityQueue 的区别
它们均是Queue接口的实现。拥有FIFO的特点,它们的区别在于排序行为。LinkedList 支持双向列表操作,
PriorityQueue 按优先级组织的队列,元素的出队次序由元素的自然排序或者由Comparator比较器指定。
10.线程安全的集合类。Vector、HashTable、Properties和Stack
11.BlockingQueue
Java.util.concurrent.BlockingQueue是一个队列,在进行获取元素时,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。
12.如何对一组对象进行排序
如果需要对一个对象数组进行排序,我们可以使用Arrays.sort()方法。如果我们需要排序一个对象列表,我们可以使用Collections.sort()方法。排序时是默认根据元素的自然排序(使用Comparable)或使用Comparator外部比较器。Collections内部使用数组排序方法,所有它们两者都有相同的性能,只是Collections需要花时间将列表转换为数组。
13.Comparable和Comparator接口区别
Comparator位于包java.util下,而Comparable位于包java.lang下
如果我们需要使用Arrays或Collections的排序方法对对象进行排序时,我们需要在自定义类中实现Comparable接口并重写compareTo方法,compareTo方法接收一个参数,如果this对象比传递的参数小,相等或大时分别返回负整数、0、正整数。Comparable被用来提供对象的自然排序。String、Integer实现了该接口。
Comparator比较器的compare方法接收2个参数,根据参数的比较大小分别返回负整数、0和正整数。
Comparator 是一个外部的比较器,当这个对象自然排序不能满足你的要求时,你可以写一个比较器来完成两个对象之间大小的比较。用 Comparator 是策略模式(strategy design pattern),就是不改变对象自身,而用一个策略对象(strategy object)来改变它的行为。
14.与Java集合框架相关的有哪些最好的实践
(1)根据需要选择正确的集合类型。比如,如果指定了大小,我们会选用Array而非ArrayList。如果我们想根据插入顺序遍历一个Map,我们需要使用TreeMap。如果我们不想重复,我们应该使用Set。
(2)一些集合类允许指定初始容量,所以如果我们能够估计到存储元素的数量,我们可以使用它,就避免了重新哈希或大小调整。
(3)基于接口编程,而非基于实现编程,它允许我们后来轻易地改变实现。
(4)总是使用类型安全的泛型,避免在运行时出现ClassCastException。
(5)使用JDK提供的不可变类作为Map的key,可以避免自己实现hashCode()和equals()。
IO/NIO
1.IO和NIO
在以前的Java IO中,都是阻塞式IO,NIO引入了非阻塞式IO。
第一种方式:我从硬盘读取数据,然后程序一直等,数据读完后,继续操作。这种方式是最简单的,叫阻塞IO。
第二种方式:我从硬盘读取数据,然后程序继续向下执行,等数据读取完后,通知当前程序(对硬件来说叫中断,对程序来说叫回调),然后此程序可以立即处理数据,也可以执行完当前操作在读取数据。
2.流与块的比较
原来的 I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。面向流 的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。这样做是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。
一个 面向块 的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。
3.通道与流
Channel是一个对象,可以通过它读取和写入数据。通道与流功能类似,不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), 而通道可以用于读、写或者同时用于读写。
4.缓冲区Buffer
在 NIO 库中,所有数据都是用缓冲区处理的。在 NIO 库中,所有数据都是用缓冲区处理的。
Position: 表示下一次访问的缓冲区位置
Limit: 表示当前缓冲区存放的数据容量。
Capacity:表示缓冲区最大容量
flip()方法:它将 limit 设置为当前 position。它将 position 设置为 0
clear方法:它将 limit 设置为与 capacity 相同。它设置 position 为 0。
线程
1.什么是线程
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,可以使用多线程对运算密集型任务提速。比如,如果一个线程完成一个任务要100 毫秒,那么用十个线程完成改任务只需 10 毫秒。Java在语言层面对多线程提供了很好的支持。
2.线程和进程有什么区别
从概念上:
进程:一个程序对一个数据集的动态执行过程,是分配资源的基本单位。
线程:存在于进程内,是进程内的基本调度单位。共享进程的资源。
从执行过程中来看:
进程:拥有独立的内存单元,而多个线程共享内存,从而提高了应用程序的运行效率。
线程:每一个独立的线程,都有一个程序运行的入口、顺序执行序列、和程序的出口。但是线程不能够独立的执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
从逻辑角度来看:(重要区别)
多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但是,操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理及资源分配。
简言之,一个程序至少有一个进程,一个进程至少有一个线程。进程是资源分配的基本单位,线程共享进程的资源。
3.如何在 Java 中实现线程
继承Thread类或实现Runnable接口。
4.用 Runnable 还是 Thread
Java 不支持类的多重继承,但允许你调用多个接口。所以如果你要继承其他类,当然是实现Runnable接口好了。
5.Thread 类中的 start () 和 run () 方法有什么区别
start ()方法被用来启动新创建的线程,而且 start ()内部调用了 run ()方法,这和直接调用 run ()方法的效果不一样。当你调用 run ()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start ()方法才会启动新线程。也就是用start方法来启动线程,才是真正实现了多线程。而run方法只是一个普通方法。
6.Java 中 Runnable 和 Callable 有什么不同
Runnable和 Callable 都代表那些要在不同的线程中执行的任务。Runnable 从 JDK1.0 开始就有了,Callable 是在 JDK1.5 增加的。它们的主要区别是 Callable 的 call () 方法可以返回值和抛出异常,而 Runnable 的 run ()方法没有这些功能。
7.Java 中 CyclicBarrier 和 CountDownLatch 有什么不同
它们都是JUC下的类,CyclicBarrier 和 CountDownLatch 都可以用来让一组线程等待其它线程。区别在于CountdownLatch计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。
8.Java 内存模型是什么
Java 内存模型规定和指引Java 程序在不同的内存架构、CPU 和操作系统间有确定性地行为。它在多线程的情况下尤其重要。Java内存模型对一个线程所做的变动能被其它线程可见提供了保证,它们之间是先行发生关系。这个关系定义了一些规则让程序员在并发编程时思路更清晰。
线程内的代码能够按先后顺序执行,这被称为程序次序规则。
对于同一个锁,一个解锁操作一定要发生在时间上后发生的另一个锁定操作之前,也叫做管程锁定规则。
前一个对volatile的写操作在后一个volatile的读操作之前,也叫volatile变量规则。
一个线程内的任何操作必需在这个线程的 start ()调用之后,也叫作线程启动规则。
一个线程的所有操作都会在线程终止之前,线程终止规则。
一个对象的终结操作必需在这个对象构造完成之后,也叫对象终结规则。
传递性
9.Java 中的 volatile 变量是什么
Java 语言提供了一种稍弱的同步机制,即volatile
变量。但是volatile并不容器完全被正确、完整的理解。
一般来说,volatile具备2条语义,或者说2个特性。第一是保证volatile修饰的变量对所有线程的可见性,这里的可见性是指当一条线程修改了该变量,新值对于其它线程来说是立即可以得知的。而普通变量做不到这一点。
第二条语义是禁止指令重排序优化,这条语义在JDK1.5才被修复。
关于第一点:根据JMM,所有的变量存储在主内存,而每个线程还有自己的工作内存,线程的工作内存保存该线程使用到的变量的主内存副本拷贝,线程对变量的操作在工作内存中进行,不能直接读写主内存的变量。在volatile可见性这一点上,普通变量做不到的原因正因如此。比如,线程A修改了一个普通变量的值,然后向主内存进行回写,线程B在线程A回写完成后再从主内存读取,新变量才能对线程B可见。其实,按照虚拟机规范,volatile变量依然有工作内存的拷贝,要借助主内存来实现可见性。但由于volatile的特殊规则保证了新值能立即同步回主内存,以及每次使用从主内存刷新,以此保证了多线程操作volatile变量的可见性。
关于第二点:先说指令重排序,指令重排序是指CPU采用了允许将多条指令不按规定顺序分开发送给相应的处理单元处理,但并不是说任意重排,CPU需要正确处理指令依赖情况确保最终的正确结果,指令重排序是机器级的优化操作。那么为什么volatile要禁止指令重排序呢,又是如何去做的。举例,DCL(双重检查加锁)的单例模式。volatile修饰后,代码中将会插入许多内存屏障指令保证处理器不发生乱序执行。同时由于Happens-before规则的保证,在刚才的例子中写操作会发生在后续的读操作之前。
除了以上2点,volatile还保证对于64位long和double的读取是原子性的。因为在JMM中允许虚拟机对未被volatile修饰的64位的long和double读写操作分为2次32位的操作来执行,这也就是所谓的long和double的非原子性协定。
基于以上几点,我们知道volatile虽然有这些语义和特性在并发的情况下仍然不能保证线程安全。大部分情况下仍然需要加锁。
除非是以下2种情况,1.运算结果不依赖变量的当前值,或者能够确保只有单一线程修改变量的值;2.变量不需要与其他的状态变量共同参与不变约束。
10.Java 中,编写多线程程序的时候你会遵循哪些最佳实践?
a)给线程命名,这样可以帮助调试。
b)最小化同步的范围,而不是将整个方法同步,只对关键部分做同步。
c)如果可以,更偏向于使用 volatile 而不是 synchronized。
d)使用更高层次的并发工具,而不是使用 wait() 和 notify() 来实现线程间通信,如 BlockingQueue,CountDownLatch 及 Semeaphore。
e)优先使用并发集合,而不是对集合进行同步。并发集合提供更好的可扩展性。
11.什么是线程安全?Vector 是一个线程安全类吗
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。很显然你可以将集合类分成两组,线程安全和非线程安全的。Vector 是用同步方法来实现线程安全的, 而和它相似的 ArrayList 不是线程安全的。
12.Java 中什么是竞态条件? 举个例子说明。
竞态条件会导致程序在并发情况下出现一些 bugs。多线程对一些资源的竞争的时候就会产生竞态条件,如果首先要执行的程序竞争失败排到后面执行了,那么整个程序就会出现一些不确定的 bugs。这种 bugs 很难发现而且会重复出现,因为线程间的随机竞争。一个例子就是无序处理。
13.Java 中如何停止一个线程
当 run () 或者 call () 方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用 volatile 布尔变量来退出 run ()方法的循环或者是取消任务来中断线程。其他情形:异常 - 停止执行 休眠 - 停止执行 阻塞 - 停止执行
14.一个线程运行时发生异常会怎样
简单的说,如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler 是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候 JVM 会使用 Thread.getUncaughtExceptionHandler ()来查询线程的 UncaughtExceptionHandler 并将线程和异常作为参数传递给 handler 的 uncaughtException ()方法进行处理。
15.如何在两个线程间共享数据?
通过共享对象来实现这个目的,或者是使用像阻塞队列这样并发的数据结构
16.Java 中 notify 和 notifyAll 有什么区别
notify ()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。而 notifyAll ()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。
17.为什么 wait, notify 和 notifyAll 这些方法不在 thread 类里面
一个很明显的原因是 JAVA 提供的锁是对象级的而不是线程级的。如果线程需要等待某些锁那么调用对象中的 wait ()方法就有意义了。如果 wait ()方法定义在 Thread 类中,线程正在等待的是哪个锁就不明显了。简单的说,由于 wait,notify 和 notifyAll 都是锁级别的操作,所以把他们定义在 Object 类中因为锁属于对象。
18.什么是ThreadLocal
ThreadLocal,线程局部变量。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,每个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本,是线程隔离的。线程隔离的秘密在于ThreadLocalMap类(ThreadLocal的静态内部类)
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java 提供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。
ThreadLocal的方法:void set(T value)、T get()以及T initialValue()。
ThreadLocal是如何为每个线程创建变量的副本的:
首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。初始时,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
总结:
a.实际通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的
b.为何threadLocals的键为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal;
c.在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法
19.什么是 FutureTask?
在 Java 并发程序中 FutureTask 表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是调用了 Runnable 接口所以它可以提交给 Executor 来执行。
20.Java 中 interrupted 和 isInterruptedd 方法的区别
interrupted是静态方法,isInterruptedd是一个普通方法
如果当前线程被中断(没有抛出中断异常,否则中断状态就会被清除),你调用interrupted方法,第一次会返回true。然后,当前线程的中断状态被方法内部清除了。第二次调用时就会返回false。如果你刚开始一直调用isInterrupted,则会一直返回true,除非中间线程的中断状态被其他操作清除了。也就是说isInterrupted 只是简单的查询中断状态,不会对状态进行修改。
21.为什么 wait 和 notify 方法要在同步块中调用
如果不这么做,代码会抛出 IllegalMonitorStateException异常。还有一个原因是为了避免 wait 和 notify 之间产生竞态条件。
22.为什么你应该在循环中检查等待条件?
处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。因此,当一个等待线程醒来时,不能认为它原来的等待状态仍然是有效的,在 notify ()方法调用之后和等待线程醒来之前这段时间它可能会改变。这就是在循环中使用 wait ()方法效果更好的原因。
23.Java 中的同步集合与并发集合有什么区别
同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在 Java1.5 之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java1.5加入了并发集合像 ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。它们大部分位于JUC包下。
24.Java 中堆和栈有什么不同
每个线程都有自己的栈内存,用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其它线程是不可见的。而堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时 volatile 变量就可以发挥作用了,它要求线程从主存中读取变量的值。
25.什么是线程池? 为什么要使用它?
创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。从 JDK1.5 开始,Java API 提供了 Executor 框架让你可以创建不同的线程池。比如单线程池,每次处理一个任务;数目固定的线程池或者是缓存线程池(一个适合很多生存期短的任务的程序的可扩展线程池)。
26.如何写代码来解决生产者消费者问题?
在现实中你解决的许多线程问题都属于生产者消费者模型,就是一个线程生产任务供其它线程进行消费,你必须知道怎么进行线程间通信来解决这个问题。比较低级的办法是用 wait 和 notify 来解决这个问题,比较赞的办法是用 Semaphore 或者 BlockingQueue 来实现生产者消费者模型。
27.如何避免死锁?
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足以下四个条件:
互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。
28.Java 中活锁和死锁有什么区别?
活锁和死锁类似,不同之处在于处于活锁的线程或进程的状态是不断改变的,活锁可以认为是一种特殊的饥饿。一个现实的活锁例子是两个人在狭小的走廊碰到,两个人都试着避让对方好让彼此通过,但是因为避让的方向都一样导致最后谁都不能通过走廊。简单的说就是,活锁和死锁的主要区别是前者进程的状态可以改变但是却不能继续执行。
29.怎么检测一个线程是否拥有锁
在 java.lang.Thread 中有一个方法叫 holdsLock (),当且仅当当前线程拥有某个具体对象的锁时它返回true。
30.你如何在 Java 中获取线程堆栈
eak 组合键来获取线程堆栈,Linux 下用 kill -3 命令。你也可以用 jstack 这个工具来获取,它对线程 id 进行操作,你可以用 jps 这个工具找到 id。
31.JVM内存配置参数
1.-Xmx:最大堆大小
2.-Xms:初始堆大小(最小内存值)
3.-Xmn:年轻代大小
4.-XXSurvivorRatio:3 意思是Eden:Survivor=3:2
5.-Xss栈容量
6.-XX:+PrintGC 输出GC日志
7.-XX:+PrintGCDetails 输出GC的详细日志
32.Java 中 synchronized 和 ReentrantLock 有什么不同
Java 在过去很长一段时间只能通过 synchronized 关键字来实现互斥,它有一些缺点。比如你不能扩展锁之外的方法或者块边界,尝试获取锁时不能中途取消等。Java 5 通过 Lock 接口提供了更复杂的控制来解决这些问题。 ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义且它还具有可扩展性。
33.有三个线程 T1,T2,T3,怎么确保它们按顺序执行
可以用线程类的 join ()方法。具体操作是在T3的run方法中调用t2.join(),让t2执行完再执行t3;T2的run方法中调用t1.join(),让t1执行完再执行t2。这样就按T1,T2,T3的顺序执行了
34.Thread 类中的 yield 方法有什么作用
Yield 方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃 CPU 占用而不能保证使其它线程一定能占用 CPU,执行 yield ()的线程有可能在进入到暂停状态后马上又被执行。
35.Java 中 ConcurrentHashMap 的并发度是什么
ConcurrentHashMap 把实际 map 划分成若*分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16,这样在多线程情况下就能避免争用。
36.Java 中 Semaphore是什么
JUC下的一种新的同步类,它是一个计数信号。从概念上讲,Semaphore信号量维护了一个许可集合。如有必要,在许可可用前会阻塞每一个 acquire (),然后再获取该许可。每个 release ()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。信号量常常用于多线程的代码中,比如数据库连接池。
37.如果你提交任务时,线程池队列已满。会发会生什么?
这个问题问得很狡猾,许多程序员会认为该任务会阻塞直到线程池队列有空位。事实上如果一个任务不能被调度执行那么 ThreadPoolExecutor’s submit ()方法将会抛出一个 RejectedExecutionException 异常。
38.Java 线程池中 submit () 和 execute ()方法有什么区别
两个方法都可以向线程池提交任务,execute ()方法的返回类型是 void,它定义在 Executor 接口中, 而 submit ()方法可以返回持有计算结果的 Future 对象,它定义在 ExecutorService 接口中,它扩展了 Executor 接口,其它线程池类像 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 都有这些方法。
39.什么是阻塞式方法?
阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket 的 accept ()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。
40.Swing 是线程安全的吗?
你可以很肯定的给出回答,Swing 不是线程安全的。你不能通过任何线程来更新 Swing 组件,如 JTable、JList 或 JPanel,事实上,它们只能通过 GUI 或 AWT 线程来更新。这就是为什么 Swing 提供 invokeAndWait() 和 invokeLater() 方法来获取其他线程的 GUI 更新请求。这些方法将更新请求放入 AWT 的线程队列中,可以一直等待,也可以通过异步更新直接返回结果。
41.Java 中 invokeAndWait 和 invokeLater 有什么区别
这两个方法是 Swing API 提供给 Java 开发者用来从当前线程而不是事件派发线程更新 GUI 组件用的。InvokeAndWait ()同步更新 GUI 组件,比如一个进度条,一旦进度更新了,进度条也要做出相应改变。如果进度被多个线程跟踪,那么就调用 invokeAndWait ()方法请求事件派发线程对组件进行相应更新。而 invokeLater ()方法是异步调用更新组件的。
42.Swing API 中那些方法是线程安全的?
虽然Swing不是线程安全的但是有一些方法是可以被多线程安全调用的。如repaint (), revalidate ()。 JTextComponent 的 setText ()方法和 JTextArea 的 insert () 和 append () 方法也是线程安全的。
43.如何在 Java 中创建 Immutable 对象
Immutable 对象可以在没有同步的情况下共享,降低了对该对象进行并发访问时的同步化开销。可是 Java 没有@Immutable 这个注解符,要创建不可变类,要实现下面几个步骤:通过构造方法初始化所有成员、对变量不要提供 setter 方法、将所有的成员声明为私有的,这样就不允许直接访问这些成员、在 getter 方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝。
44.Java 中的 ReadWriteLock 是什么?
一般而言,读写锁是用来提升并发程序性能的锁分离技术的成果。Java 中的 ReadWriteLock 是 Java 5 中新增的一个接口,一个 ReadWriteLock 维护一对关联的锁,一个用于只读操作一个用于写。在没有写线程的情况下一个读锁可能会同时被多个读线程持有。写锁是独占的,你可以使用 JDK 中的 ReentrantReadWriteLock 来实现这个规则,它最多支持 65535 个写锁和 65535 个读锁。
45.多线程中的忙循环是什么?
忙循环就是程序员用循环让一个线程等待,不像传统方法 wait (), sleep () 或 yield () 它们都放弃了 CPU 控制,而忙循环不会放弃 CPU,它就是在运行一个空循环。这么做的目的是为了保留 CPU 缓存,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。
46.volatile 变量和 atomic 变量有什么不同
volatile 变量和 atomic 变量看起来很像,但功能却不一样。volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量那么 count++ 操作并不是原子性的。而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如 getAndIncrement ()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。
47.如果同步块内的线程抛出异常会发生什么?
无论你的同步块是正常还是异常退出的,里面的线程都会释放锁,所以对比锁接口我更喜欢同步块,因为它不用我花费精力去释放锁,该功能可以在 finally block 里释放锁实现。
48.如何在 Java 中创建线程安全的 Singleton
5种,急加载,同步方法,双检锁,静态内部类,枚举
49.如何强制启动一个线程?
这个问题就像是如何强制进行 Java 垃圾回收,目前还没有觉得方法,虽然你可以使用 System.gc ()来进行垃圾回收,但是不保证能成功。在 Java 里面没有办法强制启动一个线程,它是被线程调度器控制着且 Java 没有公布相关的 API。
50.Java 中的 fork join 框架是什么?
fork join 框架是 JDK7 中出现的一款高效的工具,Java 开发人员可以通过它充分利用现代服务器上的多处理器。它是专门为了那些可以递归划分成许多子模块设计的,目的是将所有可用的处理能力用来提升程序的性能。fork join 框架一个巨大的优势是它使用了工作窃取算法,可以完成更多任务的工作线程可以从其它线程中窃取任务来执行。
51.Java 多线程中调用 wait () 和 sleep ()方法有什么不同?
Java 程序中 wait 和 sleep 都会造成某种形式的暂停,它们可以满足不同的需要。wait ()方法意味着条件等待,如果等待条件为真且其它线程被唤醒时它会释放锁,而 sleep ()方法仅仅释放 CPU 资源或者让当前线程短暂停顿,但不会释放锁。
52.双亲委派模型中的方法
findLoadedClass(),LoadClass(),findBootstrapClassOrNull(),findClass(),resolveClass()
53.NIO、AIO、BIO
BIO即同步阻塞IO,适用于连接数目较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4之前的唯一选择,但程序直观、简单、易理解。
NIO即同步非阻塞IO,适用于连接数目多且连接比较短的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
AIO即异步非阻塞IO,适用于连接数目多且连接比较长的架构,如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK1.7开始支持
GC、内存相关
1.对哪些区域回收
Java运行时数据区域:程序计数器、JVM栈、本地方法栈、方法区和堆。
由于程序计数器、JVM栈、本地方法栈3个区域随线程而生随线程而灭,对这几个区域内存的回收和分配具有确定性。而方法区和堆则不一样,程序需要在运行时才知道创建哪些对象,对这部分内存的分配是动态的,GC关注的也就是这部分内存。
2.如何判定对象需要回收
引用计数法:给对象加上一个计数器,当有一个地方引用它,计数器+1,引用失效时,计数器-1,当计数器为0时,判定该对象可回收。引用计数法优点是实现简单,python,flashplayer等使用引用计数法进行内存管理。引用计数法的缺点在于无法解决循环引用的问题。
在Java中使用可达性分析算法法判定对象是否“死亡”。可达性分析法是指通过称为GC-Roots的对象为起始点,从这些结点向下搜索,当从GCRoots到这个对象不可达时,被判定为可收回的对象。
3.可作为GC Roots的对象
可作为GC Roots的对象:虚拟机栈中引用的对象 方法区中静态属性引用的对象 方法区中常量引用的对象 本地方法栈中JNI引用的对象
4.对象的自我救赎
即使在可达性算法中判定为不可达时,也并非一定被回收。对象存在自我救赎的可能。要真正宣告对象的死亡,需要经历2次标记的过程。如果对象经过可达性分析法发现不可达时,对象将被第一次标记被进行筛选,筛选的条件是此对象是否有必要执行finalize方法。如果对象没有重写finalize方法或finalize方法已经被JVM调用过,则判定为不需要执行。
如果对象被判定为需要执行finalize方法,该对象将被放置在一个叫做F-Queue的队列中,JVM会建立一个低优先级的线程执行finalize方法,如果对象想要完成自我救赎需要在finalize方法中与引用链上的对象关联,比如把自己也就是this赋值给某个类变量。当GC第二次对F-Queue中对象标记时,该对象将被移出“即将回收”的集合,完成自我救赎。简言之,finalize方法是对象逃脱死亡命运的最后机会,并且任何对象的finalize方法只会被JVM调用一次。
5.垃圾回收算法
Mark-Sweep法:标记清除法 容易产生内存碎片,导致分配较大对象时没有足够的连续内存空间而提前出发GC。这里涉及到另一个问题,即对象创建时的内存分配,对象创建内存分配主要有2种方法,分别是指针碰撞法和空闲列表法。指针碰撞法:使用的内存在一侧,空闲的在另一侧,中间使用一个指针作为分界点指示器,对象内存分配时只要指针向空闲的移动对象大小的距离即可。
空闲列表法:使用的和空闲的内存相互交错无法进行指针碰撞,JVM必须维护一个列表记录哪些内存块可用,分配时从列表中找出一个足够的分配给对象,并更新列表记录。所以,当采用Mark-Sweep算法的垃圾回收器时,内存分配通常采用空闲列表法。
Copy法:将内存分为2块,每次使用其中的一块,当一块满了,将存活的对象复制到另一块,把使用过的那一块一次性清除。显然,Copy法解决了内存碎片的问题,但算法的代价是内存缩小为原来的一半。现代的垃圾收集器对新生代采用的正是Copy算法。但通常不执行1:1的策略,HotSpot虚拟机默认Eden区Survivor区8:1。每次使用Eden和其中一块Survivor区。也就是说新生代可用内存为新生代内存空间的90%。
Mark-Compact法:标记整理法。它的第一阶段与Mark-Sweep法一样,但不直接清除,而是将存活对象向一端移动,然后清除端边界以外的内存,这样也不存在内存碎片。
分代收集算法:将堆内存划分为新生代,老年代,根据新生代老年代的特点选取不同的收集算法。因为新生代对象大多朝生夕死,而老年代对象存活率高,没有额外空间进行分配担保,通常对新生代执行复制算法,老年代执行Mark-Sweep算法或Mark-Compact算法。
6.垃圾收集器
通常来说,新生代老年代使用不同的垃圾收集器。新生代的垃圾收集器有Serial(单线程)、ParNew(Serial的多线程版本)、ParallelScavenge(吞吐量优先的垃圾收集器),老年代有SerialOld(单线程老年代)、ParallelOld(与ParallelScavenge搭配的多线程执行标记整理算法的老年代收集器)、CMS(标记清除算法,容易产生内存碎片,可以开启内存整理的参数),以及当前最先进的垃圾收集器G1,G1通常面向服务器端的垃圾收集器,在我自己的Java应用程序中通过-XX:+PrintGCDetails,发现自己的垃圾收集器是使用了ParallelScavenge+ParallelOld的组合。
7.内存分配和回收的策略
1.对象优先在Eden区分配,默认Eden与Survivor的比例为8:1
2.大对象直接进入老年代
3.长期存活的进入老年代:JVM给每个对象定义一个年龄计数器,当对象在Eden区出生并躲过一次MinorGC,并且Survivor可以容纳的话,将被移入Survivor区,年龄设为1。以后每在Survivor区躲过一次MinorGC,年龄加一岁,当对象年龄加到15岁时,晋升到老年代。当然15岁的默认值可以通过-XX虚拟机参数设置。
4.动态对象年龄判定:有的时候无需到达15岁即晋升老年代。判定方法是如果Survivor区中相同年龄的所有对象大小的总和大于Survivor区空间的一半,年龄大于或等于该年龄的对象直接进入老年代
5.空间分配担保
在发生MinorGC之前,虚拟机会检查老年代最大可用连续空间是否大于新生代所有对象总和,如果成立,确保这次MinorGC安全。否则,虚拟机会查看HandlePromotionFailure设置是否允许担保失败。如果允许,虚拟机会接着查看老年代最大连续可用空间是否大于历次晋升到老年代对象的平均大小,如果大于,则进行一次MinorGC,尽管这次MinorGC是有风险的,如果小于或者HandlePromotionFailure设置为不允许,要改为一次FullGC
8.方法区的回收
方法区通常会与永久代划等号,实际上二者并不等价,只不过是HotSpot虚拟机设计者用永久代实现方法区,并将GC分代扩展至方法区。
永久代垃圾回收通常包括两部分内容:废弃常量和无用的类。常量的回收与堆区对象的回收类似,当没有其他地方引用该字面量时,如果有必要,将被清理出常量池。
判定无用的类的3个条件:
1.该类的所有实例都已经被回收,也就是说堆中不存在该类的任何实例
2.加载该类的ClassLoader已经被回收
3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
当然,这也仅仅是判定,不代表立即卸载该类。
9.Java中有内存泄漏吗?
内存泄露的定义: 当某些对象不再被应用程序所使用,但是由于仍然被引用而导致垃圾收集器不能释放。
内存泄漏的原因:对象的生命周期不同。比如说
对象A引用了对象B. A的生命周期比B的要长得多,当对象B在应用程序中不会再被使用以后, 对象 A 仍然持有着B的引用. (根据虚拟机规范)在这种情况下GC不能将B从内存中释放. 这种情况很可能会引起内存问题,倘若A还持有着其他对象的引用,那么这些被引用的(无用)对象也不会被回收,并占用着内存空间。甚至有可能B也持有一大堆其他对象的引用。这些对象由于被 B 所引用,也不会被垃圾收集器所回收,所有这些无用的对象将消耗大量宝贵的内存空间。并可能导致内存泄漏
怎样防止:
1、当心集合类,比如HashMap,ArrayList等,因为这是最容易发生内存泄露的地方.当集合对象被声明为static时,他们的生命周期一般和整个应用程序一样长。
10.OOM解决办法、
内存溢出的空间:Permanent Generation和Heap Space,也就是永久代和堆区
第一种情况永久代的溢出:出现这种问题的原因可能是应用程序加载了大量的jar或class,使虚拟机装载类的空间不够,与Permanent Generation Space的大小有关。
解决办法有2种:1、通过虚拟机参数-XX:PermSize和-XX:MaxPermSize调整永久代大小 2、清理程序中的重复的Jar文件,减少类的重复加载
第二种堆区的溢出:发生这种问题的原因是java虚拟机创建的对象太多,在进行垃圾回收之间,虚拟机分配的到堆内存空间已经用满了,与Heap Space的size有关。解决这类问题有两种思路:
1、检查程序,看是否存在死循环或不必要地重复创建大量对象,定位原因,修改程序和算法。
2、通过虚拟机参数-Xms和-Xmx设置初始堆和最大堆的大小
11.DirectMemory直接内存
直接内存并不是Java虚拟机规范定义的内存区域的一部分,但是这部分内存也被频繁使用,而且也可能导致OOM异常的出现。
JDK1.4引入了NIO,这是一种基于通道和缓冲区的非阻塞IO模式,它可以使用Native函数库分配直接堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,使得在某些场合显著提高性能,因为它避免了在Java堆和本地堆之间来回复制数据。
算法与数据结构
1.如何判断链表是否有环
方法1:快慢指针法 2.设两个工作指针p、q,p总是向前走,但q每次都从头开始走,对于每个节点,看p走的步数是否和q一样。比如p从A走到D,用了4步,而q则用了14步。因而步数不等,出现矛盾,存在环。
2.红黑树
二叉搜索树:(Binary Search Tree又名:二叉查找树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉搜索树。
红黑树是一棵二叉搜索树,它在每个结点上增加一个存储位来表示结点的颜色,可以是RED或BLACK。通过对任何一条从根到叶子的简单路径上各个结点的颜色进行约束,红黑树没有一条路径会比其他路径长出2倍,所以红黑树是近似平衡的,使得红黑树的查找、插入、删除等操作的时间复杂度最坏为O(log n),但需要注意到在红黑树上执行插入或删除后将不在满足红黑树性质,恢复红黑树的属性需要少量(O(log
n))的颜色变更(实际是非常快速的)和不超过三次树旋转(对于插入操作是两次)。虽然插入和删除很复杂,但操
作时间仍可以保持为 O(log n) 次。具体如何保证?引出红黑树的5个性质。
红黑树的5个性质:满足以下五个性质的二叉搜索树
- 每个结点或是红色的或是黑色的
- 根结点是黑色的
- 每个叶结点是黑色的
- 如果一个结点是红色的,则它的两个子结点是黑色的
- 对于每个结点,从该结点到其后代叶结点的简单路径上,均包含相同数目的黑色结点
插入操作:
由于性质的约束,插入的结点都是红色的。插入时性质1、3始终保持。破坏性质2当且仅当当前插入结点为根节点。变一下颜色即可。如果是破坏性质4或5,则需要旋转和变色来继续满足红黑树的性质。下面说一说插入的几种情况,约定当前插入结点为N,其父结点为P,叔叔为U,祖父为G
情形1:树空,直接插入违反性质1,将红色改黑。
情形2:N的父结点为黑,不必修改,直接插入
从情形3开始的情形假定N结点的父结点P为红色,所以存在G,并且G为黑色。且N存在一个叔叔结点U,尽管U可能为叶结点。
情形3:P为红,U为红(G结点一定存在且为黑)这里不论P是G的左孩子还是右孩子;不论N是P的左孩子还是右孩子。
首先把P、U改黑,G改红,并以G作为一个新插入的红结点重新进行各种情况的检查,若一路检索至根节点还未结束,则将根结点变黑。
情形4:P为红,U为黑或不存在(G结点一定存在且为黑),且P为G的左孩子,N为P的左孩子(或者P为G的右孩子,N为P的右孩子,保证同向的)。
P、G右旋并将P、G变相反色。因为P取代之前黑G的位置,所以P变黑可以理解,而G变红是为了不违反性质5。
情形5:P为红,U为黑或不存在,且P为G的左孩子,N为P的右孩子(或P为G的右孩子,N为P的左孩子,保证是反向的),对N,P进行一次左旋转换为情形4
删除操作比插入复杂一些,但最多不超过三次旋转可以让红黑树恢复平衡。
其他
- 黑高从某个结点x出发(不含x)到达一个叶结点的任意一条简单路径上的黑色结点个数称为该结点的黑高。红黑树的黑高为其根结点的黑高。
- 一个具有n个内部结点的红黑树的高度h<=2lg(n+1)
- 结点的属性(五元组):color key left right p
- 动态集合操作最坏时间复杂度为O(lgn)
3.数据库索引的实现
数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。
数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。
B树:
为了描述B-Tree,首先定义一条数据记录为一个二元组[key, data],key为记录的键值,对于不同数据记录,key是互不相同的;data为数据记录。那么B-Tree是满足下列条件的数据结构:
d为大于1的一个正整数,称为B-Tree的度。用来表示每个结点包含的关键字个数的上界和下界。可以证明h<=logd((N+1)/2)
h为一个正整数,称为B-Tree的高度。
每个非叶子节点由n-1个key和n个指针组成,其中d<=n<=2d。
每个叶子节点最少包含一个key和两个指针,最多包含2d-1个key和2d个指针,叶节点的指针均为null 。
所有叶节点具有相同的深度,等于树高h。
key和指针互相间隔,节点两端是指针。
一个节点中的key从左到右非递减排列。
所有节点组成树结构。
每个指针要么为null,要么指向另外一个节点。
如果某个指针在节点node最左边且不为null,则其指向节点的所有key小于v(key1),其中v(key1)为node的第一个key的值。
如果某个指针在节点node最右边且不为null,则其指向节点的所有key大于v(keym),其中v(keym)为node的最后一个key的值。
如果某个指针在节点node的左右相邻key分别是keyi和keyi+1且不为null,则其指向节点的所有key小于v(keyi+1)且大于v(keyi)。
由于B-Tree的特性,在B-Tree中按key检索数据的算法非常直观:首先从根节点进行二分查找,如果找到则返回对应节点的data,否则对相应区间的指针指向的节点递归进行查找,直到找到节点或找到null指针,前者查找成功,后者查找失败。
一个度为d的B-Tree,设其索引N个key,则其树高h的上限为logd((N+1)/2),检索一个key,其查找节点个数的渐进复杂度为O(logdN)。从这点可以看出,B-Tree是一个非常有效率的索引数据结构。
B+树:
B-Tree有许多变种,其中最常见的是B+Tree,例如MySQL就普遍使用B+Tree实现其索引结构。
B+树是B树的变形,它把所有的data都放在叶子结点中,只将关键字和子女指针保存于内结点,内结点完全是索引的功能。
与B-Tree相比,B+Tree有以下不同点:
1、每个节点的指针上限为2d而不是2d+1。
2、内节点不存储data,只存储key;叶子节点存储data不存储指针。
一般在数据库系统或文件系统中使用的B+Tree结构都在经典B+Tree的基础上进行了优化,增加了顺序访问指针。
在B+Tree的每个叶子节点增加一个指向相邻叶子节点的指针
例如图4中如果要查询key为从18到49的所有数据记录,当找到18后,只需顺着节点和指针顺序遍历就可以一次性访问到所有数据节点,极大提到了区间查询效率。
为什么B树(B+树)?
一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。
这涉及到磁盘存取原理、局部性原理和磁盘预读。
先从B-Tree分析,根据B-Tree的定义,可知检索一次最多需要访问h个节点。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。为了达到这个目的,在实际实现B-Tree还需要使用如下技巧:
每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个node只需一次I/O。
B-Tree中一次检索最多需要h-1次I/O(根节点常驻内存),渐进复杂度为O(h)=O(logdN)。一般实际应用中,出度d是非常大的数字,通常超过100,因此h非常小(通常不超过3)。
综上所述,用B-Tree作为索引结构效率是非常高的。
而红黑树这种结构,h明显要深的多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,所以红黑树的I/O渐进复杂度也为O(h),效率明显比B-Tree差很多。
至于B+Tree为什么更适合外存索引,原因和内节点出度d有关。
由于B+Tree内节点去掉了data域,因此可以拥有更大的出度,拥有更好的性能。
4.一致性Hash
第一:简单介绍
一致性哈希算法是分布式系统中常用的算法。比如,一个分布式的存储系统,要将对象存储到具体的节点上,如果采用普通的hash方法,将数据映射到具体的节点上,如key%N,N是机器节点数。
1、考虑到比如一个服务器down掉,服务器结点N变为N-1,映射公式必须变为key%(N-1)
2、访问量加重,需要添加服务器结点,N变为N+1,映射公式变为hash(object)%(N+1)
当出现1,2的情况意味着我们的映射都将无效,对服务器来说将是一场灾难,尤其是对缓存服务器来说,因为缓存服务器映射的失效,洪水般的访问都将冲向后台服务器。
第二点:hash算法的单调性
Hash 算法的一个衡量指标是单调性,单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。
consistent hash 也是一种hash 算法,简单的说,在移除 / 添加一个结点时,它能够尽可能小的改变已存在的映射关系,尽可能的满足单调性的要求。
第三点:将对象和服务器结点分别映射到环型空间
通常的一致性哈希做法是将 value 映射到一个 32 位的 key 值,也即是 0~2^32-1 次方的数值空间;我们可以将这个空间想象成一个首( 0 )尾( 2^32-1 )相接的圆环。
我们可以通过hash函数将我们的key映射到环型空间中,同时根据相同的哈希算法把服务器也映射到环型空间中,顺便提一下服务器或者某个计算节点的 hash 计算,一般的方法可以使用机器的 IP 地址或者机器名作为 hash 输入。
第四点:将对象映射到服务器
在这个环形空间中,如果沿着顺时针方向从对象的 key 值出发,直到遇见一个 服务器结点,那么就将该对象存储在这个服务器结点上,因为对象和服务器的hash 值是固定的,因此这个 cache 必然是唯一和确定的。
这时候考察某个服务器down机或者需要添加服务器结点,也就是移除和添加的操作,我们只需要几个对象的映射。
第五点:虚拟结点
Hash 算法的另一个指标是平衡性 (Balance)。平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。
对于上述的做法,可能导致某些对象都映射到某个服务器,使得分布不平衡。为此可以采用“虚拟结点”的做法。
“虚拟结点”( virtual node )是实际节点在 hash 空间的复制品,一实际结点对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以 hash 值排列。引入“虚拟结点”会让我们的映射分布更为平衡一些。
引入“虚拟结点”前:
Hash(“192.168.1.1”);
引入“虚拟结点”后:
Hash(“192.168.1.1#1”);
Hash(“192.168.1.1#2”);
JavaEE
1、解析XML
DOM、SAX、JDOM、DOM4J
- [x] DOM DOM树驻留内存
可以进行修改和写入,耗费内存。
步骤:创建DocumentBuilderFactory对象 -> 创建DocumentBuilder对象 -> Document document = db.parse(“xml”)
- [x] SAX 事件驱动模式
获取一个SAXParserFactory工厂的实例 -> 根据该实例获取SAXParser -> 创建Handler对象 -> 调用SAXParser的parse方法解析
用于读取节点数据 不易编码 事件有顺序 很难同时访问xml的多处数据
- [x] JDOM
创建一个SAXBuilder的对象 -> 创建一个输入流,加载xml文件 ->通过saxBuilder的build方法将输入流加载至saxBuilder并接收Document对象
使用具体类而不使用接口
- [x] DOM4J
通过SAXReader的read方法加载xml文件并获取document对象
使用接口和抽象类,灵活性好,功能强大
其他
1.开源软件有哪些?
Eclipse、Linux及其Linux下的大多数软件、Git等。
Apache下的众多软件:Lucene、Velocity、Maven、高性能Java网络框架MINA、版本控制系统SVN、应用服务器Tomcat、Http服务器Apache、MVC框架Struts、持久层框架iBATIS、Apache SPARK、ActiveMQ
2.开源协议
MIT:相对宽松。适用:JQuery
Apache:相对宽松与MIT类似的协议,考虑有专利的情况。适用:Apache服务器、SVN
GPL:GPLV2和GPLV3,如果你在乎作品的传播和别人的修改,希望别人也以相同的协议分享出来。
LGPL:主要用于一些代码库。衍生代码可以以此协议发布(言下之意你可以用其他协议),但与此协议相关的代码必需遵循此协议。
BSD:较为宽松的协议,包含两个变种BSD 2-Clause 和BSD 3-Clause,两者都与MIT协议只存在细微差异。
上面各协议只是针对软件或代码作品,如果你的作品不是代码,比如视频,音乐,图片,文章等,共享于公众之前,也最好声明一下协议以保证自己的权益不被侵犯,CC协议。
上一篇: java 基础 面试题