线程
基础
线程和进程
进程(Process) 机器内存中运行的应用程序,应用程序启动后系统会分配一块内存空间给这个进程,进程与进程间相互独立,即内存不共享。一个进程允许启动 N 个线程,例如 Windows 系统中一个后缀是.exe 程序就是一个进程。进程是系统资源分配的基本单位
线程(Thread) 进程中的某个执行流程,一个进程允许启动 N 个线程,例如 java 进程中可以运行 N 个线程。进程和线程的关系是一对多,线程与线程之间共享进程的内存。线程是调度器(即 CPU)调度的基本单位
区别
- 进程是资源分配的最小单位,线程是程序执行的最小单位
- 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此 CPU 切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多
- 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。但是如何处理好同步与互斥是编写多线程程序的难点
- 多进程程序更健壮,多线程程序只要一个线程死掉,整个进程虽然不会死掉,但是功能会受影响,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间
并发和并行
并发 当我们的计算机硬件资源只有一个 CPU 也就是处理器时,执行多个线程时实际上是将多个线程按时间段进行分割,每个时间段执行一个线程。比如上午 10 点 56 分执行线程一,10 点 58 分执行线程二,这样的执行方式就是并发
并行 并行就完全相反,它是计算机有两个或两个以上 CPU 时,执行多线程时,cpu1 执行线程 1、cpu2 执行线程 2,这样的执行就是并行的方式。
由上两个概念解析可知,并发是多个线程执行时没有时间上的重叠,而并行是可以有时间上的重叠
打个比方 你吃饭吃到一半,电话来了,你放下筷子接了电话,接完后继续拿起筷子吃饭,这就是并发 你吃饭吃到一半,电话来了,你边打电话边吃饭,这就是并行
创建方式
目前已知应该一共 4 种方式
继承 Thread 类重写 run 方法
public class ThreadExample {
public static void main(String[] args) {
new MyThread().start();
}
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("hello world!");
}
}
}
MyThread 类继承 Thread 类,重写 run 方法,然后调用 start 方法启动线程,当创建完 thread 对象后该线程并没有被启动执行.
调用了 start 方法后才是真正启动了线程。其实当调用 start 方法时,线程是处于就绪状态,就绪状态是表明该线程已获取了除 CPU 资源外的其它资源,等获取 CPU 资源后才会真正处于运行状态
run 方法执行完,线程就处于终止状态。
- 优点 run 方法内获取当前线程直接使用 this 就行了,无须使用 Thread.currentThread()方法
- 缺点 Java 不支持多继承,如果继承了 Thread 类,就不能再继承其它类,另外任务与代码没有分离,当多个线程执行一样的任务时候需要多份任务代码,而 Runable 没有这个限制。而且任务没有返回值
注意: 线程执行顺序与创建顺序无关
实现 Runnable 接口的 run 方法
public class ThreadExample {
public static void main(String[] args) {
RunnableTask task = new RunnableTask();
new Thread(task).start();
new Thread(task).start();
}
public static class RunnableTask implements Runnable {
@Override
public void run() {
System.out.println("hello world!");
}
}
}
两个线程公用同一个 task 代码逻辑。RunableTask 还可以添加参数进行任务区分
- 优点 RunableTask 可继承其他类
- 缺点 任务没有返回值
实现 Callable 接口使用 FutureTask
public class ThreadExample {
public static void main(String[] args) throws InterruptedException {
// 创建异步任务
FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
new Thread(futureTask).start();
try {
// 等待任务执行完毕,并返回结果
System.out.println(futureTask.get());
} catch (ExecutionException e) {
e.printStackTrace();
}
}
public static class CallerTask implements Callable<String> {
@Override
public String call() {
return "hello world!";
}
}
}
- 优点 有返回值
- 缺点 futureTask.get()是一个阻塞方法。要想不阻塞,只能在 futureTask.get()代码之前,加上下列代码
while(!futureTask.isDone()){
//检查是否完成,如果没完成,那可以让主线程去做其他操作,不会被阻塞
}
使用线程池
public class ThreadExample {
public static void main(String[] args) {
ThreadPoolTask threadPoolTask = new ThreadPoolTask();
ExecutorService fixedThreadPool =
new ThreadPoolExecutor(
5,
10,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(1024));
for (int i = 0; i < 5; i++) {
fixedThreadPool.execute(threadPoolTask);
}
fixedThreadPool.shutdown();
}
public static class ThreadPoolTask implements Runnable {
@Override
public void run() {
System.out.println("线程名:" + Thread.currentThread().getName() + "\nhello world!");
}
}
}
对于线程池的说明可见线程池篇,相关的注意措施都已在那篇说明
生命周期
线程在代码中执行时,会根据具体操作步骤时刻变化自己的状态。整个线程执行过程就是它的生命周期
状态
线程状态一共有 7 种状态,但是分为 6 大类,详见 Thread 类中的内部枚举类 State
源码(java11)如下:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
如下列表对这些状态进行说明
| 线程状态分类 | 简单说明 |
|---|---|
| NEW | 前述线程创建方式中已说明,线程创建完就处于此状态,只有调用 start 方法才会到下一状态 |
| RUNNABLE | 运行状态其实包含 running 和 ready 两个状态 |
| BLOCKED | 线程被锁(之后会详细说明锁相关知识)阻塞 |
| WAITING | 线程处于此状态时候,还需要其他状态对它进行通知或中断等操作,否则会一直处于这个等待状态 |
| TIME_WAITING | 和上列不同之处在于它有时间限制,当在指定时间过后还没等到其他线程通知或中断等操作,就不等了,直接返回 |
| TERMINATED | 线程执行结束后状态 |
如果有过线上调优经验,在导出的 dump 文件里会发现每个线程都有这些线程状态说明,从而方便我们定位线上系统性能问题发生的具体原因
为啥 running 和 ready 两个状态可以合并成 runnable 状态?
之前线程创建方式中已说明: 就绪状态(ready)是表明该线程已获取了除 CPU 资源外的其它资源,获取 CPU 资源后才会真正处于运行状态(running)。在 JVM 层面,它隐藏了这两个状态,只能看到 runnable 状态(其实部分 wating 状态也被隐藏了)
示意图如下

这样做的原因是因为
- 操作系统的线程状态是围绕着 cpu 这一核心与 JVM 侧重点不同
- JVM 线程状态的改变通常只与自身显式引入机制有关,就是说线程状态的改变,通常是自身机制引发 比如后面要说的 synchronize 让线程进入 BLOCKED 状态,sleep,wait 等方法则让它进入 WATING 状态
生命周期过程
见下图,该图参考了参考书单 2 的 4.1.4 小节(个人认为状态描述正确,但是触发状态变化的方法持保留意见)

- 创建好线程,它处于 NEW 状态
- 调用 start 方法后,开始运行,进入 READY 状态。当获取 cpu 资源后,处于 RUNNING 状态。统称为 RUNNABLE 状态
- 执行 wait 方法后,进入 WAITING 转态。前述列表中已说,其他线程通知或中断操作后,它才能返回到 RUNNABLE 状态
- 如果执行 wait(long)或 sleep(long)等带超时时间参数的方法后,线程进入 TIMED_WAITING 状态。超时时间一过就不等了,直接返回到 RUNNABLE 状态
- 线程调用同步方法,在获取不到锁时,会进入到 BLOCKED 状态(锁知识会在之后详细说明)
- 执行 run 或 interrupt 方法后,就进入 TERMINATED 状态。整个线程执行完毕,生命周期终结
注意: 如图所示,状态变化不一定就执行上述这些方法,还可以执行图中展示的其他方法
触发线程状态变化的方法说明
Thread 类方法
- Thread.start() 其实已经写了很多遍了,通过这个方法线程开始进入 RUNNABLE 状态
- Thread.yield() 线程获取的 cpu 资源用完之后,调用此方法从 RUNNING 状态切换为 READY 状态。所以上图中也称为系统调度
- Thread.sleep(long) static 方法,线程类和线程实例调用,效果一样
- Thread.join()和 Thread.join(long) 让父线程等待子线程结束之后才继续运行。其实是调用 join 方法的线程(父线程)进入 TIMED_WAITING 状态,等待 join 方法所属的线程(子线程)结束后再继续运行
- Thread.run() 执行操作,完毕后线程操作结束
- Thread.interrupt() 将线程的中断标志位设置为 true,并没有中断线程,它只是向线程发送一个中断信号
- Thread.isInterrupted() 判断线程是否中断,不改变标志位
- Thread.interrupted() 判断当前线程是否中断,如果是 true,表明线程已中断,返回 true,返回前将标志位设置为 false
其他方法
- Thread.currentThread() 获取当前线程
- Thread.isAlive() 某个线程实例是否存活
Object 类方法
- Object.wait()和 Object.wait(long) 把持有对象线程的控制权交出去,然后处于等待状态,wait()是转换为 WAITING 状态,wait(long)是转换为 TIMED_WAITING 状态
- Object.notify() 通知某个正在等待对象控制权的线程可以继续运行
- Object.nofifyAll() 通知所有等待这个对象控制权的线程继续运行,如果有多个正在等待该对象控制权的线程时,具体唤醒哪个线程由操作系统进行调度
为什么 wait 和 notify、notifyAll 是 Object 类的方法而不是 Thread 类?
wait 和 notify、notifyAll 这几个方法都会涉及到并发编程中的锁机制。因为锁是每个对象都具备的特性,因此操作锁的方法必然和对象有关,Object 类是 java 里所有对象基类,因此这些方法是 Object 类方法,而不是 Thread 类特有的。
说穿了,它们都和对象的监视器锁有关。也就是前述的对象控制权。它们几个在被执行时,都必须保证当前运行的线程取得了对象控制权(监视器锁)。wait 一般是挂起自己,释放对象的监视器锁,让其他线程可以获得,还把自己加入等待对象控制权的线程中,直到其他线程调用了 Object 的 notify、notifyAll 方法,自身才会被唤醒(如果是调用 wait(long)方法就是加了等待时间),而 notify、notifyAll 是在释放监视器锁同时,唤醒正在等待对象控制权的线程。如果调用的是 notify,则哪个线程取得对象控制权是随机不确定的,如果是 notifyAll,则是所有等待对象控制权的线程一起被唤醒,哪个线程取得对象控制权要看操作系统的调度
后续章节会对 java 的锁机制进行进一步的说明
LockSupport 类方法
LockSupport 类是一个线程阻塞工具类,所有方法都是静态方法,可以让线程在任意位置阻塞,当然阻塞之后肯定会有唤醒
- LockSupport.park() 使当前线程挂起,进入线程 WAITING 状态,且操作系统不再会对它进行调度,直到其他线程调用了 unpark 方法 park 不同于 Thread.yield(), yield 只是告诉操作系统可以先让其他线程运行,但自己依然是可运行状态,而 park 会放弃调度资格,使线程进入 WAITING 状态
- LockSupport.unpark(Thread) 使参数传入的线程恢复成 RUNNABLE 状态,解除阻塞方法
- LockSupport.parkNanos(long nanos) 阻塞当前线程,最长不超过纳秒数,只是在 park 基础上增加了超时返回时间
- LockSupport.parkUntil(long deadline) 也是阻塞当前线程。参数是绝对时间,时间单位为毫秒,是从 1970-01-01 开始到现在某个时间点换算为毫秒后相减的值
相关疑问
Thread.sleep 和 Object.wait 的异同点
- sleep 不释放监视器锁,wait 释放
- 都可以暂停线程执行
- sleep 用于暂停执行线程,wait 用于线程之间通信和交互
- sleep 执行完,线程会被自动唤醒。wait 只能使用 Object.wait(long)使线程自动被唤醒。否则只能让其他线程调用同一个 Object 的 notify 和 notifyAll 方法唤醒
notify 和 notifyAll 的异同点
- notify 唤醒 1 个线程,notifyAll 唤醒所有线程
- notify 唤醒哪个线程随机,notifyAll 是所有线程参与获取监视器锁的竞争,竞争成功就执行,不成功就等锁下一次被释放,然后继续参与竞争
为啥要调用 Thread.start()开始执行 run 方法,不能直接调用 run 么?
线程启动后,是进入 ready 状态,虽然从 jvm 层面看是 runnable 状态(见前述)。但是它需要在运行前,获取到 CPU 时间片,做好线程运行的准备。这件事情是由 start 方法来执行,然后再自动执行 run 方法,进行真正的操作。直接执行 run 方法对于 Thread 来说只是一个普通方法,并不会在线程中执行,这样就不是多线程操作了
简而言之,start 方法会启动线程,并让线程进入 ready 状态,进行线程操作的准备工作。而 run 方法只是一个普通方法调用,还是在 main 主线程执行。另外多嘴一句: start 方法只能被调用一次,多次被调用会抛 IllegalThreadStateException 异常。run 可以被重复调用很多次
守护线程
又称 Daemon Thread,在 java 中线程可分为守护线程和应用线程。程序员用自己写的代码创建的线程就是应用线程
正如它的名字,守护线程是系统的守护者,在后台默默地完成一些系统性的服务,比如垃圾回收线程。而应用线程可认为是系统的工作线程,它会完成这个程序应该要完成的相关操作。如果应用线程全部结束,则意味着这个程序实际上已无事可做。守护线程要守护的对象已经不存在,那么整个应用程序就该结束。因此,当一个 java 应用只有守护线程时,Java 虚拟机就会自然退出
java 类的 main 方法创建的线程都是 main 线程的子线程。父线程是守护线程,则子线程也是守护线程。父线程是应用线程(非守护线程),则子线程就是应用线程
父线程在创建子线程,且启动子线程之前,可用 setDaemon(true)方法,将相应的线程设置为守护或应用线程(非守护线程)。如果没有手动设置某线程的优先级,那么该线程的优先级默认值和其父线程相同
父子线程的生命周期没有必然联系,任何一个线程先结束,都不会影响其父或子线程的生命周期
守护线程优先级较低,只是用来为系统中其他对象和线程提供服务的
上下文切换
每个线程都会被分配 CPU 时间片来执行自身的任务,当某个线程在执行完 CPU 时间片,切换到另外一个线程前会保存自身状态,以便下次再切换回来时候可以再加载自身状态,这种从保存到再加载过程就被称为一次上下文切换
上下文切换是计算密集型的,每次切换需要纳秒量级时间,所以对系统来说意味着大量 CPU 时间的被消耗。而且可能是操作系统中消耗时间最多的操作
线程优缺点
优点
- 提高吞吐率
- 提高响应速度
- 利用多核
- 最小化系统资源的使用
- 简化程序结构
缺点
- 安全问题
- 活性问题。死锁、活锁、饥饿
- 上下文切换产生的系统时间消耗
- 可靠性。单进程多线程,进程死则所有线程都死!
推荐书单
1.《实战 Java 高并发程序设计(第 2 版)》 作者:葛一鸣 出版社:机械工业出版社 出版时间:2018-10 ISBN:9787121350030
2.《Java 并发编程的艺术》 作者:方腾飞、魏鹏、程晓明 出版社:机械工业出版社 出版时间:2015-07 ISBN:9787111508243
3.《Java 编程的逻辑》 作者:马俊昌 出版社:机械工业出版社 出版时间:2018-01 ISBN:9787111587729