1. 进程与线程
1.1 进程与线程
1.1.1 进程
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
1.1.2 线程
一个进程之内可以分为一到多个线程
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器
1.1.3 两者对比
进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
进程拥有共享的资源,如内存空间等,供其内部的线程共享
进程间通信较为复杂
- 同一台计算机的进程通信称为 IPC(Inter-process communication)
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
1.2 并行与并发
单核 cpu 下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows 下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。总结为一句话就是: 微观串行,宏观并行
一般会将这种 线程轮流使用 CPU 的做法称为并发,concurrent
单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 cpu ,不至于一个线程总占用 cpu,别的线程没法干活
多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的
- 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率
- 但不是所有计算任务都能拆分,也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义
IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化
引用 Rob Pike 的一段描述: 并发(concurrent)是同一时间应对(dealing with)多件事情的能力 并行(parallel)是同一时间动手做(doing)多件事情的能力
家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发 家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一个人用锅时,另一个人就得等待) 雇了3个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时是并行
2. Java 线程
2.1 创建和运行线程
public static void main(String[] args) {
new Thread("t1") {
@Override
public void run() {
log.info("t1 run");
}
}.start();
}
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
log.info("t2 run");
}
};
new Thread(runnable, "t2").start();
}
public static void main(String[] args) throws Exception {
FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 100;
}
});
// 启动线程
new Thread(futureTask, "t3").start();
// 主线程阻塞
Integer result = futureTask.get();
log.info("结果是 {}", result);
}
2.2 线程上下文切换
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
- 线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch 频繁发生会影响性能
2.3 常见方法
| 方法名 | static | 功能说明 | 注意 | | —- | —- | —- | —- | | start() | | 启动一个新线程,在新的线程运行run()
方法中的代码 | start 方法只是让线程进入就绪,代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的 start 方法只能调用一次,如果调用了多次会出现 IllegalThreadStateException | | run() | | 新线程启动会调用的方法 | 如果在构造 Thread 对象时传入了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象来覆盖默认行为 | | join() | | 等待线程运行结束 | | | join(long) | | 等待线程运行结束,最多等待多少毫秒 | | | getId() | | 获取线程长整型的 id | id 唯一 | | getName() | | 获取线程名称 | | | setName(String) | | 修改线程名称 | | | getPriority() | | 获取线程优先级 | | | setPriority(int) | | 修改线程优先级 | Java 中规定线程优先级是 1~10 的整数,较大的优先级能提高该线程被 CPU 调度的频率 | | getState() | | 获取线程状态 | Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED | | isInterrupted() | | 判断是否被打断 | 不会清除打断标记 | | isAlive() | | 线程是否存活 | | | interrupt() | | 打断线程 | 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记
打断正在运行的线程,会设置打断标记不清除状态
打断 park 的线程,会设置打断标记不清除状态 | | interrupted() | 是 | 判断当前线程是否被打断 | 会清除打断标记 | | currentThread() | 是 | 获取正在执行的线程 | | | sleep(long) | 是 | 让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程 | | | yield() | 是 | 提示线程调度器让出当前线程的对 CPU 的使用 | |
2.4 start 和 run
直接调用 run 是在主线程中执行了 run,没有启动新的线程
使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
public static void main(String[] args) {
Thread thread = new Thread("t1") {
@Override
public void run() {
log.info("t1 start");
}
};
// 这里是 main 线程打印日志
thread.run();
// 这里是 t1 新线程打印日志
thread.start();
}
2.5 sleep 和 yield
sleep
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
yield
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
-
2.6 join
```java @Slf4j public class ThreadJoin {
static int result = 1;
public static void main(String[] args) throws Exception {
Thread thread = new Thread(() -> result = 10, "t1");
thread.start();
// 确保线程运行结束, 将结果改成 10
thread.join();
log.info("result {}", result);
}
}
```java
@Slf4j
public class ThreadMultiJoin {
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
Thread t2 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
r2 = 20;
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
// 等待 t1 时, t2 仍然在运行
t1.join();
// 因此在等待 t2 时, 只需要在等待 1s 即可
t2.join();
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
}
2.7 interrupt
打断 sleep,wait,join 的线程
这几个方法都会让线程进入阻塞状态
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
log.error(e.getMessage(), e);
}
}, "t1");
t1.start();
TimeUnit.MILLISECONDS.sleep(100);
// t1 interrupt 状态是 false
log.info("t1 interrupt 状态是 {}", t1.isInterrupted());
t1.interrupt();
// t1 interrupt 状态是 true
log.info("t1 interrupt 状态是 {}", t1.isInterrupted());
}
public static void main(String[] args) throws Exception {
Thread t2 = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
boolean interrupted = current.isInterrupted();
if (interrupted) {
// 打断状态: true
log.debug(" 打断状态: {}", interrupted);
break;
}
}
}, "t2");
t2.start();
TimeUnit.MILLISECONDS.sleep(100);
t2.interrupt();
}
public static void main(String[] args) throws Exception {
Thread t3 = new Thread(() -> {
LockSupport.park();
// 打断状态 true
log.info("打断状态 {}", Thread.currentThread().isInterrupted());
}, "t1");
t3.start();
TimeUnit.MILLISECONDS.sleep(100);
t3.interrupt();
}
public static void main(String[] args) throws Exception {
Thread t3 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
// 如果线程已经被打断, 这里的 park() 方法会失效,会一直循环 3 次
LockSupport.park();
// 打断状态 true
log.info("打断状态 {}", Thread.currentThread().isInterrupted());
}
}, "t1");
t3.start();
TimeUnit.MILLISECONDS.sleep(100);
t3.interrupt();
}
2.8 不推荐的方法
方法名 | 功能说明 |
---|---|
stop() | 停止线程运行 |
suspend() | 挂起(暂停)线程运行 |
resume() | 恢复线程运行 |
2.9 主线程与守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
log.info("子线程运行开始");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
log.error(e.getMessage(), e);
}
log.info("子线程运行结束");
}, "t1");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
TimeUnit.MILLISECONDS.sleep(100);
log.info("主线程运行完成");
}
2.10 线程状态
2.10.1 五种状态(操作系统)
【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
【运行状态】指获取了 CPU 时间片运行中的状态
- 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
【阻塞状态】
- 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】
- 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
- 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
2.10.2 六种状态(Java API)
【NEW】 线程刚被创建,但是还没有调用 start() 方法
【RUNNABLE】 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
【BLOCKED、WAITING、TIMED_WAITING】 都是 Java API 层面对【阻塞状态】的细分
【TERMINATED】 当线程代码运行