虎⽛⼀⾯
虎⽛ Java ⾯试
虎⽛属于互 联⽹中⼚,专注的是直播领域,⽐较出圈的是游戏领域的直播了,不少职业选⼿都在
虎⽛直播,曾经在校的时候,我也经常在虎⽛看 LOL 游戏直播,不过⼯作之后就看的⽐较少,短
视频平台的出现还是对以前的直播平台影响⽐较⼤。
我看了虎⽛ 2024 年第⼀〜第三季度财报,⽤⼾数和利润整体还是增⻓的状态,每个季度的总收⼊
在 15 亿元,直播收⼊在 12 亿左右,占⽐⼤头,基本盘还是没有太⼤的问题。
我们来看看 虎⽛校招薪资如何?
收集了⼏个虎⽛ 25 届开发岗位校招薪资的情况,虎⽛办公地点在⼴州:
后台 开发岗位:22k x 16 + 0.6k x 12 (每个⽉ 600 饭补)= 35w
客⼾端开发岗位:21k x 16 + 0.6k x 12 (每个⽉ 600 饭补)= 33w
运维开发岗位:20k x 16 + 0.6k x 12 (每个⽉ 600 饭补)= 32w这次来分享⼀位同学虎⽛的Java 开发岗位的⼀⾯⾯经,问题虽然不多,但是主要都是拷打并发编
程的,⽽且问题简直就是⼀环扣⼀环,步步 紧逼,还是蛮有压⼒的⾯试。
最开始从并发理论切⼊,刚把基础概念捋清楚,⻢上就跳到并发⼯具上了 。讲⼯具原理的时候,
好不容易把 CAS 搞明⽩点⼉,结果紧接着⼜得深挖 CAS 以及 AQS 这些底层技术,脑⼦还在⾼速
运转 消化呢,下⼀个问题就来了,直接问怎么⽤ AQS 去实现锁,这还 不算完,还得把
ReentranLock 的实现原理⼀股脑⼉地背出来。本以为这就结束了,谁知道⼜被要求⽤ MySQL 来实
现,⽽且各个细节都得描述得清清 楚楚

虎⽛⼀⾯
怎么理解并发问题?
并发是指在同⼀时间段内,多个任务交替执⾏。在单处理器系统中,实际上这些任务是通过快速
切换轮流使⽤处理器资源,看起来像是同时执⾏。⽽在多处 理器系统中,多个任务可以真正地同
时在不同的处理器核⼼上执⾏。
当多个线程同时访问和修改共享资源时,由于线程执⾏的顺序是不确定的,可能会导致最终的结
果依赖于线程执⾏的顺序,这种情况就称为竞争条件。例如,在⼀个简单的计数器程序中,如果
多个线程同时对计数器进⾏⾃增操作,由于线程切换的不确定性,可能会出现某些⾃增操作丢失
的情况,导致最终的计数值与预期不符。
要保证多线程的允许是安全,不要出现数据竞争造成的数据混乱的问题。
Java 的线程安全在三个 ⽅⾯体现:
原⼦性:提供互斥访问,同⼀时刻只能有⼀个线程对数据进⾏操作,在Java 中使⽤了atomic 和
synchronized 关键字来确保原⼦性;
可⻅性:⼀个线程对主内存的修改可以及时地被其他线程看到,在Java 中使⽤了synchronized 和
volatile 这两个 关键字确保可⻅性;
有序性:⼀个线程观察其他线程中的指令执⾏顺序,由于指令重排序,该观察结果⼀般杂乱⽆
序,在Java 中使⽤了happens-before 原则来确保有序性。
什么 情况会产⽣死锁问题?如何解决?
死锁只有同时满⾜以下四个条件才会发⽣:
互斥条件:互斥条件是指多个线程不能同时使⽤同⼀个资源。
持有并等待条件:持有并等待条件是指,当线程 A 已经持有了资源 1,⼜想申请资源 2,⽽资
源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时
并不会释放⾃⼰已经持有的资源 1。
不可剥夺条件:不可剥夺条件是指,当线程已经持有了资源 ,在⾃⼰使⽤完之前不能被其他线
程获取,线程 B 如果也想使⽤此资源,则只能在线程 A 使⽤完并释放后才能获取。
环路等待条件:环路等待条件指的是,在死锁发⽣的时候,两个 线程获取资源的顺序构成了环
形链。
例如,线程 A 持有资源 R1 并试图获取资源 R2 ,⽽线程 B 持有资源 R2 并试图获取资源 R1 ,此时
两个 线程相互等待对⽅释放资源,从⽽导致死锁。
public class DeadlockExample {
private static final Object resource1 = new Object ();
private static final Object resource2 = new Object ();
public static void main (String [] args ) {
Thread threadA = new Thread (() -> {
synchronized (resource1 ) {
System .out .println ("Thread A acquired resource1" );
try {
Thread .sleep (100 );
} catch (InterruptedException e) {
e.printStackTrace ();
}
synchronized (resource2 ) {
System .out .println ("Thread A acquired resource2" );
}
}
Thread threadB = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread B acquired resource2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource1) {
System.out.println("Thread B acquired resource1");
}
}
});
threadA.start();
threadB.start();
}
}避免死锁问题就只需要破环其中⼀个条件就可以,最常⻅的并且可⾏的就是使⽤资源有序分配
法,来破环环 路等待条件。
那什么 是资源有序分配法呢?线程 A 和 线程 B 获取资源的顺序要⼀样,当线程 A 是先尝试获取资
源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也
就是说,线程 A 和 线程 B 总是以相同的顺序申请⾃⼰想要的资源。

讲讲 Java 的⼀些并发⼯具?
Java 中⼀些常⽤的并发⼯具,它们位 于 java.util.concurrent 包中,常⻅的有:
CountDownLatch :CountDownLatch 是⼀个同步辅助类,它允许⼀个或多个线程等待其他线程
完成操作。它使⽤⼀个计数器进⾏初始化,调⽤ countDown() ⽅法会使 计数器减⼀,当计数器的值减为 0 时,等待的线程会被唤醒。可以把它想象成⼀个倒计时器, 当倒计时结束(计数器
为 0)时,等待的事件就会发⽣。⽰例代 码:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int numberOfThreads = 3;
CountDownLatch latch = new CountDownLatch(numberOfThreads);
// 创建并启动三个工作线程
for (int i = 0; i < numberOfThreads; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 正在工作");
try {
Thread.sleep(1000); // 模拟工作时间
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown(); // 完成工作,计数器减一
System.out.println(Thread.currentThread().getName() + " 完成工作");
}).start();
}
System.out.println("主线程等待工作线程完成");
latch.await(); // 主线程等待,直到计数器为 0
System.out.println("所有工作线程已完成,主线程继续执行");
}
}CyclicBarrier :CyclicBarrier 允许⼀组线 程互相等待,直到到 达⼀个公共 的屏障点。当所有线程
都到达这 个屏障点后,它们可以继续执⾏后续操作,并且这个屏障可以被重置循环使⽤。与
CountDownLatch 不同, CyclicBarrier 侧重于线程间的相互等待,⽽不是等待某些操作完
成。⽰例代 码:
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
int numberOfThreads = 3;
CyclicBarrier barrier = new CyclicBarrier(numberOfThreads, () -> {
for (int i = 0; i < numberOfThreads; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 正在运行");
Thread.sleep(1000); // 模拟运行时间
barrier.await(); // 等待其他线程
System.out.println(Thread.currentThread().getName() + " 已经通过屏障")
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}Semaphore :Semaphore 是⼀个计数信号量,⽤于控制同时访问某个共享资源的线程数量。通
过 acquire() ⽅法获取许可,使⽤ release() ⽅法释放许可。如果没有许可可 ⽤,线程将被阻塞,直到有许可被释放。可以⽤来限制对某些资源(如数据库连接池、⽂件操作等)的并发
访问量。代码如下:
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2); // 允许 2 个线程同时访问
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可
System.out.println(Thread.currentThread().getName() + " 获得了许可");
Thread.sleep(2000); // 模拟资源使用
System.out.println(Thread.currentThread().getName() + " 释放了许可");
semaphore.release(); // 释放许可
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}Future 和 Callable :Callable 是⼀个类似于 Runnable 的接⼝,但它可以返回结果,并且可以
抛出异常。Future ⽤于表⽰⼀个异步计算的结果,可以通过它来获取 Callable 任务的执⾏结
果或取消任务。代码如下:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class FutureCallableExample {
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newSingleThreadExecutor();
Callable<Integer> callable = () -> {
System.out.println(Thread.currentThread().getName() + " 开始执行 Callable 任务
Thread.sleep(2000); // 模拟耗时操作
return 42; // 返回结果
};
Future<Integer> future = executorService.submit(callable);
System.out.println("主线程继续执行其他任务");
try {
Integer result = future.get(); // 等待 Callable 任务完成并获取结果
System.out.println("Callable 任务的结果: " + result);
} catch (Exception e) {
e.printStackTrace();
}
executorService.shutdown();
}
}ConcurrentHashMap :ConcurrentHashMap 是⼀个线程安全的哈希表,它允许多个线程同时
进⾏读操作,在⼀定程度上⽀持并发的修改操作,避免了 HashMap 在多线程环境下需要使⽤
synchronized 或 Collections.synchronizedMap() 进⾏同步的性能问题。代码如下:
import java.util.concurrent.ConcurrentHashMap;
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key1", 1);
map.put("key2", 2);
// 并发读操作
map.forEach((key, value) -> System.out.println(key + ": " + value));
// 并发写操作
map.computeIfAbsent("key3", k -> 3);
}
}介绍⼀下 ConcurrentHashMap 并发原 理?
JDK 1.7 ConcurrentHashMap
在 JDK 1.7 中它使⽤的是数组加链表的形式 实现的,⽽数组⼜分为:⼤数组 Segment 和⼩数组
HashEntry 。 Segment 是⼀种可重⼊锁(ReentrantLock ),在 ConcurrentHashMap ⾥扮演锁的⻆⾊;HashEntry 则⽤于存储键值对数据。⼀个 ConcurrentHashMap ⾥包含⼀个 Segment 数组,⼀个 Segment ⾥包含⼀个 HashEntry 数组,每个 HashEntry 是⼀个链表结构的元素。

JDK 1.7 ConcurrentHashMap 分段锁技术将数据分成⼀段⼀段的存储,然后给每⼀段数据配⼀把
锁,当⼀个线程占⽤锁访问其中⼀个段数据的时候,其他段的数据也能被其他线程访问,能够实
现真正的并发访问。
在 JDK 1.7 中,ConcurrentHashMap 虽然是线程安全的,但因为它的底层实现是数组 + 链表的形式,所以在数据⽐较多的情况下访问是很慢的,因为要遍历整个链表,⽽ JDK 1.8 则使⽤了数组 +
链表/红⿊树的⽅式优化了 ConcurrentHashMap 的实现,具体实现结构如下:

JDK 1.8 ConcurrentHashMap JDK 1.8 ConcurrentHashMap 主要通过 volatile + CAS 或者
synchronized 来实现的线程安全的。添加元素时⾸先会判断容器是否为空:
如果为空则使⽤ volatile 加 CAS 来初始化
如果容器不为 空,则根据存储的元素计算该位置是否为空。
如果根据存储的元素计算结果为空,则利 ⽤ CAS 设置该节点;
如果根据存储的元素计算结果不为 空,则使⽤ synchronized ,然后,遍历桶中的数据,并替
换或新增节点到桶中,最后再判断是否需要转为红⿊树,这样就能保证并发访问时的线程安
全了。
如果把上⾯的执⾏⽤⼀句话归纳的话,就相当于是ConcurrentHashMap 通过对头结点加锁来保证
线程安全的,锁的粒度相⽐ Segment 来说更⼩了,发⽣冲突和加锁的频率降低了,并发操作的性
能就提⾼了。
⽽且 JDK 1.8 使⽤的是红⿊树优化了之 前的固定链表,那么当数据量⽐较⼤的时候,查询性能也得
到了很⼤的提升,从之 前的 O(n) 优化到了 O(logn) 的时间复杂度。CAS 和 AQS 有什么 关系?
CAS 和 AQS 两者的区别:
CAS 是⼀种乐观锁机制,它包含三个 操作数:内存位置(V)、预期值(A)和新值(B)。CAS
操作的逻辑是,如果内存位置 V 的值等于预期值 A,则将其更新为新值 B,否则不做任何 操
作。整个过程是原⼦性的,通常由硬件指令⽀持,如在现代处理器上, cmpxchg 指令可以实现
CAS 操作。
AQS 是⼀个⽤于构建锁和同 步器的框架,许多同步器如 ReentrantLock 、 Semaphore 、
CountDownLatch 等都是基于 AQS 构建的。AQS 使⽤⼀个 volatile 的整数 变量 state 来表
⽰同步状态,通过内置的 FIFO 队列来管理等待线程。它提供了⼀些基本的操作,如
acquire (获取资源)和 release (释放资源),这些操作会 修改 state 的值,并根据
state 的值来判断线程是否可 以获取或释放资源。AQS 的 acquire 操作通常会先尝试获取资
源,如果失败,线程将被添加到 等待队列中,并阻塞等待。 release 操作会 释放资源,并唤醒
等待队列中的线程。
CAS 和 AQS 两者的联系:
CAS 为 AQS 提供原⼦操作⽀持:AQS 内部使⽤ CAS 操作来更 新 state 变量,以实现线程安
全的状态修改。在 acquire 操作中,当线程尝试获取资源时,会使 ⽤ CAS 操作尝试将
state 从⼀个值更新为另⼀个值,如果更 新失败,说明资源已被占⽤,线程会进⼊等待队列。
在 release 操作中,当线程释放资源时,也会使 ⽤ CAS 操作将 state 恢复到相应的值,以保证状态更新的原⼦性。
如何⽤ AQS 实现⼀个可重⼊的公平锁?
AQS 实现⼀个可重⼊的公平锁的详细步骤:
- 继承 AbstractQueuedSynchronizer :创建⼀个内部类继承⾃ AbstractQueuedSynchronizer ,重写 tryAcquire 、 tryRelease 、 isHeldExclusively 等⽅法,这些⽅法将⽤于实现锁的获
取、释放和判断锁是否被当前线程持有。
- 实现可重⼊逻辑:在 tryAcquire ⽅法中,检查当前线程是否已经持有锁,如果是,则增加锁
的持有次数(通过 state 变量);如果不是,尝试使⽤ CAS 操作来获取锁。
- 实现公平性:在 tryAcquire ⽅法中,按照队列顺序来获取锁,即先检查等待队列中是否有线
程在等待,如果有 ,当前线程必须进⼊队列等待,⽽不是直接竞争锁。
- 创建锁的外部类:创建⼀个外部类,内部持有 AbstractQueuedSynchronizer 的⼦类对象,并提
供 lock 和 unlock ⽅法,这些⽅法将调⽤ AbstractQueuedSynchronizer ⼦类中的⽅法。
代码如下:
import java .util .concurrent .locks .AbstractQueuedSynchronizer ;
public class FairReentrantLock {
private static class Sync extends AbstractQueuedSynchronizer {
// 判断锁是否被当前线程持有
protected boolean isHeldExclusively () {
return getExclusiveOwnerThread () == Thread .currentThread ();
}
// 尝试获取锁
protected boolean tryAcquire (int acquires ) {
final Thread current = Thread .currentThread ();
int c = getState ();
if (c == 0) {
// 公平性检查:检查队列中是否有前驱节点,如果有,则当前线程不能获取锁
if (!hasQueuedPredecessors () && compareAndSetState (0, acquires )) {
setExclusiveOwnerThread (current );
return true ;
}
} else if (current == getExclusiveOwnerThread ()) {
// 可重入逻辑:如果是当前线程持有锁,则增加持有次数
int nextc = c + acquires ;
if (nextc < 0) {
throw new Error ("Maximum lock count exceeded" );
}
setState (nextc );
return true ;
}
return false ;
}
// 尝试释放锁
protected boolean tryRelease (int releases ) {
int c = getState () - releases ;
if (Thread .currentThread ()!= getExclusiveOwnerThread ()) {
throw new IllegalMonitorStateException ();
}
boolean free = false ;
if (c == 0) {
free = true ;
setExclusiveOwnerThread (null );
return free;
}
// 提供一个条件变量,用于实现更复杂的同步需求,这里只是简单实现
ConditionObject newCondition() {
return new ConditionObject();
}
}
private final Sync sync = new Sync();
// 加锁方法
public void lock() {
sync.acquire(1);
}
// 解锁方法
public void unlock() {
sync.release(1);
}
// 判断当前线程是否持有锁
public boolean isLocked() {
return sync.isHeldExclusively();
}
// 提供一个条件变量,用于实现更复杂的同步需求,这里只是简单实现
public Condition newCondition() {
return sync.newCondition();
}
}如何⽤ MySQL 实现⼀个可重⼊的锁?
创建⼀个保存锁记录的表:
CREATE TABLE `lock_table` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
//该字段用于存储锁的名称,作为锁的唯一标识符。
`lock_name` VARCHAR(255) NOT NULL,
// holder_thread该字段存储当前持有锁的线程的名称,用于标识哪个线程持有该锁。
`holder_thread` VARCHAR(255),
// reentry_count 该字段存储锁的重入次数,用于实现锁的可重入性加锁的实现逻辑
开启事务
执⾏ SQL SELECT holder_thread, reentry_count FROM lock_table WHERE lock_name =? FORUPDATE ,查询是否存在该记 录:
如果记录不存在,则直接加锁,执⾏ INSERT INTO lock_table (lock_name, holder_thread,
reentry_count) VALUES (?,?, 1)如果记录存在,且持有者是同⼀个线程,则可冲⼊,增加重⼊次数,执⾏ UPDATE
lock_table SET reentry_count = reentry_count + 1 WHERE lock_name =?提交事 务
解锁的逻辑:
开启事务
执⾏ SQL SELECT holder_thread, reentry_count FROM lock_table WHERE lock_name =? FORUPDATE ,查询是否存在该记 录:
如果记录存在,且持有者是同⼀个线程,且可重⼊数⼤于 1 ,则减少重⼊次数 UPDATE
lock_table SET reentry_count = reentry_count - 1 WHERE lock_name =?如果记录存在,且持有者是同⼀个线程,且可重⼊数⼩于等于 0 ,则完全释放锁, DELETE
FROM lock_table WHERE lock_name =?提交事 务
