大家好,我是厂长。
刷到一条脉脉上的陈年老贴,差点没绷住,一位华为 OD 的主管看到一位求职者的学历不错,就主动发起了邀请,然后两方就互相破防了。
我简单交代一下背景。求职者本科是北邮,硕士是清华,清华就不用提了,Top1,北邮也很厉害啊,211 院校,计算机强校。两方的对话可以说是嘲讽拉满:
“清华满足当 OD 的要求吗”,意思是你 OD 也有脸要我清华的简历?
“你本科也就 211 的而已呀”,意思是你 211 本科嚣张啥,都不满足我们的要求。
真应了那句话,强扭的瓜不甜,在双方看来是门不当户不对啊,作为旁观者的我看着都难受(🤔)。
伟大的罗素先生曾说过,“参差不齐乃幸福本源”,这个世界要允许厉害的人和平凡的人一起共事才行。厉害的就去大厂、研究所、高科技;普通的就去民营、小厂、外包打打工。总之,就是要自洽。
前几天我分享了一篇华为 OD 的面经,有小伙伴留言说这样的面经能不能多来点?那能看得上华为 OD 的小伙伴可以去看一下冲 OD 的药方哈。
如果想要更大的舞台,那不妨看看今天这份华为的面经,应该是不少小伙伴的梦中情司,华孝子、爱华信华等华的小伙伴可以提前准备起来喽。
华为面经
牛顿曾说过,“如果我比别人看得更远,那是因为我站在巨人的肩膀上”。因此,大家如果想在求职的过程中有一个好的结果,就一定要多看看前辈们的面经。
这次我们以《Java 面试指南》中同学 4 的华为一面为例,来看看如果你在面试中遇到这些面试题的话,该如何回答?
先来看技术一面的题目大纲(围绕 Java 后端四大件展开),相比较大厂的造火箭🚀,华为确实友好很多:
说一下进程的通信机制
说下工厂模式,场景
说下单例模式,有几种
说下java容器,hashmap,底层实现,效率
说下redis 键值对和hashmap的区别
TCP的可靠传输的实现,UDP可靠传输的实现,二者的差异
redis的事务,说一下
内容较长,撰写硬核面经不容易,建议大家先收藏起来,面试的时候大概率会碰到,二哥会尽量用通俗易懂+手绘图的方式,让你能背会的同时,还能理解和掌握,总之:让天下没有难背的八股 😂
01、进程间的通信机制
推荐阅读:编程十万问:进程间通信的方式有哪些?
进程间通信(IPC,Inter-Process Communication)的方式有管道、信号、消息队列、共享内存、信号量和套接字。
管道:
管道可以理解成不同进程之间的传话筒,一方发声,一方接收,声音的介质可以是空气或者电缆。
进程间的管道就是内核中的一串缓存,从管道的一端写入数据,另一端读取。数据只能单向流动,遵循先进先出(FIFO)的原则。
①、匿名管道:允许具有亲缘关系的进程(如父子进程)进行通信。
②、命名管道:允许无亲缘关系的进程通信,通过在文件系统中创建一个特殊类型的文件来实现。
缺点:管道的效率低,不适合进程间频繁地交换数据。
信号 :
信号可以理解成以前的 BB 机,用于通知接收进程某件事情发生了,是一种较为简单的通信方式,主要用于处理异步事件。
比如kill -9 1050
就表示给 PID 为 1050 的进程发送SIGKIL
信号。
这里顺带普及一下 Linux 中常用的信号:
SIGHUP:当我们退出终端(Terminal)时,由该终端启动的所有进程都会接收到这个信号,默认动作为终止进程。
SIGINT:程序终止(interrupt)信号。按
Ctrl+C
时发出,大家应该在操作终端时有过这种操作。SIGQUIT:和 SIGINT 类似,按
Ctrl+\
键将发出该信号。它会产生核心转储文件,将内存映像和程序运行时的状态记录下来。SIGKILL:强制杀死进程,本信号不能被阻塞和忽略。
SIGTERM:与 SIGKILL 不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出。
消息队列:
消息队列是保存在内核中的消息链表,按照消息的类型进行消息传递,具有较高的可靠性和稳定性。
缺点:消息体有一个最大长度的限制,不适合比较大的数据传输;存在用户态与内核态之间的数据拷贝开销。
共享内存:
允许两个或多个进程共享一个给定的内存区,一个进程写⼊的东西,其他进程⻢上就能看到。
共享内存是最快的进程间通信方式,它是针对其他进程间通信方式运行效率低而专门设计的。
缺点:当多进程竞争同一个共享资源时,会造成数据错乱的问题。
信号量:
信号量可以理解成红绿灯,红灯停(信号量为零),绿灯行(信号量非零)。它本质上是一个计数器,用来控制对共享资源的访问数量。
它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。Java 中的 java.util.concurrent.Semaphore 类就实现了类似的功能。
控制信号量的⽅式有两种原⼦操作:
⼀个是 P 操作(wait,减操作),当进程希望获取资源时,它会执行P操作。如果信号量的值大于0,表示有资源可用,信号量的值减1,进程继续执行。如果信号量的值为0,表示没有可用资源,进程进入等待状态,直到信号量的值变为大于0。
另⼀个是 V 操作(signal,加操作),当进程释放资源时,它会执行V操作,信号量的值加1。如果有其他进程因为等待该资源而被阻塞,这时会唤醒其中一个进程。
套接字Socket:
这个和 Java 中的 Socket 很相似,提供网络通信的端点,可以让不同机器上运行的进程之间进行双向通信。
02、说下工厂模式和场景
推荐阅读:refactoringguru.cn:工厂模式
工厂模式(Factory Pattern)属于创建型设计模式,主要用于创建对象,而不暴露创建对象的逻辑给客户端。
其在父类中提供一个创建对象的方法, 允许子类决定实例化对象的类型。
举例来说,卡车 Truck 和轮船 Ship 都必须实现运输工具 Transport 接口,该接口声明了一个名为 deliver 的方法。
卡车都实现了 deliver 方法,但是卡车的 deliver 是在陆地上运输,而轮船的 deliver 是在海上运输。
调用工厂方法的代码(客户端代码)无需了解不同子类之间的差别,只管调用接口的 deliver 方法即可。
工厂模式的主要类型
①、简单工厂模式(Simple Factory):它引入了创建者的概念,将实例化的代码从应用程序的业务逻辑中分离出来。简单工厂模式包括一个工厂类,它提供一个方法用于创建对象。
class SimpleFactory {
public static Transport createTransport(String type) {
if ("truck".equalsIgnoreCase(type)) {
return new Truck();
} else if ("ship".equalsIgnoreCase(type)) {
return new Ship();
}
return null;
}
public static void main(String[] args) {
Transport truck = SimpleFactory.createTransport("truck");
truck.deliver();
Transport ship = SimpleFactory.createTransport("ship");
ship.deliver();
}
}
②、工厂方法模式(Factory Method):定义一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类的实例化推迟到子类进行。
interface Transport {
void deliver();
}
class Truck implements Transport {
@Override
public void deliver() {
System.out.println("在陆地上运输");
}
}
class Ship implements Transport {
@Override
public void deliver() {
System.out.println("在海上运输");
}
}
interface TransportFactory {
Transport createTransport();
}
class TruckFactory implements TransportFactory {
@Override
public Transport createTransport() {
return new Truck();
}
}
class ShipFactory implements TransportFactory {
@Override
public Transport createTransport() {
return new Ship();
}
}
public class FactoryMethodPatternDemo {
public static void main(String[] args) {
TransportFactory truckFactory = new TruckFactory();
Transport truck = truckFactory.createTransport();
truck.deliver();
TransportFactory shipFactory = new ShipFactory();
Transport ship = shipFactory.createTransport();
ship.deliver();
}
}
应用场景
数据库访问层(DAL)组件:工厂方法模式适用于数据库访问层,其中需要根据不同的数据库(如MySQL、PostgreSQL、Oracle)创建不同的数据库连接。工厂方法可以隐藏这些实例化逻辑,只提供一个统一的接口来获取数据库连接。
日志记录:当应用程序需要实现多种日志记录方式(如向文件记录、数据库记录或远程服务记录)时,可以使用工厂模式来设计一个灵活的日志系统,根据配置或环境动态决定具体使用哪种日志记录方式。
03、说下单例模式
推荐阅读:refactoringguru.cn:单例模式
单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。单例模式主要用于控制对某些共享资源的访问,例如配置管理器、连接池、线程池、日志对象等。
实现单例模式的关键点:
私有构造方法:确保外部代码不能通过构造器创建类的实例。
私有静态实例变量:持有类的唯一实例。
公有静态方法:提供全局访问点以获取实例,如果实例不存在,则在内部创建。
常见的单例模式实现:
01、饿汉式
饿汉式单例(Eager Initialization)在类加载时就急切地创建实例,不管你后续用不用得到,这也是饿汉式的来源,简单但不支持延迟加载实例。
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
02、懒汉式
懒汉式单例(Lazy Initialization)在实际使用时才创建实例,“确实懒”(😂)。这种实现方式需要考虑线程安全问题,因此一般会带上 synchronized 关键字。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
03、双重检查锁定
双重检查锁定(Double-Checked Locking)结合了懒汉式的延迟加载和线程安全,同时又减少了同步的开销,主要是用 synchronized 同步代码块来替代同步方法。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
当 instance 创建后,再次调用 getInstance 方法时,不会进入同步代码块,从而提高了性能。
在 instance 前加上 volatile 关键字,可以防止指令重排,因为 instance = new Singleton()
并不是一个原子操作,可能会被重排序,导致其他线程获取到未初始化完成的实例。
04、静态内部类
利用 Java 的静态内部类(Static Nested Class)和类加载机制来实现线程安全的延迟初始化。
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
当第一次加载 Singleton 类时并不会初始化 SingletonHolder,只有在第一次调用 getInstance 方法时才会导致 SingletonHolder 被加载,从而实例化 instance。
05、枚举
使用枚举(Enum)实现单例是最简单的方式,也能防止反射攻击和序列化问题。
public enum Singleton {
INSTANCE;
// 可以添加实例方法
}
04、说下 Java 容器和 HashMap
Java 容器可以分为两条大的支线:
①、Collection,主要由 List、Set、Queue 组成:
List 代表有序、可重复的集合,典型代表就是封装了动态数组的 ArrayList 和封装了链表的 LinkedList;
Set 代表无序、不可重复的集合,典型代表就是 HashSet 和 TreeSet;
Queue 代表队列,典型代表就是双端队列 ArrayDeque,以及优先级队列 PriorityQueue。
②、Map,代表键值对的集合,典型代表就是 HashMap。
推荐阅读:二哥的 Java 进阶之路:详解 HashMap
JDK 1.7 中,HashMap 的数据结构是
数组
+链表
,不过应该已经没人在用了,所以我们主要说一下 JDK 8 中 HashMap 的数据结构。
JDK 8 中 HashMap 的数据结构是数组
+链表
+红黑树
。
数据结构示意图如下:
也就是说,HashMap 的底层数据结构最主要的还是数组,当发生哈希冲突的时候就用链表来解决;不过,如果链表过长时,查询效率会比较低,于是当链表的长度超过 8 时,链表就会转换为红黑树。
HashMap 之所以叫“哈希表”,是因为它利用了哈希函数来计算键的哈希值,然后根据哈希值来决定元素在数组中的位置,这样就可以实现高效的增删改查效率。
数组的查询效率是 O(1),如果哈希冲突,就会用链表来解决,链表的查询效率是 O(n),于是当链表的长度超过 8 时,链表就会转换为红黑树,红黑树的查询效率是 O(logn)。
当向 HashMap 中添加一个键值对时,会使用哈希函数计算键的哈希码,确定其在数组中的位置。如果该位置已有元素(发生哈希冲突),则新元素将被添加到链表的末尾或红黑树中。如果键已经存在,其对应的值将被新值覆盖。
当从 HashMap 中获取元素时,也会使用哈希函数计算键的位置,然后根据位置在数组、链表或者红黑树中查找元素。
随着元素的不断添加,HashMap 的容量(也就是数组大小)可能不足,于是就需要进行扩容,阈值是capacity * loadFactor
,capacity 为容量,loadFactor 为负载因子,默认为 0.75。扩容后的数组大小是原来的 2 倍,然后把原来的元素重新计算哈希值,放到新的数组中。
总的来说,HashMap 是一种通过哈希表实现的键值对集合,它通过将键哈希化成数组索引,并在冲突时使用链表或红黑树来存储元素,从而实现快速的查找、插入和删除操作。
05、说下 Redis 和 HashMap 的区别
Redis 是 Remote Dictionary Service 三个单词中加粗字母的组合,是一种基于键值对(key-value)的 NoSQL 数据库。
但比一般的键值对,比如 HashMap 强大的多,Redis 中的 value 支持 string(字符串)、hash(哈希)、 list(列表)、set(集合)、zset(有序集合)、Bitmaps(位图)、 HyperLogLog(基数估算)、GEO(地理信息定位)等多种数据结构。
而且因为 Redis 的所有数据都存放在内存当中,所以它的读写性能非常出色。
不仅如此,Redis 还可以将内存数据持久化到硬盘上,这样在发生类似断电或者机器故障的时候,内存中的数据并不会“丢失”。
除此之外,Redis 还提供了键过期、发布订阅、事务、流水线、Lua 脚本等附加功能,是互联网技术领域中使用最广泛的缓存中间件。
06、TCP 和 UDP 的差异
TCP 和 UDP 最根本的区别:TCP 是面向连接的,而 UDP 是无连接的。
可以这么形容:TCP 是打电话,UDP 是大喇叭(😂)。
在数据传输开始之前,TCP 需要先建立连接,数据传输完成后,再断开连接。这个过程通常被称为“三次握手”。
UDP 是无连接的,发送数据之前不需要建立连接,发送完毕也无需断开连接,数据以数据报形式发送。
在此基础上,我们可以得出:TCP 是可靠的,它通过确认机制、重发机制等来保证数据的可靠传输。而 UDP 是不可靠的,数据包可能会丢失、重复、乱序。
说说 TCP 和 UDP 的应用场景?
TCP: 适用于那些对数据准确性要求高于数据传输速度的场合。例如:网页浏览、电子邮件、文件传输(FTP)、远程控制、数据库链接。
UDP: 适用于对速度要求高、可以容忍一定数据丢失的场合。例如:QQ 聊天、在线视频、网络语音电话、广播通信。容忍一定的数据丢失。
07、说下 Redis 事务
Redis 支持简单的事务,可以将多个命令打包,然后一次性的,按照顺序执行。
主要通过 multi、exec、discard、watch 等命令来实现:
multi:标记一个事务块的开始
exec:执行所有事务块内的命令
discard:取消事务,放弃执行事务块内的所有命令
watch:监视一个或多个 key,如果在事务执行之前这个 key 被其他命令所改动,那么事务将被打断
这里简单说一下 Redis 事务的原理:
使用 MULTI 命令开始一个事务。从这个命令执行之后开始,所有的后续命令都不会立即执行,而是被放入一个队列中。在这个阶段,Redis 只是记录下了这些命令。
使用 EXEC 命令触发事务的执行。一旦执行了 EXEC,之前 MULTI 后队列中的所有命令会被原子地(atomic)执行。这里的“原子”意味着这些命令要么全部执行,要么(在出现错误时)全部不执行。
如果在执行 EXEC 之前决定不执行事务,可以使用 DISCARD 命令来取消事务。这会清空事务队列并退出事务状态。
WATCH 命令用于实现乐观锁。WATCH 命令可以监视一个或多个键,如果在执行事务的过程中(即在执行 MULTI 之后,执行 EXEC 之前),被监视的键被其他命令改变了,那么当执行 EXEC 时,事务将被取消,并且返回一个错误。
Redis 事务的注意点有哪些?
Redis 事务是不支持回滚的,不像 MySQL 的事务一样,要么都执行要么都不执行;一旦 EXEC 命令被调用,所有命令都会被执行,即使有些命令可能执行失败。失败的命令不会影响到其他命令的执行。
Redis 事务为什么不支持回滚?
引入事务回滚机制会大大增加 Redis 的复杂性,因为需要跟踪事务中每个命令的状态,并在发生错误时逆向执行命令以恢复原始状态。
Redis 是一个基于内存的数据存储系统,其设计重点是实现高性能。事务回滚需要额外的资源和时间来管理和执行,这与 Redis 的设计目标相违背。因此,Redis 选择不支持事务回滚。
换句话说,就是我 Redis 不想支持事务,也没有这个必要。
— 完 —
最近厂长整理了一份程序员学习大礼包,包含数据结构与算法的最核心知识点,有兴趣的小伙伴可以扫码添加微信,备注“礼包”。