多线程 线程池 并发
核心知识点
1. 线程与进程
进程(Process):是操作系统资源分配的基本单位。每个进程都有独立的内存空间。
线程(Thread):是 CPU 调度的基本单位,是程序执行流的最小单元。一个进程可以包含多个线程,线程共享进程的内存空间。
2. 并发与并行
并发(Concurrency):指多个任务在同一时间段内交替执行,但在任一时刻只有一个任务在运行。
并行(Parallelism):指多个任务在同一时刻同时执行。这需要多核 CPU 的支持。
3. 线程状态
一个线程的完整生命周期会经历 6 种状态:
NEW:线程被创建但未启动。
RUNNABLE:线程正在执行,或正在等待 CPU 调度。
BLOCKED:线程正在等待监视器锁(如
synchronized
)进入同步代码块。WAITING:线程无限期地等待另一个线程执行特定操作。
TIMED_WAITING:线程在指定的时间内等待。
TERMINATED:线程执行完毕或异常终止。
4. 线程安全问题
当多个线程同时访问和修改共享资源时,由于执行顺序不确定,可能导致数据不一致,这就是竞态条件(Race Condition)。解决线程安全问题的核心是同步(Synchronization)。
5. 线程池(Thread Pool)
线程池是一种管理和复用线程的机制。它通过一个队列来存放任务,并用一组固定的线程来执行这些任务。
优点:
降低开销:避免了频繁创建和销毁线程的资源消耗。
资源管理:控制并发线程数,防止因线程过多而耗尽系统资源。
提高响应速度:任务无需等待新线程创建,可立即执行。
高频面试题与解答
1. synchronized
和 volatile
关键字的区别?
synchronized
:提供互斥性和可见性。它保证同一时刻只有一个线程可以访问同步代码块,并且在释放锁时,会将工作内存中的共享变量刷新到主内存,保证其他线程的可见性。volatile
:只提供可见性,不提供互斥性。它保证对一个volatile
变量的读写操作都是直接在主内存中进行,确保多个线程看到的是同一个值。但它不保证原子性,不适合用于复合操作(如i++
)。
2. synchronized
和 ReentrantLock
的区别?
synchronized
:是 Java 语言内置的关键字。
使用简单,由 JVM 自动管理锁的获取和释放。
是非公平锁。
ReentrantLock
:是
java.util.concurrent.locks.Lock
接口的实现类。功能更丰富,提供了可中断、可限时、公平锁(可选)等功能。
需要手动获取和释放锁,通常在
try-finally
块中确保释放,避免死锁。
3. 为什么说 volatile
不保证原子性?
volatile
确保了可见性,但不保证原子性。例如,i++
实际上是三个操作:
读取
i
的值。对
i
进行加 1 操作。将新值写入
i
。
在多线程环境下,一个线程读取 i
的值后,另一个线程可能已经完成了 i++
操作,但第一个线程的写入操作会覆盖后一个线程的结果,导致数据错误。
4. 请解释一下 CAS?它解决了什么问题?
CAS(Compare-and-Swap) 是一种无锁算法。它包含三个操作数:
内存位置 V:需要更新的变量。
旧的预期值 A:假设该变量当前的值。
新的值 B:想要更新成的值。
CAS 的原子操作是:当且仅当内存位置 V 的值等于 A 时,才将 V 的值更新为 B。如果 V 的值不等于 A,则说明已被其他线程修改,更新失败。
它解决了在多线程下非阻塞地更新共享变量的问题,是许多并发工具(如 AtomicInteger
)的底层实现。
5. 什么是死锁?如何避免?
死锁是指两个或多个线程在执行过程中,因争夺资源而造成相互等待,若无外力干涉,它们都将无法继续执行。
死锁的四个必要条件:
互斥条件:资源不能被共享。
请求与保持:线程已持有资源,又请求新资源。
不剥夺:已分配的资源不能被强制剥夺。
循环等待:形成一个环形链,每个线程都在等待下一个线程释放资源。
避免策略:破坏这四个条件中的任意一个。最常用的方法是破坏循环等待条件,通过对资源进行排序,按顺序申请资源。
6. 使用线程池的好处是什么?ThreadPoolExecutor
的核心参数有哪些?
好处:
降低资源消耗:通过线程复用,减少创建和销毁线程的开销。
提高响应速度:任务提交后立即执行,无需等待线程创建。
方便管理:统一管理线程,控制并发数量,防止资源耗尽。
核心参数:
corePoolSize
:核心线程数,线程池中始终保持的线程数量。maximumPoolSize
:最大线程数,线程池中允许的最大线程数量。keepAliveTime
:当线程数大于核心线程数时,多余的空闲线程的存活时间。unit
:keepAliveTime
的时间单位。workQueue
:任务队列,用于存放待执行的任务。threadFactory
:创建线程的工厂。handler
:拒绝策略,当任务队列已满且线程数达到最大值时,如何处理新任务。
7. Runnable
和 Callable
有什么区别?
Runnable
:run()
方法没有返回值。不能抛出受检异常。
Callable
:call()
方法有返回值,通常是Future<V>
。可以抛出异常。
执行方式:
Runnable
通常由Thread
执行;Callable
通常由ExecutorService
提交,并通过Future
对象获取结果。
8. CountDownLatch
和 CyclicBarrier
的区别?
CountDownLatch
:倒计时门闩。它允许一个或多个线程等待,直到其他线程完成操作。它是一次性的,count
减到 0 后就不能再用了。CyclicBarrier
:循环屏障。它允许多个线程相互等待,直到所有线程都到达同一个屏障点,然后它们可以一起继续执行。CyclicBarrier
可以重复使用。
9. 什么是守护线程(Daemon Thread)?
守护线程是一种特殊的线程,它在后台为其他非守护线程提供服务。JVM 在所有非守护线程都结束后,会自动终止。例如,Java 的垃圾回收线程(GC)就是一个守护线程。
10. happens-before
原则是什么?
happens-before
原则定义了多线程环境下,一个操作的结果对另一个操作的可见性。它是 Java 内存模型(JMM)中的核心概念。
例如,如果操作 A happens-before
操作 B,那么操作 A 的结果对于操作 B 是可见的,且操作 A 的执行顺序在操作 B 之前。它为开发者提供了可见性保证,无需关心底层的重排序。
1. 阻塞(Blocking)与非阻塞(Non-blocking)的区别是什么?
阻塞:当一个线程执行一个操作,如果该操作的资源没有准备好,它会挂起自身,让出 CPU 资源,直到资源就绪。例如,当线程试图获取一个被其他线程持有的
synchronized
锁时,它会进入阻塞状态。非阻塞:当一个线程执行操作时,如果资源没有准备好,它会立即返回失败,而不会挂起。例如,
CAS
(Compare-and-Swap)操作就是非阻塞的,它会不断重试直到成功。
2. 解释一下 AQS(AbstractQueuedSynchronizer)框架。
AQS 是一个同步器框架,它是 java.util.concurrent
包中许多同步工具(如 ReentrantLock
, Semaphore
, CountDownLatch
等)的底层核心。
核心思想:AQS 维护了一个共享资源状态(state),以及一个FIFO 的等待队列。当线程尝试获取资源失败时,它会被封装成一个节点并进入等待队列。当持有资源的线程释放资源时,它会唤醒等待队列中的下一个线程。
作用:它为同步器提供了统一的模板,开发者只需要实现获取和释放资源的抽象方法,就可以构建自己的同步器,而无需关心复杂的线程排队、唤醒等底层细节。
3. Semaphore
(信号量)是什么?它有什么用?
Semaphore
是一个计数器。它可以控制同时访问特定资源的线程数量。
工作原理:
Semaphore
维护一个许可证(permits)计数器。acquire()
方法会消耗一个许可证。如果许可证数量为 0,线程就会被阻塞,直到有其他线程释放许可证。release()
方法会释放一个许可证,并可能唤醒等待的线程。
用途:常用于限制对某些共享资源的并发访问数,例如控制数据库连接池、线程池或并发下载数。
4. 什么是读写锁(ReentrantReadWriteLock
)?它有什么优势?
读写锁是一种更高级的锁,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。
优势:在读多写少的并发场景中,读写锁能够显著提高系统的吞吐量,因为它允许多个读操作并行执行。如果使用
ReentrantLock
,读操作也会相互阻塞,导致性能下降。
5. 什么是公平锁(Fair Lock)和非公平锁(Unfair Lock)?
公平锁:按照线程请求锁的先后顺序来获取锁。在锁释放时,等待时间最长的线程将优先获取锁。这保证了所有线程都能获得执行机会,但会带来性能上的开销。
非公平锁:当锁可用时,任何线程都可以尝试获取,不管队列中是否还有其他等待的线程。新来的线程可能会比队列中等待已久的线程先获得锁。它的性能通常比公平锁高。
ReentrantLock
默认是非公平锁。
6. ThreadLocal
是什么?它的工作原理是什么?
ThreadLocal
提供了一种线程本地存储的机制。它可以让每个线程都拥有一个独立的、互不影响的变量副本。
工作原理:
ThreadLocal
内部维护了一个ThreadLocalMap
,这个Map
的键是ThreadLocal
对象本身,值是线程私有的变量副本。当线程调用get()
方法时,它会从自己的ThreadLocalMap
中获取对应的变量。用途:常用于在多线程环境下,为每个线程存储独立的数据库连接、用户信息等,避免线程安全问题。
7. synchronized
和 ConcurrentHashMap
的区别?
这个问题是想考察你对悲观锁和乐观锁的理解。
synchronized
:是悲观锁,它通过锁住整个对象来保证线程安全。当一个线程访问时,其他所有线程都被阻塞。这在写操作频繁的场景下性能较差。ConcurrentHashMap
:采用分段锁或CAS
等乐观锁机制。它只锁住哈希表中的一部分(一个桶),从而允许多个线程同时对不同桶进行读写操作,大大提高了并发性能。
8. 什么是 Fork/Join
框架?它有什么特点?
Fork/Join
是一个用于并行执行任务的框架,是 java.util.concurrent
包的一部分。它基于“分而治之”的思想。
核心思想:
Fork(拆分):将一个大的任务递归地拆分为多个足够小的子任务。
Join(合并):等待所有子任务执行完毕,然后将它们的结果合并。
特点:它利用**工作窃取(work-stealing)**算法,让空闲的线程从其他线程的队列中“窃取”任务来执行,以充分利用 CPU 资源,提高效率。
9. 如何确保一个类是线程安全的?
要确保一个类是线程安全的,需要保证其状态在被多个线程访问时不会出现不一致。常见的方法有:
不可变(Immutable):将类设计为不可变类,所有字段都是
final
,且没有任何方法可以修改其状态。同步化:使用
synchronized
关键字或ReentrantLock
等锁机制来同步对共享字段的访问。使用原子类:使用
AtomicInteger
、AtomicLong
等原子类来确保对变量的修改是原子性的。使用线程安全集合:使用
ConcurrentHashMap
、CopyOnWriteArrayList
等线程安全的集合类。
10. String
、StringBuffer
和 StringBuilder
哪个是线程安全的?
String
:不可变,因此是线程安全的。一旦创建,其值不能被改变。StringBuffer
:是线程安全的。它的所有公共方法都使用了synchronized
关键字进行同步,适合在多线程环境下使用。StringBuilder
:非线程安全的。它没有同步机制,性能比StringBuffer
高,适合在单线程环境下使用。