源本科技 | 码上会

企业社招真题 Java 多线程与并发

2026/04/05
3
0

synchronized 和 ReentrantLock 的区别

两者都是可重入锁,核心区别在使用方式和功能。synchronized 是 JVM 层面的关键字,自动加锁解锁,异常时也能释放锁,使用简单但功能单一。ReentrantLock 是 JDK 层面的 API,需手动 lock/unlock(配合 finally),更灵活。它支持公平锁、可中断锁、多个 Condition 条件队列,还能尝试获取锁。synchronized 只有非公平锁,无法中断等待。JDK1.8 后 synchronized 优化后性能接近 ReentrantLock,简单同步用 synchronized,复杂场景如需要超时、中断、多条件则用 ReentrantLock。

synchronized 和 volatile 关键字的区别

两者都用于并发,作用完全不同。volatile 是轻量级同步机制,只保证可见性和禁止指令重排,不保证原子性,比如 i++ 这种复合操作用 volatile 仍会线程不安全。synchronized 是重量级锁,能同时保证可见性、原子性、有序性,但会阻塞线程。volatile 不会引起线程上下文切换,开销极小;synchronized 会导致线程阻塞,开销更大。volatile 修饰变量,synchronized 修饰方法或代码块。简单的状态标记用 volatile,复杂的复合操作必须用 synchronized。

Java 内存模型(JMM)和 happens-before 原则

JMM 是一套规范,解决多线程下内存可见性、有序性问题。它定义了主内存和线程工作内存,线程操作变量会先拷贝到工作内存,修改后同步回主内存。happens-before 是 JMM 的核心规则,用来判断操作是否有序、结果是否可见,不用深入底层就能确定线程执行顺序。比如解锁先于加锁、volatile 写先于读、线程 start 先于执行等。只要满足 happens-before,前一个操作结果对后一个可见,保证并发安全,是编写线程安全代码的重要依据。

Java 内存模型中可见性、原子性、有序性的含义

可见性是一个线程修改了共享变量,其他线程能立即看到最新值,多线程下工作内存缓存会导致不可见,可用 volatile、synchronized 保证。原子性是操作不可分割,要么全部执行,要么都不执行,比如赋值是原子性,i++ 是三步操作不具备原子性,需加锁保证。有序性是程序执行顺序符合代码逻辑,JVM 会指令重排优化,多线程下会出问题,JMM 通过 volatile、内存屏障、happens-before 禁止重排,保证有序。这三大特性是线程安全的核心。

volatile 关键字的作用及底层原理

volatile 主要有两个作用:保证变量可见性、禁止指令重排。底层靠内存屏障实现。写 volatile 变量时,会加写屏障,强制把工作内存值同步到主内存;读的时候加读屏障,强制从主内存读取最新值,保证可见性。内存屏障还会禁止指令重排序,避免多线程下代码执行顺序错乱。但 volatile不能保证原子性,因为它不阻塞线程,只针对单个读 / 写操作。适合状态标记、单例双重检查等场景,不适合计数、累加等复合操作。

线程池工作原理及调优策略

线程池就是复用线程,避免频繁创建销毁。提交任务时,先判断核心线程数是否满,没满就新建核心线程执行;满了放入阻塞队列;队列满了就开非核心线程;达到最大线程数就执行拒绝策略。调优先看场景:CPU 密集型设核心数为 CPU 核心数 +1,IO 密集型设大一些,比如核心数 *2。队列用有界队列,避免 OOM。根据任务类型调整核心线程、最大线程、队列容量,选择合适拒绝策略,监控线程活跃度,避免线程过多或阻塞。

Executor 框架解决的问题

Executor 框架简化线程的创建、管理、调度,解决手动创建线程的弊端。手动用 Thread 创建线程,不好控制数量,频繁创建销毁开销大,还可能导致资源耗尽。Executor 提供 ThreadPoolExecutor、Executors 工具类,统一管理线程生命周期,复用线程,控制并发数,提供任务队列和拒绝策略。它解耦任务提交和执行,开发者只需定义任务,不用关心线程细节。还支持异步执行、定时任务,提升并发效率,是 Java 高并发开发的基础框架。

线程间通信的实现方式

线程间通信主要是共享内存和消息传递。常用方式:wait/notify/notifyAll,配合 synchronized 使用,线程等待后释放锁,避免死循环,是经典方式。用 Lock 的 Condition,await/signal/signalAll,比 wait/notify 更灵活,支持多个等待队列。volatile 共享变量,适合简单状态通知。管道流用于字节 / 字符传输。CountDownLatch、CyclicBarrier、Semaphore 等工具类,实现线程间同步等待。实际开发中,Lock+Condition 更灵活,synchronized+wait/notify 更简单,按需选择。

死锁的产生及避免方法

死锁是多个线程互相持有对方需要的锁,都不释放,全部阻塞。产生需四个条件:互斥、持有并等待、不可抢占、循环等待。避免死锁就破坏任一条件。统一锁的获取顺序,所有线程按相同顺序加锁,破坏循环等待。给锁加超时时间,用 ReentrantLock 的 tryLock,超时放弃,破坏持有并等待。减少锁嵌套,避免一个锁内加另一个锁。尽量不使用独占锁,用并发工具类。开发中避免嵌套锁,规范加锁顺序,就能大幅减少死锁。

守护线程与用户线程的区别

Java 线程分守护线程和用户线程。用户线程是工作线程,JVM 会等所有用户线程结束才退出。守护线程是后台服务线程,为用户线程服务,比如 GC 线程。当所有用户线程结束,不管守护线程是否执行完,JVM 都会直接退出。设置方式是 thread.setDaemon(true),必须在 start 前调用。守护线程不能用来处理业务逻辑、关闭资源,因为随时可能终止。用户线程执行业务,守护线程做后台辅助工作,这是最核心的区别。

ThreadLocal 实现线程间数据隔离的原理及用途

ThreadLocal 实现线程数据隔离,每个线程有自己的独立副本,互不干扰。原理是每个 Thread 有 ThreadLocalMap,以 ThreadLocal 为 key,存储数据。set 时把值存到当前线程的 map,get 时从当前线程取,其他线程访问不到。它不解决共享变量问题,而是隔离数据。常用于存储用户会话、数据库连接、事务上下文,避免层层传递参数。使用完必须 remove,否则会内存泄漏,因为线程池线程复用,map 不会被清理。

乐观锁与悲观锁的概念及应用

悲观锁认为并发一定会冲突,每次操作都加锁,独占资源,其他线程阻塞。比如 synchronized、ReentrantLock,适合写多的场景,保证强一致性。乐观锁认为冲突概率低,不加锁,更新时判断数据是否被修改。常用 CAS 实现,比较版本号或原值,一致就更新,不一致重试。比如 Atomic 原子类、MySQL 版本号机制。乐观锁无锁开销,并发性能高,适合读多写少场景。两种锁按需选择,写多读少用悲观,读多写少用乐观。

锁优化技术(锁升级、自旋锁等)

JVM 对 synchronized 做了大量锁优化。锁升级:无锁→偏向锁→轻量级锁→重量级锁,逐级升级,减少开销。偏向锁默认同一个线程获取锁,无同步操作;轻量级锁用 CAS 自旋,不阻塞线程;自旋锁让线程循环尝试获取锁,不进入阻塞,减少上下文切换,适合锁持有时间短的场景。还有锁粗化、锁消除,锁粗化合并连续加锁,锁消除消除无必要的锁。这些优化让 synchronized 性能大幅提升,适配不同并发场景。

AQS(AbstractQueuedSynchronizer)原理及应用

AQS 是 Java 并发锁的基础框架,ReentrantLock、CountDownLatch 都基于它。核心是volatile state 状态位双向队列。获取锁时,用 CAS 修改 state,成功就持有;失败就加入队列阻塞。释放锁时,修改 state,唤醒队列后继线程。AQS 分独占和共享模式,独占锁同一时间一个线程持有,共享锁可多个线程持有。它封装了线程阻塞、唤醒、队列管理,开发者只需重写 tryAcquire、tryRelease 等方法,简化同步组件开发。

内存屏障的作用

内存屏障是 CPU 指令,用来解决可见性和有序性问题。它有两个作用:一是强制刷新缓存,写屏障把数据同步到主内存,读屏障从主内存读取最新值,保证可见性。二是禁止指令重排,屏障前后的指令不能跨越屏障执行,保证有序性。volatile 底层就是靠内存屏障实现,synchronized、Lock 也会用到。JMM 靠内存屏障保证 happens-before 规则,避免多线程下指令重排导致的数据错乱,是实现线程安全的底层关键。

as-if-serial 语义的含义

as-if-serial 是单线程下的语义规则,意思是不管指令怎么重排,最终执行结果和代码顺序一致。JVM 为了优化效率,会对指令重排序,但在单线程里,重排不会改变结果,程序员不用关心。它只保证单线程安全,多线程下如果没有同步措施,指令重排会导致结果错误。比如多线程下未用 volatile,指令重排会让变量赋值顺序错乱,出现脏数据。这个语义是 JVM 优化的基础,同时也提醒多线程必须加同步。

线程协作机制

线程协作是让多个线程按规则执行,比如等待、唤醒、同步。常用 wait/notify/notifyAll,结合 synchronized,线程调用 wait 释放锁进入等待,notify 唤醒等待线程。Lock 的 Condition 更灵活,一个锁可以多个 Condition,分别等待唤醒。CountDownLatch 是倒计时,线程等待计数为 0 再执行。CyclicBarrier 是循环屏障,所有线程到齐再一起执行。Semaphore 控制并发线程数。这些机制解决线程间执行顺序、等待通知问题,保证业务流程有序。

高并发下实现高效并发的策略

高并发要兼顾性能和安全,核心策略:减少锁竞争,用无锁编程如 volatile、CAS、Atomic 类。用细粒度锁,比如 ConcurrentHashMap 分段锁,缩小锁范围。使用线程池复用线程,避免创建开销。用读写锁,读共享写独占,提升读性能。使用 CopyOnWrite 容器,读无锁。异步处理,用消息队列削峰填谷。多级缓存,减少数据库压力。合理分库分表,分散请求。优化 GC,减少 STW 时间。多种手段结合,实现高吞吐、低延迟的高效并发。

线程安全的单例模式实现方式

线程安全单例保证一个类只有一个实例。饿汉式:类加载就创建,天生线程安全,简单但可能浪费内存。懒汉式:synchronized 修饰方法,线程安全但效率低。双重检查锁 DCL:两次判断 null,加 volatile 禁止重排,安全高效。静态内部类:类加载不创建,调用时才初始化,利用类加载机制保证安全,推荐使用。枚举单例:最简单,天然防反射、反序列化破坏,是最安全的方式。日常开发推荐静态内部类或枚举单例。

双重检查锁定单例模式及问题

双重检查锁是高效的单例实现,两次判断 instance 是否为 null,中间加锁,避免频繁加锁。但早期有指令重排问题:创建对象分分配内存、初始化、赋值三步,重排可能先赋值再初始化,其他线程拿到未初始化的对象。解决方法是给 instance 加 volatile,禁止指令重排。加 volatile 后,DCL 安全高效,是常用单例方式。它兼顾懒加载和性能,注意必须加 volatile,否则在多线程下仍有风险。

如何安全终止一个线程

安全终止线程不能用 stop(),会强制终止,导致数据不一致。推荐用标志位:定义 volatile 布尔变量,线程循环判断标志,为 false 就退出。阻塞线程可用 interrupt() 中断,线程捕获 InterruptedException,清理资源后退出。interrupt 只是设置中断标志,不会直接终止,需配合代码判断。不要用废弃方法,通过优雅的标志位或中断,让线程执行完逻辑、释放资源后正常结束,保证数据安全和程序稳定。