# 联想 Java 面试

大家好,我是小林。

大家面试联想的时候,可能需要准备好英语表达,面试过程中可能会要求你用英语来回答问题,联想因为有很多国外的同事,工作中也经常跟国外的同事协助,所以英语是会在工作中日常交流的。

甚至,可能有一面的面试官,就是外国人面试官,这种情况,就需要全程英语交流。 联想的工作氛围还是不错,不加班,听说朝九晚六,十几天年假,妥妥的外企风格,羡慕了。

这次,来分享一位同学的联想 Java 后端校招的面经,主要是问八股比较多,但是整体上不算难,都是比较基础的问题,主要就问了计算机网络、操作系统、Java 集合一些问题。

技术面之后,就进行英语对话环节了,需要用英语说一下项目中遇到的最大问题,这最好提前准备一下,当场用英语去表达,肯定讲的不好。

img

# 计算机网络

# IP协议是哪一层的?

在网络层。

TCP/IP 网络通常是由上到下分成 4 层,分别是应用层,传输层,网络层和网络接口层

  • 应用层 支持 HTTP、SMTP 等最终用户进程
  • 传输层 处理主机到主机的通信(TCP、UDP)
  • 网络层 寻址和路由数据包(IP 协议)
  • 链路层 通过网络的物理电线、电缆或无线信道移动比特

# 域名转化为IP地址用到了什么协议?

dns 协议

# 说说DNS的解析过程。

  1. 客户端首先会发出一个 DNS 请求,问 www.server.com 的 IP 是啥,并发给本地 DNS 服务器(也就是客户端的 TCP/IP 设置中填写的 DNS 服务器地址)。
  2. 本地域名服务器收到客户端的请求后,如果缓存里的表格能找到 www.server.com,则它直接返回 IP 地址。如果没有,本地 DNS 会去问它的根域名服务器:“老大, 能告诉我 www.server.com 的 IP 地址吗?” 根域名服务器是最高层次的,它不直接用于域名解析,但能指明一条道路。
  3. 根 DNS 收到来自本地 DNS 的请求后,发现后置是 .com,说:“www.server.com 这个域名归 .com 区域管理”,我给你 .com 顶级域名服务器地址给你,你去问问它吧。”
  4. 本地 DNS 收到顶级域名服务器的地址后,发起请求问“老二, 你能告诉我 www.server.com 的 IP 地址吗?”
  5. 顶级域名服务器说:“我给你负责 www.server.com 区域的权威 DNS 服务器的地址,你去问它应该能问到”。
  6. 本地 DNS 于是转向问权威 DNS 服务器:“老三,www.server.com对应的IP是啥呀?”server.com 的权威 DNS 服务器,它是域名解析结果的原出处。为啥叫权威呢?就是我的域名我做主。
  7. 权威 DNS 服务器查询后将对应的 IP 地址 X.X.X.X 告诉本地 DNS。
  8. 本地 DNS 再将 IP 地址返回客户端,客户端和目标建立连接。

至此,我们完成了 DNS 的解析过程。现在总结一下,整个过程我画成了一个图。

#

# 改过host文件吗?

hosts 文件是一个系统文件,用于将主机名映射到 IP 地址。在某些情况下,它可以用于进行域名解析,类似于 DNS 的功能。通常在操作系统中,hosts 文件的位置如下:

  • Windows: C:\Windows\System32\drivers\etc\hosts
  • Linux/Unix/Mac: /etc/hosts hosts 文件通常包含多行,每行包含一个 IP 地址和一个或多个主机名,格式如下:
P地址 主机名 [别名]
127.0.0.1   localhost
192.168.1.10  my-server

使用场景:

  1. 本地开发:可以将特定域名指向本地服务器的 IP 地址,以便于开发和测试。
  2. 阻止网站:可以通过将网站域名指向 127.0.0.1 来阻止访问,例如将 example.com 指向 127.0.0.1 可以阻止该网站的访问。
  3. 加速解析:对于常用的自家服务器,可以直接通过 hosts 文件来加速网络请求的解析。

# 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中存储的元素个数
 private int size;
}

通过这两个字段可以看出,ArrayList 的实现主要就是:

  1. 用了一个 Object 的数组,用来保存所有的元素;
  2. 一个 size 变量用来保存当前数组中已经添加了多少元素。

接着看下最重要的 add 操作时的源代码:

public boolean add(E e) {
     ensureCapacityInternal(size + 1); // Increments modCount!!
     elementData[size++] = e;
     return true;
 }

nsureCapacityInternal() 的作用就是如果将当前的新元素加到列表后面,判断列表的 elementData 数组的大小是否满足。

如果 size + 1 的这个需求长度大于 elementData 这个数组的长度,那么就要对这个数组进行扩容。由此看到 add 元素时,实际有两个大的步骤:

  1. 判断 elementData 数组 capacity 容量是否满足需求,是否需要扩容。
  2. 在 elementData 对应位置上设置值。

这样就出现了第一个导致线程不安全的隐患,在多个线程进行 add 操作时可能会导致 elementData 数组越界。

ArrayList 线程不安全的原因

ArrayList 默认数组大小为 10。假设现在已经添加进去 9 个元素了,size = 9。

  1. 线程 A 执行完 add 方法中的 ensureCapacityInternal(size+1) 挂起了。
  2. 线程 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
  2. 线程 A 开始添加一个元素,值为 A。此时它执行第一条操作,将 A 放在了 elementData 下标为 0 的位置上。
  3. 接着线程 B 刚好也要开始添加一个值为 B 的元素,且走到了第一步操作。此时线程 B 获取到 size 的值依然为 0,于是它将 B 也放在了 elementData 下标为 0 的位置上。
  4. 线程 A 开始将 size 的值增加为 1。
  5. 线程 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() {
            public void run() {
                for (int i = 1000; i < 2000; i++) {
                    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个元素为:38个元素为:10039个元素为:410个元素为:100411个元素为:
第12个元素为:100513个元素为:6

可以看到第 11 个元素的值为 ,这也就是上面所说的情况。多测试几次的话,数组越界的异常也可以复现出来。

# 项目

  • 项目介绍,数据模型设计等
  • 说一下项目中遇到的最大问题(用英语表述)

对了,最新的互联网大厂后端面经都会在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。

img