计算机⽹络
联想 Java ⾯试
⼤家⾯试联想的时候,可能需要准备好 英语表达,⾯试过程中可能会要求你⽤英语来回答问题,
联想因为有很多国外的同事,⼯作中也 经常跟国外的同事协助,所以英语是会在⼯作中⽇常交流
的。
甚⾄,可能有⼀⾯的⾯试官,就是外国⼈⾯试官,这种情况,就需要全程英语交流。 联想的⼯作
氛围还是不错,不加班,听说朝九晚六,⼗⼏天年假,妥妥 的外企⻛格,羡慕了。
这次,来分享⼀位同学的联想 Java 后端校招的⾯经,主要是问⼋股⽐较多,但是整体上不 算难,
都是⽐较基础的问题,主要就问了计算机⽹络、操作系统、Java 集合⼀些问题。
技术⾯之后,就进⾏英语对话环节了,需要⽤英语说 ⼀下项⽬中遇到的最⼤问题,这最好提前准
备⼀下,当场⽤英语去表达,肯定讲的不好。

计算机⽹络
协议是哪⼀层的?
在⽹络层。
TCP/IP ⽹络通常是由上到下分成 4 层,分别 是应⽤层,传输层,⽹络层和⽹络接⼝层。

应⽤层 ⽀持 HTTP 、SMTP 等最终⽤⼾进程
传输层 处理主机到主机的通信(TCP 、UDP )
⽹络层 寻址和路由数据包(IP 协议)
链路层 通过⽹络的物理电线、电缆或⽆线信道移动⽐特
域名转化为IP 地址 ⽤到了什么 协议?
dns 协议
说说 DNS 的解析过程。
客⼾端⾸先会发出⼀个 DNS 请求,问 www.server.com 的 IP 是啥,并发给本地 DNS 服务器(也就是客⼾端的 TCP/IP 设置中填写的 DNS 服务器地址 )。
本地域 名服务器收到客⼾端的请求后,如果缓存⾥的表格能找到 www.server.com ,则它直接
返回 IP 地址 。如果没有,本地 DNS 会去问它的根域名服务器:“⽼⼤, 能告诉我
www.server.com 的 IP 地址 吗?” 根域名服务器是最⾼层次的,它不直接⽤于域名解析,但能
指明⼀条道路。
- 根 DNS 收到来⾃本地 DNS 的请求后,发现后置是 .com ,说:“ www.server.com 这个域名归
.com 区域管理”,我给你 .com 顶级域名服务器地址 给你,你去问问 它吧。”
- 本地 DNS 收到顶级域名服务器的地址 后,发起请求问“⽼⼆, 你能告诉我 www.server.com 的
IP 地址 吗?”
- 顶级域名服务器说:“我给你负责 www.server.com 区域的权威 DNS 服务器的地址 ,你去问它
应该能问到”。
- 本地 DNS 于是转向问权威 DNS 服务器:“⽼三, www.server.com 对应的IP 是啥
呀?”server.com 的权威 DNS 服务器, 它是域名解析结果的原出处。为啥叫权威呢?就是我的
域名我做主。
权威 DNS 服务器查询后将对 应的 IP 地址 X.X.X.X 告诉本地 DNS 。
本地 DNS 再将 IP 地址 返回客⼾端,客⼾端和⽬标建⽴连接。
⾄此,我们完成了 DNS 的解析过程。现在总结⼀下,整个过程我画成了⼀个图。

改过host ⽂件吗?
hosts ⽂件是⼀个系统⽂件,⽤于将主机名映射到 IP 地址 。在某些情况下,它可以⽤于进⾏域
名解析,类似于 DNS 的功能。通常在操作系统中, hosts ⽂件的位置如下:
Windows : C:\Windows\System32\drivers\etc\hostsLinux/Unix/Mac : /etc/hosts hosts ⽂件通常包含多⾏,每⾏包含⼀个 IP 地址 和⼀个或多个主机名,格式如下:
P地址 主机名 [别名]
127.0.0.1 localhost
192.168.1.10 my-server
使⽤场景:
本地开发:可以将特定域名指向本地服务器的 IP 地址 ,以便 于开发和测试。
阻⽌⽹站:可以通过将⽹站域名指向 127.0.0.1 来阻⽌访问,例如将 example.com 指向
127.0.0.1 可以阻⽌该⽹站的访问。
- 加速解析:对于常⽤的⾃家服务器, 可以直接通过 hosts ⽂件来加速⽹络请求的解析。
P地址 主机名 [别名 ]127.0.0.1 localhost 192.168.1.10 my-server
https 的握⼿过程?
传统的 TLS 握⼿基本都是使⽤ RSA 算法来实现密钥交换的,在将 TLS 证书部署服务端时,证书⽂
件其实就是服务端的公钥,会在 TLS 握⼿阶段传递给客⼾端,⽽服务端的私钥则⼀直留在服务
端,⼀定要确保私钥不能被窃取。
在 RSA 密钥协商算法中,客⼾端会⽣成随机密钥,并使⽤服务端的公钥加密后再传给服务端。根
据⾮对称加密算法,公钥加密的消息仅能通过私钥解密,这样服务端解密后,双⽅就得到了相同
的密钥,再⽤它加密应⽤消息。
我⽤ Wireshark ⼯具抓了⽤ RSA 密钥交换的 TLS 握⼿过程,你可以从下 ⾯看到,⼀共经历了四次
握⼿: 

TLS 第⼀次握⼿
⾸先,由客⼾端向服务器发起加密通信请求,也就是 ClientHello 请求。在这⼀步,客⼾端主要向
服务器发送以下信息:
(1)客⼾端⽀持的 TLS 协议版本,如 TLS 1.2 版本。
(2)客⼾端⽣产的随机数(Client Random ),后⾯⽤于⽣成「会话秘钥」条件之⼀。
(3)客⼾端⽀持的密码套件列表,如 RSA 加密算法。
TLS 第⼆次握⼿
服务器收到客⼾端请求后,向客⼾端发出响应,也就是 SeverHello 。服务器回应的内容有如下内
容:
(1)确认 TLS 协议版本,如果浏览器不⽀持,则关闭加密通信。
(2)服务器⽣产的随机数(Server Random ),也是后⾯⽤于⽣产「会话秘钥」条件之⼀。
(3)确认的密码套件列表,如 RSA 加密算法。(4)服务器的数字证书。
TLS 第三次握⼿
客⼾端收到服务器的回应之后,⾸先通过浏览器或者操作系统中的 CA 公钥,确认服务器的数字证
书的真实性。
如果证书没有问题,客⼾端会从数字证书中 取出服务器的公钥,然后使⽤它加密报⽂,向服务器
发送如下信息:
(1)⼀个随机数(pre-master key )。该随机数会被服务器公钥加密。
(2)加密通信算法改变通知,表⽰随后的信息都将⽤「会话秘钥」加密通信。
(3)客⼾端握⼿结束通知,表⽰客⼾端的握⼿阶段已经结 束。这⼀项同时把之前所有内容的发
⽣的数据做个摘要,⽤来供服务端校验。
上⾯第⼀项的随机数是整个握⼿阶段的第三个 随机数,会发给服务端,所以这个随机数客⼾端和
服务端都是⼀样的。
服务器和客⼾端有了这三个 随机数(Client Random 、Server Random 、pre-master key ),接着
就⽤双⽅协商的加密算法,各⾃⽣成本次通信的「会话秘钥」。
TLS 第四次握⼿
服务器收到客⼾端的第三个 随机数(pre-master key )之后,通过协商的加密算法,计算出本次通
信的「会话秘钥」。
然后,向客⼾端发送最后的信息:
(1)加密通信算法改变通知,表⽰随后的信息都将⽤「会话秘钥」加密通信。
(2)服务器握⼿结束通知,表⽰服务器的握⼿阶段已经结 束。这⼀项同时把之前所有内容的发
⽣的数据做个摘要,⽤来供客⼾端校验。
⾄此,整个 TLS 的握⼿阶段全部结束。接下来,客⼾端与服务器进⼊加密通信,就完全是使⽤普
通的 HTTP 协议,只不过⽤「会话秘钥」加密内容。
操作系统
死锁是什么 ?怎么产 ⽣的?
死锁只有同时满⾜以下四个条件才会发⽣:
互斥条件:互斥条件是指多个线程不能同时使⽤同⼀个资源。
持有并等待条件:持有并等待条件是指,当线程 A 已经持有了资源 1,⼜想申请资源 2,⽽资
源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时
并不会释放⾃⼰已经持有的资源 1。
不可剥夺条件:不可剥夺条件是指,当线程已经持有了资源 ,在⾃⼰使⽤完之前不能被其他线
程获取,线程 B 如果也想使⽤此资源,则只能在线程 A 使⽤完并释放后才能获取。
环路等待条件:环路等待条件指的是,在死锁发⽣的时候,两个 线程获取资源的顺序构成了环
形链。
如何避免死锁?
避免死锁问题就只需要破环其中⼀个条件就可以,最常⻅的并且可⾏的就是使⽤资源有序分配
法,来破环环 路等待条件。
那什么 是资源有序分配法呢?线程 A 和 线程 B 获取资源的顺序要⼀样,当线程 A 是先尝试获取资
源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也
就是说,线程 A 和 线程 B 总是以相同的顺序申请⾃⼰想要的资源。

Java
常⽤的数据结构,简单说⼀下
了解数组、哈希表、链表、栈、队列、⼆叉树、b+ 树等。
Java 中的ArrayList 了解吗?
ArrayList 是容量可变的⾮线程安全列表,其底层使⽤数组实现,当⼏何扩容时,会创建更⼤的
数组,并把原数组复制到 新数 组。ArrayList ⽀持对元素的快速随机访问,但插⼊与删除速度很
慢。
ArrayList 适⽤于需要频繁访问集 合元素的场景。它基于数组实现,可以通过索引快 速访问元
素,因此在按索引查找、遍历和随机访问元素的操作上具有较⾼的性能。当需要频繁访问和遍
历集合元素,并且集合⼤⼩不经常改变时,推荐使⽤ArrayList
ArrayList 插⼊元素的过程是怎样的?
插⼊的过程分为:
** 在 ArrayList 的末尾插⼊元素,** 当我们向 ArrayList 的末尾插⼊元素时,只需将新元素添加到
内部数组的最后⼀个位置即可,不需要移动其他元素。因此,该操作的时间复杂度是 O(1) 。** 在 ArrayList 的中间或开头插⼊元素,** 当我们向 ArrayList 的中间或开头插⼊元素时,需要将
插⼊位置之后的所有元素都向后 移动⼀位,以腾出空间给新元素。因此,该操作的时间复杂度
是 O(n) 。插⼊的时候,如果底层数组⼤⼩不够,就会发⽣扩容:构造ArrayList 的时候,默认的底层数组
⼤⼩是10 ,不够的话就动态扩容,扩容的数组是原来的 1.5 倍,ArrayList 的扩容操作涉及到数组
的复制和内存的重新分配,所以在频繁添加⼤量元素时,扩容操作可能会影响性能,为了 减少
扩容带来的性能损耗,可以在初始化ArrayList 时预分配⾜够⼤的容量,避免频繁触发扩容操
作。
ArrayList 是线程安全的吗?
不是线程安全的,ArrayList 变成线程安全的⽅式有:
使⽤Collections 类的synchronizedList ⽅法将ArrayList 包装成线程安全的List :
List<String> synchronizedList = Collections.synchronizedList(arrayList);使⽤CopyOnWriteArrayList 类代替ArrayList ,它是⼀个线程安全的List 实现:
CopyOnWriteArrayList<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>(arrayList)使⽤Vector 类代替ArrayList ,Vector 是线程安全的List 实现:
Vector<String> vector = new Vector<>(arrayList);ArrayList 哪⼀步会导致线程不安全?
ArrayList 源码分析
⾸先看看 这个类所拥 有的部分属性字段:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{/**
* 列表元素集合数组如果新建ArrayList对象时没有指定大小,那么会将
\* EMPTY_ELEMENTDATA赋值给elementData,
\* 并在第一次添加元素时,将列表容量设置为DEFAULT_CAPACITY*/
transient Object[] elementData;
// 列表大小,elementData中存储的元素个数通过这 两个 字段可以看出,ArrayList 的实现主要就是:
⽤了⼀个 Object 的数组,⽤来保存所有的元素;
⼀个 size 变量⽤来保存当前数组中已经添加了多少元素。
接着看 下最重要的 add 操作时的源代码:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
nsureCapacityInternal() 的作⽤就是如果将当前的新元素加到列 表后⾯,判断列表的 elementData数组的⼤⼩是否满⾜。
如果 size + 1 的这个需求⻓度⼤于 elementData 这个数组的⻓度,那么就要对这个数组进⾏扩
容。由此看到 add 元素时,实际有两个 ⼤的步骤:
判断 elementData 数组 capacity 容量是否满⾜需求,是否需要扩容。
在 elementData 对应位置上设置值。
这样就出现了第⼀个导致线程不安全的隐患,在多个线程进⾏ add 操作时可能会导致
elementData 数组越界。
ArrayList 线程不安全的原因
ArrayList 默认数组⼤⼩为 10 。假设现在已经添加进去 9 个元素了,size = 9 。
1. 线程 A 执⾏完 add ⽅法中的 ensureCapacityInternal(size+1) 挂起了。- 线程 B 开始执⾏,校验数组容量发现不需要扩容。于是把 “b” 放在了下 标为 9 的位置,且 size
⾃增 1。此时 size = 10 。
3. 线程 A 接着执⾏,尝试把 “a” 放在下标为 10 的位置,因为 size = 10 。但因为数组还没有扩容,最⼤的下标才为 9,所以会 抛出数组越界异常ArrayIndexOutOfBoundsException 。
另外第⼆步 elementData[size++] = e 设置值的操作同样会导致线程不安全。从这⾥可以看出,这步操作也不 是⼀个原⼦操作,它由如下两 步操作构成:
1. elementData[size] = e;
2. size = size + 1;在单线程执⾏这两条代码时没有任何 问题,但是当多线程环境下执⾏时,可能就会发⽣⼀个线程
的值覆盖另⼀个线程添加的值,具体逻辑如下:
1. 列表⼤⼩为 0,即size=0- 线程 A 开始添加⼀个元素,值为 A。此时它执⾏第⼀条操作,将 A 放在了 elementData 下标为
0 的位置上。
- 接着线程 B 刚好也要开始添加⼀个值为 B 的元素,且⾛到了第⼀步操作。此时线程 B 获取到
size 的值依然为 0,于是它将 B 也放在了 elementData 下标为 0 的位置上。
线程 A 开始将 size 的值增加为 1。
线程 B 开始将 size 的值增加为 2。
这样线程 AB 执⾏完毕后,理想中情况为 size 为 2,elementData 下标 0 的位置为 A,下标 1 的位
置为 B。⽽实际情况变成了 size 为 2,elementData 下标为 0 的位置变成了 B,下标 1 的位置上什 么都没有。并且后续除⾮使⽤ set ⽅法修改此位置的值,否则将⼀直为 ,因为 size 为 2,添加元
素时会从下 标为 2 的位置上开始。
案例复现
⽤如下的代码可以进⾏安全性的校验:
public static void main (String [] args ) {
final List <Integer > list = new ArrayList <Integer >();
try {
// 线程 A将0-1000 添加到 list
new Thread (new Runnable () {
@Override
public void run () {
for (int i = 0; i < 1000 ; i++ ) {
list .add (i);
try {
Thread .sleep (1);
} catch (InterruptedException e) {
e.printStackTrace ();
}
}
}
}).start ();
// 线程 B将1000-2000 添加到列表
new Thread (new Runnable () {
list.add(i);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印所有结果
for (int i = 0; i < list.size(); i++) {
System.out.println("第" + (i + 1) + "个元素为:" + list.get(i));
}
}最后的输出结果中,有如下的部分:
第7个元素为:3
第8个元素为:1003
第9个元素为:4
第10个元素为:1004
第11个元素为:
第12个元素为:1005
第13个元素为:6
可以看到第 11 个元素的值为 ,这也就是上⾯所说的情况。多测试⼏次的话,数组越界的异常也可
以复现出来。
项⽬
项⽬介绍,数据模型设计 等
说⼀下项⽬中遇到的最⼤问题(⽤英语表述)
