字节跳动一面(已挂)
最近的面经给大家做个复盘总结,方便后续参加面试的同学查漏补缺。
从题目能看得出来,妥妥的八股盛宴,并且主要集中我一直给大家强调的 Java 后端四大件上(Java、Spring、MySQL 和 Redis)+计算机网络,那希望接下来的复盘大家都能认认真真过一遍,知彼知己,百战不殆。
下次碰到原题一定要过哦。
同学 34 字节跳动一面
项目(自己的实习项目)中遇到的最大挑战,以及难点,是怎么解决的
(以技术派举例)遇到最深刻的一个问题是,如何解决高并发情况下,大量用户同时访问同一篇热点文章,在缓存未命中的情况下,大量请求会同时访问数据库,对 DB 造成极大的请求压力,很容易将我们的 MySQL 打宕机,进而影响整个服务,这个时候该怎么办?
一开始尝试通过 Redis 的 setIfAbsent(key,value,time)
手动释放锁,但遇到了锁不能及时释放的问题、误释放别人的锁,以及过期时间的设置是否合理等问题。
最后还是通过引入 Redission 的看门狗算法进行解决,这样就可以一劳永逸了,不过手动尝试的方式的确也让我对看门狗算法有了一个更深入更直接的了解,它的内部实现也是按照我之前手动的逻辑实现的,起一个定时任务,每 10 秒检查一下锁是否释放,如果没有释放就延长至 30 秒。
Java有哪些并发容器
Java 提供了多种并发容器,主要包括:
- ConcurrentHashMap:线程安全的哈希表,支持高并发读写操作。
- CopyOnWriteArrayList:写时复制的 ArrayList,适用于读多写少的场景。
- BlockingQueue:阻塞队列,常用实现有 ArrayBlockingQueue 和 LinkedBlockingQueue,适用于生产者-消费者场景。
- ConcurrentLinkedQueue:非阻塞的线程安全队列。
- ConcurrentSkipListMap:基于跳表实现的线程安全有序 Map。
jvm的底层结构了解吗
JVM 大致可以划分为三个部分:类加载器、运行时数据区和执行引擎。
① 类加载器,负责从文件系统、网络或其他来源加载 Class 文件,将 Class 文件中的二进制数据读入到内存当中。
② 运行时数据区,JVM 在执行 Java 程序时,需要在内存中分配空间来处理各种数据,这些内存区域按照 Java 虚拟机规范可以划分为方法区、堆、虚拟机栈、程序计数器和本地方法栈。
③ 执行引擎,也是 JVM 的心脏,负责执行字节码。它包括一个虚拟处理器、即时编译器 JIT 和垃圾回收器。
说说Java的并发关键字
最常用的有两个关键字,分别是:
- synchronized:用于方法或者代码块,确保同一时间只有一个线程可以执行被 synchronized 修饰的代码,适用于保护共享资源的访问。
- volatile:用于变量,确保变量的可见性,防止指令重排序,适用于状态标志等场景。
volatile在jvm里的底层实现,volatile的可见性是怎么实现的
当线程对 volatile 变量进行写操作时,JVM 会在这个变量写入之后插入一个写屏障指令,这个指令会强制将本地内存中的变量值刷新到主内存中。
StoreStore; // 保证写入之前的操作不会重排 volatile_write(); // 写入 volatile 变量 StoreLoad; // 保证写入后,其他线程立即可见
当线程对 volatile 变量进行读操作时,JVM 会插入一个读屏障指令,这个指令会强制让本地内存中的变量值失效,从而重新从主内存中读取最新的值。
当声明一个 volatile 变量 x:
volatile int x = 0
线程 A 对 x 写入后会将其最新的值刷新到主内存中,线程 B 读取 x 时由于本地内存中的 x 失效了,就会从主内存中读取最新的值。
单线程下不加volatile和加volatile性能开销有很大区别吗
单线程下,volatile 的性能开销相对较小,因为没有线程竞争和上下文切换的开销。
但 volatile 仍然会引入一些额外的内存屏障指令,以确保内存可见性。我之前做过一个测试,普通变量1亿次操作只用了2-3毫秒,volatile 变量 1亿次操作用了25-29毫秒,大概是普通变量的 10 倍左右。
虽然相对差异很大,但绝对时间差异很小,都是毫秒级。
讲讲LinkedList的使用场景
LinkedList 适用于:
- 在列表中间频繁插入和删除元素的场景。
- 顺序访问多于随机访问的场景。
- LinkedList 可以实现队列(FIFO)和栈(LIFO)。
说说双端队列ArrayDeque的,或者说队列和栈的使用场景
双端队列 ArrayDeque 是一个基于数组的,可以在两端插入和删除元素的队列。
队列和栈的区别了解吗?
队列是一种先进先出(FIFO, First-In-First-Out)的数据结构,第一个加入队列的元素会成为第一个被移除的元素,适用于需要按顺序处理任务的场景,比如消息队列、任务调度等。
栈是一种后进先出(LIFO, Last-In-First-Out)的数据结构,最后一个加入栈的元素会成为第一个被移除的元素,适用于需要回溯的场景,比如函数调用栈、浏览器历史记录等。
工厂模式了解吗
工厂模式(Factory Pattern)属于创建型设计模式,主要用于创建对象,而不暴露创建对象的逻辑给客户端。
其在父类中提供一个创建对象的方法, 允许子类决定实例化对象的类型。
举例来说,卡车 Truck 和轮船 Ship 都必须实现运输工具 Transport 接口,该接口声明了一个名为 deliver 的方法。
卡车都实现了 deliver 方法,但是卡车的 deliver 是在陆地上运输,而轮船的 deliver 是在海上运输。
调用工厂方法的代码(客户端代码)无需了解不同子类之间的差别,只管调用接口的 deliver 方法即可。
线程不是有线程名吗,怎么做到让线程的前缀名统一,通过factory来实现
可以通过工厂模式创建线程,并在工厂方法中设置线程的前缀名。这样可以确保所有通过工厂创建的线程都具有统一的命名规范。
class NamedThreadFactory implements ThreadFactory { private final String prefix; private int count = 0; public NamedThreadFactory(String prefix) { this.prefix = prefix; } @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setName(prefix + "-" + count++); return thread; } } public class ThreadFactoryDemo { public static void main(String[] args) { ThreadFactory factory = new NamedThreadFactory("MyThread"); Runnable task = () -> { System.out.println("线程名称: " + Thread.currentThread().getName()); }; for (int i = 0; i < 5; i++) { Thread thread = factory.newThread(task); thread.start(); } } }
线程池里面的任务队列是什么队列
阻塞队列。
BlockingQueue主要有哪几个实现类
常用的有五种,有界队列 ArrayBlockingQueue;无界队列 LinkedBlockingQueue;优先级队列 PriorityBlockingQueue;延迟队列 DelayQueue;同步队列 SynchronousQueue。
①、ArrayBlockingQueue:一个有界的先进先出的阻塞队列,底层是一个数组,适合固定大小的线程池。
ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10, true);
②、LinkedBlockingQueue:底层是链表,如果不指定大小,默认大小是 Integer.MAX_VALUE,几乎相当于一个无界队列。
技术派实战项目中,就使用了 LinkedBlockingQueue 来配置 RabbitMQ 的消息队列。
③、PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。任务按照其自然顺序或 Comparator 来排序。
适用于需要按照给定优先级处理任务的场景,比如优先处理紧急任务。
④、DelayQueue:类似于 PriorityBlockingQueue,由二叉堆实现的无界优先级阻塞队列。
Executors 中的 newScheduledThreadPool()
就使用了 DelayQueue 来实现延迟执行。
public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); }
⑤、SynchronousQueue:每个插入操作必须等待另一个线程的移除操作,同样,任何一个移除操作都必须等待另一个线程的插入操作。
Executors.newCachedThreadPool()
就使用了 SynchronousQueue,这个线程池会根据需要创建新线程,如果有空闲线程则会重复使用,线程空闲 60 秒后会被回收。
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
spring的主要特性是什么
Spring 的特性还是挺多的,我按照在实际工作/学习中用得最多的几个来说吧。
首先最核心的就是 IoC 控制反转和 DI 依赖注入,让 Spring 有能力帮我们管理对象的创建和依赖关系。
比如我写一个 UserService,需要用到 UserDao,以前得自己 new 一个 UserDao 出来,现在只要在 UserService 上加个 @Service
注解,在 UserDao 字段上加个 @Autowired
,Spring 就会自动帮我们处理好这些依赖关系。
这样代码的耦合度就大大降低了,测试的时候也更容易 mock。
第二个就是 AOP 面向切面编程。这个在我们处理一些横切关注点的时候特别有用,比如说我们要给某些 Controller 方法都加上权限控制,如果没有 AOP 的话,每个方法都要写一遍加权代码,维护起来很麻烦。
用了 AOP 之后,我们只需要写一个切面类,定义好切点和通知,就能统一处理了。事务管理也是同样的道理,加个 @Transactional
注解就搞定了。
AOP和装饰器模式有什么区别
AOP 和装饰器模式都是为了在不修改原有代码的情况下,动态地为对象添加额外的行为。
装饰器模式是通过创建一个包装类来实现的,这个包装类持有被装饰对象的引用,并在调用方法时添加额外的逻辑。装饰器模式通常需要手动编写包装类,适用于单个对象的增强。
// 基础组件接口 interface Component { void operation(); } // 具体组件 class ConcreteComponent implements Component { public void operation() { System.out.println("执行基本操作"); } } // 装饰器基类 abstract class Decorator implements Component { protected Component component; public Decorator(Component component) { this.component = component; } public void operation() { component.operation(); } } // 具体装饰器 class ConcreteDecorator extends Decorator { public ConcreteDecorator(Component component) { super(component); } public void operation() { addedBehavior(); super.operation(); addedBehavior(); } private void addedBehavior() { System.out.println("添加的新功能"); } }
tcp握手为什么是3次,挥手为什么是4次
使用三次握手可以建立一个可靠的连接。这一过程的目的是确保双方都知道对方已准备好进行通信,并同步双方的序列号,从而保持数据包的顺序和完整性。
- 第一次握手:客户端发送 SYN 包(连接请求)给服务器,如果这个包延迟了,客户端不会一直等待,它可能会重试并发送一个新的连接请求。
- 第二次握手:服务器收到 SYN 包后,发送一个 SYN-ACK 包(确认接收到连接请求)回客户端。
- 第三次握手:客户端收到 SYN-ACK 包后,再发送一个 ACK 包给服务器,确认收到了服务器的响应。
TCP 挥手为什么需要四次呢?
因为 TCP 是全双工通信协议,数据的发送和接收需要两次一来一回,也就是四次,来确保双方都能正确关闭连接。
- 第一次挥手:客户端表示数据发送完成了,准备关闭,你确认一下。
- 第二次挥手:服务端回话说 ok,我马上处理完数据,稍等。
- 第三次挥手:服务端表示处理完了,可以关闭了。
- 第四次挥手:客户端说好,进入 TIME_WAIT 状态,确保服务端关闭连接后,自己再关闭连接。
http和https有什么区别
HTTPS 是 HTTP 的增强版,在 HTTP 的基础上加入了 SSL/TLS 协议,确保数据在传输过程中是加密的。
HTTP 的默认端⼝号是 80,URL 以http://
开头;HTTPS 的默认端⼝号是 443,URL 以https://
开头。
讲一下https的tls握手过程
HTTPS 的连接建立在 SSL/TLS 握手之上,其过程可以分为两个阶段:握手阶段和数据传输阶段。
①、客户端向服务器发起请求
②、服务器接收到请求后,返回自己的数字证书,包含了公钥、颁发机构等信息。
③、客户端收到服务器的证书后,验证证书的合法性,如果合法,会生成一个随机码,然后用服务器的公钥加密这个随机码,发送给服务器。
④、服务器收到会话密钥后,用私钥解密,得到会话密钥。
⑤、客户端和服务器通过会话密码对通信内容进行加密,然后传输。
客户端怎么检验ca证书的合法性
首先,所有的证书都是由 CA 机构签发的,CA 机构是一个受信任的第三方机构,它会对证书的申请者进行身份验证,然后签发证书。
CA 就像是网络世界的公安局,具有极高的可信度。
CA 签发证书的过程是非常严格的:
- 首先,CA 会把持有者的公钥、⽤途、颁发者、有效时间等信息打成⼀个包,然后对这些信息进⾏ Hash 计算,得到⼀个 Hash 值;
- 然后 CA 会使⽤⾃⼰的私钥将该 Hash 值加密,⽣成 Certificate Signature;
- 最后将 Certificate Signature 添加在⽂件证书上,形成数字证书。
如果服务端发送给客户端的加密算法是客户端没有的,这种情况会怎样
如果客户端不支持服务器建议的任何加密算法,那么安全连接将建立失败。
当客户端向服务器发起一个 HTTPS 请求时,它会发送一个 ClientHello 消息。
这个消息包含了客户端支持的加密算法列表(称为密码套件),以及其他一些参数。
服务器收到 ClientHello 后,会从客户端提供的密码套件中选择一个它也支持的密码套件,并在 ServerHello 消息中返回给客户端。
如果服务器无法找到一个双方都支持的密码套件,那么它会发送一个警告消息,表示无法协商出一个共同的加密算法,随后连接将被终止。#牛客AI配图神器#