Java多线程面试题核心解析:线程池、锁机制与并发原理
2026/8/31 15:03:30 网站建设 项目流程

这次我们来看 Java 多线程面试题。内容不绕弯子,直接按考点拆。无论是校招还是社招,多线程基本都在二面或三面出现,而且经常和项目细节绑在一起问。很多候选人能背出synchronizedLock的区别,但一问到线程池拒绝策略、ThreadLocal的 key 为什么用弱引用、volatile到底保证什么,就明显卡壳。这篇内容把 Java 多线程面试题按知识体系整理成一份可直接复习的资料,每个章节给出核心结论、可运行的代码示例和面试回答思路,适合准备 Java 面试、想快速过一遍并发知识点的读者。

如果你只剩 3 天复习多线程,直接按这份清单走:第一天吃透线程基础、锁机制和 JMM;第二天搞定线程池、并发工具类和阻塞队列;第三天刷高频问答、过一遍 Spring Boot 中的多线程实战场景,再排查一遍常见问题。下面进入正题。

1. Java 多线程面试核心考点速览

先看整体知识分布,方便对照自查。

考点模块核心内容面试出现频率推荐掌握程度
线程基础创建方式、生命周期、startrun必会
JMM 与线程安全可见性、原子性、有序性、volatile必会
锁机制synchronizedLockCASAQS必会
线程池七大参数、执行流程、拒绝策略很高必会
ThreadLocal原理、内存泄漏、使用场景中高必会
并发工具类CountDownLatchCyclicBarrierSemaphore掌握
阻塞队列ArrayBlockingQueueLinkedBlockingQueue中高掌握
并发容器ConcurrentHashMapCopyOnWriteArrayList中高掌握
异步编程CompletableFutureFutureTask中高掌握
项目实战Spring Boot 异步、多线程调用外部接口会叙述场景

从这个表能看到,线程池、锁和 JMM 是面试官最常下手的位置。项目经历里如果能写清“我用线程池处理了什么任务、怎么设置的参数、有没有遇到拒绝或超时”,就是加分项。

2. 线程基础:生命周期与创建方式

2.1 线程创建的四种方式

Java 中创建线程主要有四种方式:

  1. 继承Thread类,重写run()
  2. 实现Runnable接口,传给Thread
  3. 实现Callable接口,配合FutureTask获取返回值。
  4. 通过线程池提交任务,底层也是基于RunnableCallable

实际开发中,继承Thread的方式很少用,因为 Java 单继承限制会让类设计变差。多数场景优先Runnable,需要返回值时用Callable

// Callable 示例 ExecutorService executor = Executors.newSingleThreadExecutor(); Future<Integer> future = executor.submit(() -> { return 1 + 1; }); Integer result = future.get(); // 阻塞获取结果 executor.shutdown();

2.2 线程六种状态与生命周期

线程状态在Thread.State枚举中定义:

public enum State { NEW, // 新建,还没调用 start() RUNNABLE, // 可运行,可能在运行,也可能在等待 CPU BLOCKED, // 阻塞,等待监视器锁 WAITING, // 等待,需要其他线程唤醒 TIMED_WAITING, // 限时等待,时间到自动唤醒 TERMINATED; // 终止 }

面试容易问这几个点:

  • start()run()的区别:start()会创建新线程并执行run()run()只是普通方法调用,不会启动新线程。
  • sleep()yield()的区别:sleep()让线程进入TIMED_WAITING,不释放锁;yield()只是让出 CPU,状态还是RUNNABLE
  • wait()sleep()的区别:wait()必须在同步代码块中调用,会释放锁;sleep()不释放锁。

一个易错点是:BLOCKED状态只在等待 synchronized 监视器锁时出现。如果是Lock接口的阻塞等待,线程处于WAITINGTIMED_WAITING,不进入BLOCKED

2.3 为什么说run()不能直接调用

直接调用run()是在当前线程执行任务,没有创建新线程。面试可以这样回答:start()的本质是告诉 JVM 创建一个新的操作系统线程,由该线程回调run();直接调run()只是普通方法调用,无法实现异步执行。

经常有候选人被问到“线程池提交任务时,如果直接调run()会怎样”,其实线程池内部也是这样,只有真正把任务交给 worker 线程执行才叫异步,否则就在提交线程里串行跑。

3. JMM 与线程安全三要素

3.1 Java 内存模型

Java 内存模型(JMM)规定了共享变量在不同线程之间的可见性规则。它定义了一套抽象内存结构,不是实际的物理内存布局:

  • 所有共享变量存在主内存。
  • 每个线程有自己的工作内存,保存共享变量的副本。
  • 线程操作变量时,先把主内存变量读到工作内存,修改后再写回主内存。

这就带来三个问题:

  • 可见性:线程 A 修改了变量,线程 B 不一定立刻看到。
  • 原子性:复合操作如i++,不是一步完成。
  • 有序性:编译器和 CPU 可能重排序。

volatile解决可见性和有序性,不解决原子性。synchronizedLock同时解决三个问题,因为锁保证互斥,也就保证了原子性。

3.2volatile到底保证什么

volatile有两个核心语义:

  1. 写一个volatile变量时,JMM 会把该线程工作内存中的变量强制刷新到主内存。
  2. 读一个volatile变量时,JMM 会使线程工作内存中该变量的副本失效,必须从主内存重新读取。

面试经典题:用volatile实现线程安全的单例。

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; } }

这里为什么用volatilenew Singleton()不是原子操作,在 JVM 层面分三步:

  1. 分配内存。
  2. 初始化对象。
  3. 引用指向内存地址。

如果不用volatile,2 和 3 可能重排序,另一个线程会拿到一个“未完全初始化”的对象。

volatile不能解决原子性的典型例子是count++。所以日常开发中,不要指望volatile替代锁或原子类。

3.3 happens-before 规则

JMM 定义了一组 happens-before 规则,用来判断某个写操作是否对另一个读操作可见:

  • 程序顺序规则:同一线程中,前面的操作 happens-before 后面的操作。
  • 锁规则:解锁 happens-before 后续的加锁。
  • volatile规则:volatile写 happens-before 后续的volatile读。
  • 传递性:A happens-before B,B happens-before C,则 A happens-before C。
  • 线程启动规则:start()之前的操作 happens-before 新线程中的操作。
  • 线程终止规则:线程中的所有操作 happens-before 其他线程检测到该线程终止。

回答可见性问题时,抬出 happens-before 规则会很加分。面试官会接着问“volatile为什么能保证可见性”,其实就是volatile写读规则。

4. 锁机制:synchronized、Lock、CAS、AQS

4.1synchronized的锁升级过程

synchronized在 JDK 1.6 之后做了大量优化,锁存在四种状态,按竞争程度升级:

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。

  • 偏向锁:只有一个线程访问同步块时,偏向该线程,减少 CAS 开销。
  • 轻量级锁:有竞争时,通过 CAS 尝试获取锁,适应线程交替执行场景。
  • 重量级锁:竞争激烈时,升级为依赖操作系统互斥量的锁,涉及用户态和内核态切换,开销大。

面试被问到“synchronized为什么重”,要能解释锁升级和重量级锁的内核态切换开销。

4.2synchronizedLock怎么选

高频对比题,建议直接记住下表:

对比点synchronizedLock
锁获取方式自动获取/释放手动lock()/unlock()
可中断性不可中断lockInterruptibly()可中断
公平性非公平ReentrantLock支持公平/非公平
多个条件一个监视器newCondition()支持多个条件
性能优化后差距不大灵活性高

synchronized还是ReentrantLock,结论是:能不加锁就不加锁;必须加锁时优先synchronized,因为写法简单、不容易忘释放锁。只有需要可中断、公平锁、多条件队列时,才用ReentrantLock

ReentrantLock lock = new ReentrantLock(true); // 公平锁 Condition notFull = lock.newCondition(); Condition notEmpty = lock.newCondition(); lock.lock(); try { // 业务逻辑 } finally { lock.unlock(); }

4.3 CAS 与 ABA 问题

CAS(Compare And Swap)是原子操作,底层调用 CPU 的cmpxchg指令。它包含三个操作数:内存位置 V、旧值 A、新值 B。只有 V 等于 A 时,才把 V 更新为 B。

Java 中AtomicIntegerLongAdderConcurrentHashMap都依赖 CAS。

CAS 的经典问题是 ABA:

  1. 线程 1 读取值为 A。
  2. 线程 2 把值从 A 改成 B,再改回 A。
  3. 线程 1 执行 CAS,发现值还是 A,于是 CAS 成功。

实际上变量已经被改过两次。解决 ABA 可以使用版本号,Java 中AtomicStampedReference就是这个思路。

4.4 AQS 是什么

AQS(AbstractQueuedSynchronizer)是ReentrantLockSemaphoreCountDownLatch等并发工具类的底层框架。核心是一个volatile int state和一个 CLH 变体的等待队列。

  • state == 0表示锁未被占用。
  • 线程获取锁失败,会封装成 Node 节点进入同步队列,阻塞挂起。
  • 持有锁的线程释放后,唤醒队列头部节点。

回答 AQS 时,不需要背内部实现细节,但要能说清“状态变量 + FIFO 等待队列 + 模板方法模式”这三层。

5. 线程池核心考点

5.1 七大参数

线程池在 Java 面试里出现频率极高,核心是ThreadPoolExecutor的构造参数。直接背记忆点:

public ThreadPoolExecutor( int corePoolSize, // 核心线程数 int maximumPoolSize, // 最大线程数 long keepAliveTime, // 非核心线程空闲存活时间 TimeUnit unit, // 存活时间单位 BlockingQueue<Runnable> workQueue, // 任务队列 ThreadFactory threadFactory, // 线程工厂 RejectedExecutionHandler handler // 拒绝策略 )

面试必问:线程池执行流程。

  1. 提交任务后,当前线程数小于corePoolSize,创建核心线程执行。
  2. 当前线程数等于corePoolSize,任务进入workQueue排队。
  3. 队列满,当前线程数小于maximumPoolSize,创建非核心线程执行。
  4. 队列满且线程数达到maximumPoolSize,执行拒绝策略。

这个流程一定要按顺序说,面试官会追问“队列满了为什么是先加线程而不是先拒绝”,答案是:线程池设计思想是先用队列缓冲,再扩线程,最后才拒绝,尽可能保证任务被执行。

5.2 四种拒绝策略

ThreadPoolExecutor内置四种策略:

策略行为
AbortPolicy直接抛RejectedExecutionException
CallerRunsPolicy调用者线程自己执行任务
DiscardPolicy静默丢弃任务
DiscardOldestPolicy丢弃队列最老任务,然后重试提交

平时默认是AbortPolicy。实际项目里CallerRunsPolicy比较实用,因为它能保留任务,但会对调用方造成背压,需要评估调用方能否承受。

5.3 线程池创建方式

禁用Executors中的快捷方法创建线程池,原因是:

  • FixedThreadPoolSingleThreadPool任务队列使用了无界LinkedBlockingQueue,可能堆积大量请求导致 OOM。
  • CachedThreadPool线程数无上限,可能创建大量线程导致 OOM。
  • ScheduledThreadPool同样存在线程数上限问题。

面试时直接回答:阿里巴巴开发规范明确建议手动创建ThreadPoolExecutor,明确参数,规避资源耗尽风险。

ThreadPoolExecutor executor = new ThreadPoolExecutor( 4, 8, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000), new ThreadFactoryBuilder().setNameFormat("biz-thread-%d").build(), new ThreadPoolExecutor.CallerRunsPolicy() );

5.4 如何设置线程池参数

这是个高频场景题,没有唯一答案,但可以给出合理思路。任务分两类:

  • CPU 密集型:线程数设置为 CPU 核数 + 1。
  • IO 密集型:线程数设置为 CPU 核数 * 2,或者按“CPU 核数 / (1 - 阻塞系数)”估算。

阻塞系数高时,线程数可以更多。实际项目中还要考虑下游服务吞吐量和调用超时时间,不能只套公式。

线上排查线程池问题,先看监控指标:活跃线程数、队列积压量、拒绝任务数。如果队列长期积压,优先调大队列容量或增加核心线程数,而不是无脑调大maximumPoolSize

6. ThreadLocal 与内存泄漏

6.1 ThreadLocal 原理

ThreadLocal为每个线程提供变量副本。每个Thread对象内部有一个ThreadLocalMap,key 是ThreadLocal对象,value 是线程本地变量。

关键设计点:

  • ThreadLocalMap的 key 是ThreadLocal的弱引用。
  • value 是强引用。
  • 弱引用 key 在 GC 时可能被回收,变成key == null的 Entry。
  • 如果不清理ThreadLocalMap中的无效 Entry,value 会一直存在,直到线程结束。

6.2 内存泄漏与规避

线程池中的线程是复用的,不会轻易销毁,如果ThreadLocal的 value 不被及时删除,就会发生内存泄漏。

规避手段是:每次用完ThreadLocal都调用remove()

private static final ThreadLocal<UserContext> USER_CONTEXT = new ThreadLocal<>(); public void process() { try { USER_CONTEXT.set(user); // 业务代码 } finally { USER_CONTEXT.remove(); } }

面试回答模板:ThreadLocal的设计是 key 弱引用、value 强引用。弱引用 key 被回收后,value 无法被访问但依然被ThreadLocalMap引用。线程池复用时线程不销毁,value 就形成泄漏。解决方法是显式remove()

顺带记住ThreadLocal的常见应用:SimpleDateFormat线程安全改造、Spring 事务上下文、TraceId 传递、用户登录信息保存。

7. 并发工具类与阻塞队列

7.1 CountDownLatch 与 CyclicBarrier

两者都是线程协作工具,容易混淆。

CountDownLatch:一个或多个线程等待其他线程完成后再继续,计数递减到 0 时放行。

int taskCount = 5; CountDownLatch latch = new CountDownLatch(taskCount); for (int i = 0; i < taskCount; i++) { executor.submit(() -> { try { // 执行任务 } finally { latch.countDown(); } }); } latch.await(10, TimeUnit.SECONDS);

CyclicBarrier:一组线程互相等待,全部到达屏障点后一起继续,可以循环复用。

对比结论:CountDownLatch是一次性的,减计数,适合任务汇总;CyclicBarrier可循环,加计数,适合多线程任务分批执行,比如多阶段并行计算。

7.2 Semaphore

Semaphore控制同时访问共享资源的线程数,适合做限流。

Semaphore semaphore = new Semaphore(3); for (int i = 0; i < 10; i++) { executor.submit(() -> { try { semaphore.acquire(); // 受限资源访问 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { semaphore.release(); } }); }

Semaphore区别于线程池的地方是:线程池限制线程数量,Semaphore限制资源访问并发数,两者可以配合使用。

7.3 阻塞队列

阻塞队列是线程池的核心组成部分。常见实现:

  • ArrayBlockingQueue:有界数组队列,FIFO。
  • LinkedBlockingQueue:链表队列,可设置容量,默认容量Integer.MAX_VALUE,所以无界时要小心 OOM。
  • SynchronousQueue:不存储元素,每个插入操作必须等待一个移除操作。
  • PriorityBlockingQueue:支持优先级排序。
  • DelayQueue:延迟队列,任务到期后出队。

put()take()是阻塞方法,一个在队列满时阻塞,一个在队列空时阻塞。生产消费者模型就可以基于这两个方法实现。

7.4 并发容器

ConcurrentHashMap是面试重点。回答这个问题要注意版本差异。

JDK 1.7 中,ConcurrentHashMap用分段锁实现。默认 16 个 Segment,每个 Segment 相当于一个小的HashMap,锁粒度是 Segment。

JDK 1.8 中,组织结构改成数组 + 链表 + 红黑树,锁粒度细化到桶,使用synchronized+ CAS。插入时如果桶为空,直接用 CAS;桶不为空时,锁住桶头节点。因为细化了锁粒度,并发能力比 1.7 更强。

CopyOnWriteArrayList是读写分离思想的实现。写操作时复制底层数组,写完后替换引用;读操作不加锁。适合读多写少的场景,比如白名单、路由配置等。缺点是内存占用高,每次写都会复制数组。

8. Spring Boot 中的多线程场景

8.1 Spring Boot 请求本身是多线程吗

很多候选人被问“Spring Boot 的请求是多线程吗”。准确回答是:

  • 每个 HTTP 请求默认由 Tomcat 的线程池中的线程处理。
  • Spring Boot 默认使用 Tomcat,这里有最大工作线程数配置,比如server.tomcat.max-threads
  • 同一个请求内部默认是单线程执行的,除非显式开启异步处理。
  • 多个请求之间是并发的,但每个请求自己走一条独立的线程路径。

这里经常会问“Controller 是线程安全的吗”。答案是:默认单例,且不是线程安全的。如果 Controller 里定义了一个可变的实例变量,多个请求同时写入就会产生线程安全问题。正确做法是不在 Controller 中保存可变状态。

8.2 多线程调用外部接口

项目里常见的场景是:主线程需要并行调用多个外部接口,汇总结果后返回。可以这样实现:

public OrderDetail queryOrderDetail(Long orderId) { CompletableFuture<OrderInfo> orderFuture = CompletableFuture .supplyAsync(() -> orderClient.getOrder(orderId), bizExecutor); CompletableFuture<UserInfo> userFuture = CompletableFuture .supplyAsync(() -> userClient.getUser(orderId), bizExecutor); return CompletableFuture.allOf(orderFuture, userFuture) .thenApply(v -> { OrderDetail detail = new OrderDetail(); detail.setOrder(orderFuture.join()); detail.setUser(userFuture.join()); return detail; }) .join(); }

这样能把顺序调用的 N 次耗时压到一次并发耗时,但要注意三个问题:

  1. 外部接口超时设置,避免多个线程同时卡住。
  2. 线程池参数和下游吞吐量匹配。
  3. 异常链路要捕获并把错误信息透出到上层。

8.3 CompletableFuture 核心用法

CompletableFutureFuture的增强版,支持编排多个异步任务。

常用方法:

  • supplyAsync():异步执行有返回值的任务。
  • thenApply():任务完成后转换结果。
  • thenAccept():任务完成后消费结果。
  • exceptionally():异常恢复。
  • allOf():等待所有任务完成。
  • anyOf():任意一个任务完成即可。

需要强调:CompletableFuture默认使用 ForkJoinPool.commonPool,如果任务是阻塞 IO,要传入自定义线程池,避免公共线程池被打满。

9. Java 多线程高频面试题清单

下面整理一份高频题目的“答题骨架”,适合最后一天速记。

9.1 什么是线程安全?如何实现线程安全?

线程安全是指多个线程访问同一个共享变量时,程序行为依然正确。实现方式有:使用局部变量、加锁、使用原子类、使用不可变对象、使用并发容器。不要只说“加锁”,要能列举多种手段。

9.2 为什么不建议用Executors创建线程池

因为没有显式指定线程池参数,容易造成资源耗尽。FixedThreadPool使用无界队列,任务堆积可能 OOM;CachedThreadPool线程数无上限,线程过多也可能 OOM。回答时把ThreadPoolExecutor的七大参数带上,说明手动创建更可控。

9.3synchronized锁的是什么

锁对象取决于用法:

  • 修饰实例方法:锁当前实例this
  • 修饰静态方法:锁当前类的Class对象。
  • 修饰代码块:锁括号里指定的对象。

面试加分点:静态方法和实例方法不能互相影响,因为它们锁对象不同。

9.4 什么是死锁?如何避免?

死锁是多个线程互相持有对方需要的锁,导致互相等待且永不释放。经典条件是互斥、持有并等待、不可剥夺、循环等待。

避免方式:

  • 尽量避免嵌套加锁。
  • 按固定的锁顺序加锁。
  • 使用LocktryLock(timeout),获取不到就放弃。
  • 使用并发工具类如SemaphoreReentrantLock替代手动多锁。

写一个死锁示例,面试官通常会问“怎么定位”。可以回答:先jps找到进程号,再用jstack <pid>查看线程 dump,能看到Found one Java-level deadlock提示。

9.5i++为什么不是线程安全的

因为i++在字节码层面至少有读取、加一、写回三步,不是原子操作。多个线程同时执行时,会互相覆盖写回结果。解决方式:使用AtomicInteger,或者加synchronized,或者用LongAdder提升高并发计数性能。

9.6wait()notify()为什么必须在同步代码块中

这是为了确保等待和通知操作与共享变量的修改互斥,避免“丢失通知”问题。如果不在同步块内,线程可能在“检查条件”和“进入等待”之间被打断,另一个线程此时已经执行完notify(),等待线程永远不会被唤醒。

9.7 线程池队列满了会怎样

先判断线程数是否达到maximumPoolSize。没达到就创建非核心线程;达到了就执行拒绝策略。拒绝策略默认抛异常。面试时可以补一句:线程池的设计是先入队再扩线程,这个顺序很关键。

9.8 sleep 和 wait 的区别

sleepThread的静态方法,waitObject的方法;sleep不释放锁,wait释放锁;sleep自动唤醒,wait需要notifynotifyAll唤醒。

9.9 如何实现生产者消费者模型

可以用BlockingQueue实现,这是最简单的方式;也可以用wait/notify原生实现,但要注意使用while循环检查条件,防止虚假唤醒。

9.10 什么是虚假唤醒

线程可能在没收到notify的情况下被唤醒。因此wait()一定要放在while循环中,而不是if中。

10. 多线程场景常见报错与排查

结合热搜词里的java: outofmemoryerror: insufficient memory,多线程场景最容易出现的报错集中在资源耗尽上。

问题现象可能原因排查方式解决方案
OutOfMemoryError: unable to create new native thread线程数过多或系统线程限制ulimit -ujstack统计线程数优化线程池参数,减少线程创建
OutOfMemoryError: insufficient memory堆内存不足,可能是任务队列堆积jmap -heap、GC 日志限制任务队列容量,增大堆内存或排查对象泄漏
线程池任务堆积队列设置不合理或下游响应慢监控队列大小、活跃线程数调整corePoolSize与队列容量,增加超时控制
接口响应线程卡死线程池拒绝策略触发且未熔断查看拒绝日志、jstack抓线程状态使用CallerRunsPolicy并加熔断降级
ThreadLocal内存泄漏导致 Full GC未调用remove()用 MAT 或jmap分析堆finallyremove()

排查多线程问题,优先记住三个工具命令:

jps -l jstack <pid> jstat -gcutil <pid> 1000

jstack能看到线程栈、锁等待、死锁和阻塞状态,是排查并发问题最直接的命令。

11. 3 天复习计划与最佳实践

最后给一份可执行的 3 天计划。

11.1 第一天:基础与核心概念

当天目标是把线程生命周期、JMM、volatilesynchronizedLock过一遍。每学一个概念,手写一个小例子,比如写一个多线程累加的例子,观察volatile为什么不能让count++线程安全。理论必须配合示例代码,否则面试时很难转述成自己的语言。

11.2 第二天:线程池与并发工具

当天重点做三件事:

  1. 手写一个ThreadPoolExecutor,把七大参数全部设置成明确值。
  2. 跑一个CountDownLatch等待多个任务完成的例子。
  3. CompletableFuture模拟并行调用两个外部接口。

线程池参数不要死记,要理解变化过程:任务提交时先判断核心线程,再排队,再扩线程,最后拒绝。

11.3 第三天:高频问答、项目场景与排查

第三天不再看新知识点,把手上的高频清单过一遍,挑出 8 个回答最不流畅的题目,写一遍自己的回答稿。同时准备一个多线程项目场景:比如订单详情并行查询、批量任务处理、日志异步上报,把技术点串进去。

11.4 多线程实战最佳实践

  • 线程池统一管理,禁止在方法内部随手new Thread
  • 所有线程池使用有界队列,并明确拒绝策略。
  • 原子类不滥用,长耗时场景不要长时间持有锁。
  • 批量并发任务要设置总超时时间。
  • 关键业务链路加监控,线程池队列深度、活跃线程数、拒绝次数都要有指标。
  • 加锁范围尽量小,只锁共享资源操作,不要锁整个方法。

总结与下一步

Java 多线程在面试中出现频率非常稳定,而且容易和项目经验联动。最容易丢分的不是概念本身,而是“概念都知道,但说不清楚应用场景”。复习时每学一个机制,都要问自己:这东西在项目里哪里用到了?如果没用过,写代码的时候实际跑一遍,面试就能给出真实的细节。

建议把这份清单转成自己的速查表格:线程池七大参数、拒绝策略选择、volatilesynchronized的语义边界、ThreadLocal的泄漏原因、并发工具类的适用场景。先把这五块吃透,再扩展AQSCompletableFuture和 Spring Boot 实战,多线程面试题的核心覆盖就基本到位了。真正面试时,要避免只背结论,尽量用自己的项目案例来佐证,这样比单纯背诵八股文更有说服力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询