一、线程的基础知识
- 线程和进程的区别
- 进程:正在运行程序的实例,用于加载指令、管理内存和 IO。
- 线程:进程内的执行单元,一个进程可包含多个线程。
- 区别:进程是资源分配的最小单位,线程是调度的最小单位;进程间内存独立,线程可共享进程内存;线程上下文切换成本通常低于进程。
- 并行和并发的区别
- 并行:同一时间多个核同时执行多个线程。
- 并发:同一时间多个线程轮流使用一个或多个 CPU。
- 创建线程的四种方式
- 继承
Thread
类。 - 实现
Runnable
接口。 - 实现
Callable
接口配合FutureTask
。 - 使用线程池创建线程。
- 继承
Runnable
和Callable
的区别Runnable
的run
方法无返回值,Callable
的call
方法有返回值且可抛出异常。Callable
配合Future
和FutureTask
可获取异步执行结果。
- 线程的
run()
和start()
的区别start()
用于启动线程,通过该线程调用run
方法执行逻辑代码,且只能调用一次。run()
方法封装了线程要执行的代码,可被多次调用。
- 线程的状态及状态转换
- 状态包括:新建、可运行、阻塞、等待、有时限等待、终结。
- 状态转换条件多样,如获取锁、释放锁、调用
wait()
/notify()
等方法、超时等。
- 如何保证线程按顺序执行
- 可使用线程的
join()
方法,在一个线程中启动另一个线程,确保被调用join
的线程先执行完。
- 可使用线程的
notify()
和notifyAll()
的区别notifyAll()
唤醒所有等待的线程,notify()
只随机唤醒一个等待线程。
- 在 Java 中
wait
和sleep
方法的不同- 共同点:都可让线程进入阻塞状态,暂时放弃 CPU 使用权。
- 不同点:
- 方法归属不同,
sleep()
是Thread
的静态方法,wait()
是Object
的方法。 - 醒来时机不同,
sleep()
等待指定时间后自动醒来,wait()
需被notify
唤醒,否则一直等待。 - 锁特性不同,
wait()
调用需先获取对象锁,执行后释放锁;sleep()
在同步代码块中不释放锁。
- 方法归属不同,
二、线程中并发锁
synchronized
关键字的底层原理- 通过
monitor
实现互斥,同一时刻至多只有一个线程能持有对象锁。 monitor
是 JVM 级别的对象,由 C++ 实现,与对象关联。monitor
内部有Owner
(持有锁的线程)、EntryList
(阻塞线程列表)、WaitSet
(等待线程列表)三个属性。
- 通过
synchronized
关键字的底层原理 - 进阶synchronized
属于重量级锁,涉及用户态和内核态切换、进程上下文切换,成本高、性能低。- JDK 1.6 引入偏向锁、轻量级锁优化:
- 偏向锁:适用于长时间只有一个线程使用锁的场景,第一次获取锁通过 CAS 设置线程 ID,后续判断线程 ID 即可,无需 CAS。
- 轻量级锁:适用于线程加锁时间错开且竞争不激烈的场景,通过 CAS 修改对象头的锁标志实现加锁,解锁时若失败则膨胀为重量级锁。
- 对 JMM(Java 内存模型)的理解
- 所有共享变量存储于主内存,每个线程有自己的工作内存。
- 线程对变量的操作必须在工作内存中完成,不同线程间变量值传递通过主内存。
- CAS(Compare And Swap)
- 一种乐观锁思想,体现无锁情况下保证线程操作共享数据的原子性。
- 包含当前内存值
V
、旧的预期值A
、即将更新的值B
,当且仅当A
和V
相同时,将V
修改为B
并返回true
,否则返回false
。 - CAS 底层依赖
Unsafe
类调用操作系统底层的 CAS 指令实现,常见于ReentrantLock
和AtomicXXX
类等。
- 对
volatile
的理解- 保证线程间的可见性,强制将修改的值立即写入主存。
- 禁止进行指令重排序,通过添加内存屏障实现。
- AQS(AbstractQueuedSynchronizer)
- 阻塞式锁和同步器工具的框架,是构建锁或其他同步组件的基础。
- 维护一个用
volatile
修饰的state
属性表示资源状态,提供基于 FIFO 的等待队列和条件变量实现等待、唤醒机制。 - 常见实现类有
ReentrantLock
、Semaphore
、CountDownLatch
等。
ReentrantLock
的实现原理- 利用 CAS + AQS 队列实现,可重入。
- 支持公平锁和非公平锁,默认非公平锁,公平锁效率通常低于非公平锁。
- 构造方法可接受公平参数,通过修改
state
状态和控制线程在队列中的等待、唤醒实现加锁和解锁。
synchronized
和Lock
的区别- 语法层面:
synchronized
是关键字,用 C++ 实现;Lock
是接口,用 Java 实现,且需要手动调用unlock
方法释放锁。 - 功能层面:都属于悲观锁,具备基本互斥、同步、锁重入功能,
Lock
还提供获取等待状态、公平锁、可打断、可超时、多条件变量等功能。 - 性能层面:无竞争时
synchronized
做了优化性能较好,竞争激烈时Lock
通常性能更佳。
- 语法层面:
- 死锁产生的条件及诊断方法
- 条件:一个线程需要同时获取多把锁,且多个线程相互等待对方持有的锁。
- 诊断方法:使用
jps
查看运行的线程,jstack
查看线程运行情况定位死锁问题,还可使用可视化工具如jconsole
、VisualVM
。
三、线程池
- 线程池的核心参数及工作流程
- 核心参数:
corePoolSize
:核心线程数目。maximumPoolSize
:最大线程数目(核心线程 + 救急线程的最大数目)。keepAliveTime
:救急线程的生存时间。unit
:救急线程生存时间的单位。workQueue
:任务队列,当核心线程满时,新任务加入此队列排队。threadFactory
:线程工厂,可定制线程对象的创建。handler
:拒绝策略,当所有线程都繁忙且任务队列满时触发。
- 工作流程:提交任务时,先判断核心线程数是否已满,未满则创建核心线程执行任务;核心线程满后判断任务队列是否已满,未满则将任务加入队列;队列满后判断线程数是否小于最大线程数,满足则创建救急线程执行任务;否则执行拒绝策略。
- 核心参数:
- 线程池中常见的阻塞队列
ArrayBlockingQueue
:基于数组结构的有界阻塞队列,FIFO,底层是数组,一把锁,提前初始化Node
数组。LinkedBlockingQueue
:基于链表结构的有界阻塞队列,FIFO,默认无界,支持有界,底层是链表,两把锁(头尾),创建节点时添加数据。DelayedWorkQueue
:优先级队列,保证每次出队的任务是执行时间最靠前的。SynchronousQueue
:不存储元素的阻塞队列,每个插入操作必须等待一个移出操作。
- 如何确定核心线程数
- IO 密集型任务:推荐核心线程数大小设置为
2N + 1
(N
为计算机的 CPU 核数)。 - CPU 密集型任务:推荐核心线程数大小设置为
N + 1
。
- IO 密集型任务:推荐核心线程数大小设置为
- 线程池的种类
newFixedThreadPool
:固定线程数的线程池,核心线程数与最大线程数相同,无救急线程,阻塞队列是无界的LinkedBlockingQueue
。newSingleThreadExecutor
:单线程化的线程池,只用唯一工作线程执行任务,保证任务按顺序执行,阻塞队列是无界的LinkedBlockingQueue
。newCachedThreadPool
:可缓存线程池,核心线程数为 0,最大线程数为Integer.MAX_VALUE
,阻塞队列为SynchronousQueue
。newScheduledThreadPool
:具有延迟和周期执行功能的线程池,核心线程数可指定,最大线程数为Integer.MAX_VALUE
,阻塞队列为DelayedWorkQueue
。
- 为什么不建议用
Executors
创建线程池FixedThreadPool
和SingleThreadPool
允许的请求队列长度为Integer.MAX_VALUE
,可能堆积大量请求导致 OOM。CachedThreadPool
允许的创建线程数量为Integer.MAX_VALUE
,可能创建大量线程导致 OOM。
四、线程使用场景问题
- CountDownLatch 的使用场景
- 用于线程同步协作,一个或多个线程等待其他多个线程完成某件事情之后才能执行。
- 构造参数初始化等待计数值,通过
await()
等待计数归零,countDown()
让计数减一。
- Future 的使用场景
- 在需要获取异步任务执行结果时使用,配合线程池可提升性能。
- Semaphore 的使用场景
- 通过限制执行的线程数量达到限流效果,当线程执行时先获取许可,执行完成后释放许可。
五、其他
- 对
ThreadLocal
的理解- 为每个线程分配独立的线程副本,解决变量并发访问冲突问题,同时实现线程内的资源共享。
- 主要方法有
set()
(设置值)、get()
(获取值)、remove()
(清除值)。 - 内部有一个
ThreadLocalMap
类型的成员变量,以ThreadLocal
自己作为 key,存储线程的资源对象。
ThreadLocal
的内存泄露问题ThreadLocalMap
中的Entry
继承自WeakReference
,key
为弱引用的ThreadLocal
实例,value
为线程变量副本。- 当
ThreadLocal
对象没有被外部强引用引用时,可能会被 GC 回收,但value
不会,导致内存泄露。 - 建议在使用完
ThreadLocal
后手动调用remove()
方法释放资源。