一、基础概念
1.1 CPU核心数和线程数的关系
多核心:也指单芯片多处理器(Chip Multiprocessors,简称 CMP),由美国斯坦福大学提出,其思想是将大规模并行处理器中的 SMP(对称多处理器)集成到同一芯片内,各个处理器并行执行不同的进程。这种依靠多个 CPU 同时并行地运行程序是实现超高速计算的一个重要方向,称为并行处理。
多线程:Simultaneous Multithreading,简称 SMT。SMT 可通过复制处理器上的结构状态,让同一个处理器上的多个线程同步执行并共享处理器的执行资源,可最大限度地实现宽发射、乱序的超标量处理,提高处理器运算部件的利用率,缓和由于数据相关或 Cache 未命中带来的访问内存延时。
核心数、线程数:增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下它们是 1:1 对应关系。但 Intel 引入超线程技术后,使核心数与线程数形成 1:2 的关系。
1.2 CPU时间片轮转机制
我们平时在开发的时候,并没有感受到 CPU 核心数限制带来的影响,想启动线程就启动线程,哪怕是在单核 CPU 上,为什么?这是因为 CPU 提供了一种 CPU 时间片轮转机制。
时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称 RR 调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
如果在时间片结束时进程还在运行,则 CPU 将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则 CPU 当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,即被移到队列的末尾。
时间片轮转调度中唯一有趣的一点是时间片的长度。从一个进程切换到另一个进程是需要定时间的,包括保存和装入寄存器值及内存映像,更新各种表格和队列等。假如进程切换(context switch),需要 5ms,再假设时间片设为 20ms,则在做完 20ms 有用的工作之后,CPU 将花费 5ms 来进行进程切换。CPU 时间的 20% 被浪费在了管理开销上。
为了提高 CPU 效率,我们可以将时间片设为 5000ms,这时浪费的时间只有 0.1%。但考虑到在一个分时系统中,如果有 10 个交互用户几乎同时按下回车键,将发生什么情况?假设所有其他进程都用足他们的时间片的话,最后一个不幸的进程不得不等待 5s 才获得运行机会,多数用户无法忍受一条简短命令要 5s 才做出响应。
结论可以归结如下: 时间片设的太短会导致过多的进程切换,降低了 CPU 效率;而设的太长又可能引起对短的交互请求的响应变差。将时间片设为 100ms 通常是一个比较合理的折中。
1.3 进程和线程
进程是程序运行资源分配的最小单位。进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、磁盘等等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程之间是相互独立的。
线程是 CPU 调度的最小单位,且必须依赖于进程而存在。线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的、能独立运行的基本单位,线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
线程无处不在。任何一个程序都必须要创建线程,特别是 Java,不管任何程序都必须启动一个 main 函数的主线程;Java Web 开发里面的定时任务、定时器、JSP 和 Servlet、异步消息处理机制、远程访问接口等,任何一个监听事件,onClick 的触发事件等都离不开线程和并发的知识。
1.4 并行和并发
如果有条高速公路 A 上面并排有 8 条车道,那么最大的并行车辆就是 8 辆,此条高速公路 A 同时并排行走的车辆小于等于 8 辆的时候,车辆就可以并行运行。CPU 也是这个原理,一个 CPU 相当于一个高速公路 A,核心数或者线程数就相当于并排可以通行的车道;而多个 CPU 就相当于并排有多条高速公路,而每个高速公路并排有多个车道。
当谈论并发的时候一定要加个单位时间,也就是说单位时间内并发量是多少。离开了单位时间其实是没有意义的。
总结如下:
- 并发:指应用能够交替执行不同的任务,比如单 CPU 核心下执行多线程并非是同时执行多个任务,如果开启两个线程执行,其实就是在以我们几乎毫无察觉到的速度不断去切换这两个任务,以达到”同时执行效果”。
- 并行:指应用能够同时执行不同的任务,比如,吃饭的时候可以边吃饭边打电话看电视,这几件事都可以同时进行。
- 两者区别:一个是交替执行,一个是同时执行。
二、Java内存模型(JMM)
2.1 happens-before 原则
JMM 定义了 happens-before 关系,是多线程编程的基石:
// 1. 程序次序规则:同一个线程内,按照代码顺序执行 |
2.2 volatile 的底层屏障
volatile 在 JVM 层面翻译为内存屏障指令,确保可见性和有序性:
// volatile 写 |
三、synchronized 的锁升级过程
HotSpot JVM 中的 synchronized 经过了多代优化,实现了锁的逐步升级:
无锁状态 (01) |
// 对象头的 Mark Word 变化(64位 JVM): |
3.1 synchronized 的底层实现
// 同步代码块: |
四、高并发编程的意义
由于多核多线程的 CPU 的诞生,多线程、高并发的编程越来越重要,多线程可以给程序性能处理带来质的提升。
4.1 多线程优势
充分利用 CPU 的资源:程序的基本调度单元是线程,并且一个线程也只能在一个 CPU 的一个核的一个线程上跑。如果你是个 i3 的 CPU 的话,最差也是双核心 4 线程的运算能力,如果是一个线程的程序的话,那是要浪费 3/4 的 CPU 性能。如果设计一个多线程的程序的话,那它就可以同时在多个 CPU 的多个核的多个线程上跑,可以充分地利用 CPU。
加快响应用户的时间:比如我们经常用的迅雷下载,都喜欢多开几个线程去下载。在做程序开发的时候更应该如此,特别是做互联网项目,网页的响应时间若提升 1s,如果流量大的话,就能增加不少转换量。
可以使代码模块化、异步化、简单化:例如我们在做 Android 程序开发的时候,主线程的 UI 展示部分是一块主代码程序部分,但是 UI 上的按钮响应事件的处理程序就可以做个单独的模块程序拿出来。这样既增加了异步的操作,又使程序模块化、清晰化和简单化。
4.2 多线程开发注意事项
线程之间的安全性:在同一个进程里面的多线程是资源共享的,也就是都可以访问同一个内存地址当中的一个变量。若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
线程之间的死锁:为了解决线程之间的安全性引入了 Java 的锁机制,而一不小心就会产生 Java 线程死锁的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。
过多线程导致耗尽服务器资源从而引起死机当机问题:线程数太多有可能造成系统创建大量线程而导致消耗完系统内存以及 CPU 的”过渡切换”,造成系统的死机。可以使用资源池来解决,如数据库连接池。
五、AQS(AbstractQueuedSynchronizer)深度剖析
AQS 是 Java 并发包的核心框架,ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 等都基于 AQS 构建。
// AQS 核心数据结构 |
5.1 ReentrantLock 的公平锁与非公平锁
// 非公平锁(默认):新来的线程可能直接抢到锁,跳过排队的线程 |
六、ThreadPoolExecutor 参数详解
// ThreadPoolExecutor 完整构造 |
七、ForkJoinPool 和工作窃取
// ForkJoinPool 适用于递归分解的任务(分治算法) |
八、CompletableFuture:异步编程利器
// CompletableFuture 链式异步编程 |
九、Android 特有的线程机制
9.1 Looper / Handler / MessageQueue
// Android 消息机制的核心是事件驱动模型 |
9.2 HandlerThread
// HandlerThread 是一个自带 Looper 的后台线程 |
十、认识Java线程
10.1 Java里的程序天生就是多线程的
一个 Java 程序从 main() 方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上,Java 程序天生就是多线程程序,因为执行 Main() 方法的是一个名称为 main 的线程。同时还有以下线程在运行:
- main – main 线程,用户程序入口
- Reference Handler – 清除 Reference 线程
- Finalizer – 调用对象的 finalizer 方法的线程
- Signal Dispatcher – 分发处理发送给 JVM 信号的线程
- Attach Listener – 内存 dump,线程 dump,类信息统计,获取系统属性等
- Monitor Ctrl-Break – 监控 Ctrl-Break 中断信号
10.2 线程的启动与中止
启动线程的方式有(但其实真正意义上只有前面两种):
X extends Thread,然后X.runX implements Runnable;然后交给 Thread 运行X implements Callable;然后交给 FutureTask 运行
Callable、Future 和 FutureTask:
- Runnable 是一个接口,run() 方法返回值为 void 类型,所以在执行完任务之后无法返回任何结果。
- Callable 是泛型接口,call() 方法返回 V 类型。
- Future 对于具体的 Runnable 或 Callable 任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过 get 方法获取执行结果,该方法会阻塞直到任务返回结果。
- FutureTask 实现了 RunnableFuture 接口,RunnableFuture 继承了 Runnable 和 Future 接口,所以它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。
手动中止:
暂停、恢复和停止操作对应在线程 Thread 的 API 就是 suspend()、resume() 和 stop()。但是这些 API 是过期的,不建议使用的。以 suspend() 方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样非常容易引发死锁。
安全的中止则是其他线程通过调用某个线程的 interrupt() 方法对其进行中断操作,中断好比其他线程对该线程打了个招呼,”你要中断了”,但这并不代表线程会立即停止自己的工作,因为 Java 里的线程是协作式的,不是抢占式的。线程通过检查自身中断标志位是否被置为 true 来进行响应。
如果一个线程处于阻塞状态(通过调用 thread.sleep、thread.join、thread.wait …),则在线程检查到自己的中断标识位为 true 时,会在这些阻塞方法调用处抛 InterruptedException 异常,并且在抛出异常后会立即将线程的中断标识位清除。
10.3 线程的状态
Java 中线程的状态分为 6 种:
- **初始(NEW)**:新创建一个线程对象,但还没有调用 start() 方法。
- **运行(RUNNABLE)**:Java 线程中将就绪(ready)和运行中(running)两种状态统称为”运行”。线程对象创建后,其他线程调用了该对象的 start() 方法,该状态的线程位于可运行线程池中,等待被线程调度选中。
- **阻塞(BLOCKED)**:表示线程阻塞于锁(等待获取 monitor 锁)。
- **等待(WAITING)**:进入该状态的线程需要等待其他线程做出一些动作(通知或中断),如 Object.wait()、Thread.join()、LockSupport.park()。
- **超时等待(TIMED_WAITING)**:该状态不同于 WAITING,它可以在指定的时间后自行返回线程队列,如 Thread.sleep()、Object.wait(timeout)、LockSupport.parkNanos()。
- **终止(TERMINATED)**:表示该线程已经执行完毕。
十一、死锁
11.1 死锁概念
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成一种阻塞线程,若无外力作用,它们都将无法推进下去,此时称系统处于死锁状态或者系统产生了死锁。
死锁的发生必须具备以下四个必要条件:
- 互斥条件:指进程对所分配到的资源进行排他性使用,即在某一段时间内某资源只由一个进程占用。
- 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有。
- 不剥夺条件:指进程已获得资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
- 环路等待条件:指在发生死锁时,必然存在一个进程 - 资源的环形链。
11.2 死锁的预防与解决
打破四个必要条件之一,就可以有效预防死锁:
- 打破互斥条件:改造独占性资源为虚拟资源
- 打破不可抢占条件:当无法满足时退出原占有的资源
- 打破占有且申请条件:采用资源预先分配策略
- 打破循环等待条件:实现资源有序分配策略
避免死锁的常见算法:有序资源分配法、银行家算法。
解决方法:
- 内部通过顺序比较,确定拿锁的顺序(所有线程按固定顺序获取锁)
- 采用尝试拿锁的机制(
tryLock(timeout))
11.3 死锁的危害
- 线程不工作了,但是整个程序还是活着的
- 没有任何的异常信息可供检查
- 一旦程序发生了死锁,是没有任何办法恢复的,除非重启应用程序
十二、面试常问题目
Q1: 什么是 happens-before 原则?volatile 是如何保证可见性和有序性的?
happens-before 是 JMM 定义的偏序关系,用于判断数据是否存在竞争。如果操作 A happens-before 操作 B,那么 A 的结果对 B 可见,且 A 的执行顺序在 B 之前。volatile 通过内存屏障保证:(1) 可见性——对 volatile 变量的修改会被立即刷新到主内存,并且使其他 CPU 缓存行失效;(2) 有序性——volatile 写之前和之后的指令不会被重排序。volatile 不保证原子性(如 i++ 仍然是线程不安全的)。适用于状态标志、DCL 单例、独立观察等场景。
Q2: synchronized 的锁升级过程是怎样的?为什么需要这么设计?
锁升级过程:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。偏向锁将对象头标记为偏向特定线程,该线程再次进入同步块时无需 CAS 操作——适用于大多数时间只有一个线程访问的场景。轻量级锁通过 CAS 自旋获取,避免操作系统层面的阻塞——适用于线程交替执行的场景。重量级锁使用 OS mutex,线程阻塞/唤醒——适用于竞争激烈的场景。这种逐步升级的设计是基于”大多数情况下锁的竞争不激烈”的经验假设,避免了不必要的重量级操作。
Q3: ThreadPoolExecutor 的执行流程是什么?核心线程数和最大线程数有什么区别?
执行流程:(1) 线程数 < corePoolSize → 创建新线程;(2) 线程数 >= corePoolSize → 任务入队;(3) 队列满 → 创建新线程(不超过 maximumPoolSize);(4) 达到 maximumPoolSize → 执行拒绝策略。核心线程默认不会回收(除非 allowCoreThreadTimeOut=true),空闲的非核心线程在 keepAliveTime 后回收。核心线程数应该根据 CPU 核数和任务类型设定:CPU 密集型约为核心数+1,IO 密集型约为核心数*2。
Q4: Handler 的内存泄漏是怎么产生的?如何避免?
Handler 作为 Activity 的非静态内部类(或匿名内部类),隐式持有外部 Activity 的引用。当发送延迟消息(如 postDelayed 10 分钟),消息在 MessageQueue 中持有 Handler 引用,形成引用链:MessageQueue → Message → Handler → Activity。在消息处理前 Activity 被销毁,由于这条引用链,GC 无法回收 Activity。解决方案:(1) 使用静态内部类 + WeakReference
Q5: CompletableFuture 和传统的 Future 相比有什么优势?
Future 只能通过 get() 阻塞等待结果或 isDone() 轮询,不支持回调、链式处理和组合。CompletableFuture 实现了 Future 和 CompletionStage 接口,支持:(1) 链式处理:thenApply / thenAccept / thenRun 等;(2) 组合操作:thenCombine / thenCompose / allOf / anyOf;(3) 异常处理:exceptionally / handle;(4) 异步回调:thenApplyAsync 等(使用 ForkJoinPool.commonPool());(5) 手动完成:complete / completeExceptionally。这使得异步代码可以以声明式方式表达,避免了回调地狱。
参考源码路径:
- Android Looper:
frameworks/base/core/java/android/os/Looper.java - Android Handler:
frameworks/base/core/java/android/os/Handler.java - AQS 源码:
java.util.concurrent.locks.AbstractQueuedSynchronizer - ThreadPoolExecutor 源码:
java.util.concurrent.ThreadPoolExecutor - ART Monitor:
art/runtime/monitor.cc







