这是我参与「第三届青训营 -后端场」笔记创作活动的第 6 篇笔记
多线程三要素
多线程三要素与编程语言无关,像 C、C++ 也一样有这三要素:
- 可见性:JMM 里每个线程都有自己的工作内存,工作内存(也叫本地内存)存放了共享变量(在 JMM 主内存)的一个副本,一个线程对共享变量的更新不会立即对其他线程可见
- 有序性:指令重排是指 CPU 执行指令的顺序跟代码语义的顺序不一致,指令重排的存在与编译器和 CPU 有关,与是否多线程无关,在多线程环境下指令重排会影响多线程安全,所以多线程保证有序性是必要的
- 原子性:保证没有其他线程访问正在原子操作中的变量
Java 如何保证多线程三要素:
- 可见性、有序性:volatile 关键字、使用变量句柄(VarHandle)的 CPU 内存屏障(Java 9 才开始有)
- 原子性:使用 synchronized 或 锁
JMM 保证了 happens-before 原则(共 8 条),A Happens-Before B 则保证 B 操作能够看到 A 操作的结果:
- 程序顺序规则:一个线程中的每一个操作,happens-before 于该线程中的任意后续操作。
- 监视器规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
- volatile 规则:对一个 volatile 变量的写,happens-before 于任意后续对一个 volatile 变量的读。
- 传递性:若果 A happens-before B,B happens-before C,那么 A happens-before C。
- 线程启动规则:Thread 对象的 start()方法,happens-before 于这个线程的任意后续操作。
- 线程终止规则:线程中的任意操作,happens-before 于该线程的终止监测。我们可以通过 Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
- 线程中断操作:对线程 interrupt()方法的调用,happens-before 于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted()方法检测到线程是否有中断发生。
- 对象终结规则:一个对象的初始化完成,happens-before 于这个对象的 finalize()方法的开始。
JVM 有四种内存屏障可以用来禁止屏障前后的指令发生重排:
- LoadLoad
- StoreStore
- LoadStore
- StoreLoad
JMM 对 Java 语义的比较重要的两个扩展是:
- 对
volatile语义的扩展保证了volatile变量在一些情况下不会重排序,volatile的 64 位变量double和long的读取和赋值操作都是原子的; - 对
final语义的扩展保证一个对象的构建方法结束前,所有 final 成员变量都必须完成初始化(前提是没有 this 引用溢出);
volatile 语义中的内存屏障:
- 在每个
volatile写操作前插入 StoreStore 屏障,在写操作后插入 StoreLoad 屏障; - 在每个
volatile读操作前插入 LoadLoad 屏障,在读操作后插入 LoadStore 屏障; volatile的内存屏障策略非常严格保守,保证了可见性。
final 语义中的内存屏障:
- 新建对象过程中,构造方法中对
final域的初始化写入(StoreStore 屏障)和这个对象赋值给其他引用变量,这两个操作不能重排序; - 初次读包含
final域的对象引用和读取这个final域(LoadLoad 屏障),这两个操作不能重排序; - Intel 64/IA-32 架构下写操作之间不会发生重排序 StoreStore 会被省略,这种架构下也不会对逻辑上有先后依赖关系的操作进行重排序,所以 LoadLoad 也会变省略。
进程、线程、协程
进程和线程是操作系统层面的概念,进程是操作系统分配资源的最小单位,线程是程序执行的最小单位,协程是一个线程就能执行异步任务。 JVM 线程跟操作系统线程是 1:1 的(Java 1.1 有绿色线程不是 1:1 操作系统线程的,但已经是过去式了)。 Java 目前不支持协程,异步需要通过多线程来实现,比较重。协程是一个线程就能执行异步任务,更轻量。 Java 的协程在开发中:openjdk.java.net/projects/lo… 。
线程上下文切换
当 CPU 从执行一个线程切换到执行另一个线程时,CPU 需要保存当前线程的本地数据,程序指针等状态,并加载下一个要执行的线程的本地数据,程序指针等,这个开关被称为『上下文切换』。 一般减少上下文切换的方法有:无锁并发编程、CAS 算法、使用协程等。
Java 线程的 6 种状态(Thread.State)
- NEW: 新建
- RUNNABLE: 对应操作系统线程的就绪和运行
- BLOCKED: 被
synchronized阻塞(Object#wait是 WAITING 状态,在被Object#notify或Object#notifyAll之后才进入 BLOCKED 状态) - WAITING: 被
Object#wait、Thread#join、LockSupport#park没有 timeout 的方法阻塞 - TIMED_WAITING: 被
Thread#sleep、Object#wait、Thread#join、LockSupport#park有 timeout 的方法阻塞 - TERMINATED: 线程正常或非正常地执行结束
Java 创建多线程
Java 面试有时会遇到这个问题,问你讲几种创建线程的方式,这是很经典的八股文了,如果我是面试官我绝对不问这题。这道题你需要回答的是这 4 种(其他的像通过反射、ThreadFactory 等方式创建不是本题考查内容):
- 重写
Thread的run方法 - 创建
Thread时传入Runnable - 创建
Thread时传入由FutureTask包裹的Callable - 使用线程池的
execute或submit传Runnable或Callable
public class Main {
public static void main(String[] args) {
// override
new Thread() {
@Override
public void run() {
System.out.println("override");
}
}.start();
// runnable
Runnable runnable = () -> {
System.out.println("runnable");
};
new Thread(runnable).start();
// callable
FutureTask<Void> task = new FutureTask<>(() -> {
System.out.println("callable");
return null;
});
new Thread(task).start();
// thread pool
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
System.out.println("execute runnable/callable");
});
executor.submit(() -> {
System.out.println("submit runnable/callable");
});
executor.shutdown();
}
}
InterruptedException
调用 Thread#interrupt 方法可以使这个线程的 Runnable 里声明了 throws InterruptedException 的阻塞方法抛出 InterruptedException ,同时会将 interrupted 标记设置为 true 。Thread.currentThread().interrupt() 可以将当前线程中断。捕获 InterruptedException 和调用 Thread.interrupted() 都会清除 Thread 的 interrupted 标记。
在捕获 InterruptedException 后建议调用 Thread.currentThread().interrupt() 将 interrupted 标记恢复为 true :
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// restore the interrupted status
Thread.currentThread().interrupt();
}
当 Thread 的 Runnable 里没有调用声明了 throws InterruptedException 的阻塞方法时,Thread#interrupt 无法打断线程的执行,建议在 Thread 里的循环条件上加 !Thread.currentThread().isInterrupted(),使其在被中断时能结束循环:
try {
while (!Thread.currentThread().isInterrupted()) {
Thread.sleep(2000);
doSomething();
}
} catch (InterruptedException e) {
// restore the interrupted status
Thread.currentThread().interrupt();
}
Lock 接口有个 lockInterruptibly 方法就是设计来接受中断的,这个方法会抛 InterruptedException。
线程池 ThreadPoolExecutor
1. 7 个参数
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 线程空闲时间
TimeUnit unit, // 空闲时间的单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler) {} // 拒绝策略
-
corePoolSize<=maximumPoolSize -
使用 BlockingQueue 是因为 BlockingQueue 是有界的,能控制队列里的元素个数。
-
线程工厂可以给线程设置名称、优先度等属性。
-
拒绝策略可以是抛出异常或直接忽略等操作。
-
allowCoreThreadTimeOut方法可以开启限制 核心Worker的空闲时间。 -
线程池里允许存在的最大线程数是 min(
CAPACITY,maximumPoolSize)
2. 线程池 5 种状态
ThreadPoolExecutor 使用一个 int 存储 5 个状态和 Worker 数,高 3 位(bit)存储线程池状态,剩下的存储 Worker 数。
- RUNNING:线程池创建是默认就是 RUNNING 状态,线程池正在运行,接受新任务
- SHUTDOWN:调用了
shutdown(),不接受新任务,但会执行完已有任务 - STOP:调用了
shutdownNow(),不接受新任务,并且会中断正在执行的任务 - TIDYING:SHUTDOWN 结束(队列和 Worker 在执行的任务都完成了)或 STOP 结束(Worker 都中断了),也就是线程都结束了,状态改为 TIDYING,接着调用
protected默认空实现的方法terminated()让子类能监听到线程池的结束 - TERMINATED:执行完
terminated()方法后状态改为 TERMINATED ,线程池完全结束
4. 任务执行流程
Worker 继承了 AQS 包裹了 Thread 对象,也就是一个 Worker 一个线程。
Worker数 = 核心Worker数 + 非核心Worker数
Worker 数未到达 corePoolSize 时直接创建核心 Worker ,到达 corePoolSize 时将任务入队,当对满时(workQueue.offer 返回 false)创建非核心 Worker ,如果 Worker 数已经到达 maximumPoolSize 。在没有任务后,非核心 Worker 会存活 keepAliveTime 所指定的时长后销毁。
核心 Worker 和 非核心 Worker 在执行完创建时拿到的任务后,会接着消费队列里的任务。