目录
线程上下文切换(Thread Context Switch)
进程与线程
进程
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU
,数据加载至内存。在 指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO
的。
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360
安全卫士等)
线程
一个进程之内可以分为一到多个线程。
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU
执行
Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在
windows
中进程是不活动的,只是作为线程的容器
二者对比
进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
进程拥有共享的资源,如内存空间等,供其内部的线程共享
进程间通信较为复杂
同一台计算机的进程通信称为 IPC(
Inter-process communication
)
不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
并行与并发
并发(concurrent
)是同一时间应对(
dealing with
)多件事情的能力
并行(parallel
)是同一时间动手做(
doing
)多件事情的能力
例子
家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发
家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一 个人用锅时,另一个人就得等待)
创建和运行线程
方法一,直接使用 Thread
// 创建线程对象
Thread t = new Thread() {
public void run() {
// 要执行的任务
}
};
// 启动线程
t.start();
方法二,使用 Runnable
配合
Thread
把【线程】和【任务】(要执行的代码)分开
Thread 代表线程
Runnable 可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable() {
public void run(){
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();
小结
方法1 是把线程和任务合并在了一起,方法
2
是把线程和任务分开了
用 Runnable 更容易与线程池等高级
API
配合
用 Runnable 让任务类脱离了
Thread
继承体系,更灵活
方法三,FutureTask
配合
Thread
FutureTask 能够接收 Callable
类型的参数,用来处理有返回结果的情况
// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
log.debug("hello");
return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);
输出:
19:22:27 [t3] c.ThreadStarter - hello
19:22:27 [main] c.ThreadStarter - 结果是:100
查看进程线程的方法
windows
任务管理器可以查看进程和线程数,也可以用来杀死进程
tasklist 查看进程
taskkill 杀死进程
linux
ps -fe
查看所有进程
ps -fT
-
p <PID>
查看某个进程(
PID
)的所有线程
kill
杀死进程
top 按大写 H
切换是否显示线程
top -H
-
p <PID>
查看某个进程(
PID
)的所有线程
Java
jps 命令查看所有 Java
进程
jstack <PID> 查看某个 Java
进程(
PID
)的所有线程状态
jconsole 来查看某个 Java
进程中线程的运行情况(图形界面)
栈与栈帧
Java Virtual Machine Stacks (
Java
虚拟机栈)
我们都知道 JVM
中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。
每个栈程由多个栈帧(Frame)
组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换(Thread Context Switch)
因为以下一些原因导致 cpu
不再执行当前的线程,转而执行另一个线程的代码
线程的 cpu 时间片用完
垃圾回收
有更高优先级的线程需要运行
线程自己调用了 sleep、
yield
、
wait
、
join
、
park
、
synchronized
、
lock
等方法
当 Context Switch
发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,
Java
中对应的概念就是程序计数器(Program Counter Register
),它的作用是记住下一条
jvm
指令的执行地址,是线程私有的
状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
Context Switch 频繁发生会影响性能
start 与 run
直接调用 run
是在主线程中执行了
run
,没有启动新的线程
使用 start
是启动新的线程,通过新的线程间接执行
run
中的代码
sleep 与 yield
sleep
1. 调用 sleep
会让当前线程从
Running
进入
Timed Waiting
状态(阻塞)
2. 其它线程可以使用 interrupt
方法打断正在睡眠的线程,这时
sleep
方法会抛出
InterruptedException
3. 睡眠结束后的线程未必会立刻得到执行
4. 建议用 TimeUnit
的
sleep
代替
Thread
的
sleep
来获得更好的可读性
yield
1. 调用 yield
会让当前线程从
Running
进入
Runnable
就绪状态,然后调度执行其它线程
2. 具体的实现依赖于操作系统的任务调度器
线程优先级
线程优先级会提示(hint
)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
如果 cpu
比较忙,那么优先级高的线程会获得更多的时间片,但
cpu
闲时,优先级几乎没作用
主线程与守护线程
默认情况下,Java
进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守 护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
log.debug("开始运行...");
sleep(2);
log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1);
log.debug("运行结束...");
输出
08:26:38.123 [main] c.TestDaemon - 开始运行...
08:26:38.213 [daemon] c.TestDaemon - 开始运行...
08:26:39.215 [main] c.TestDaemon - 运行结束...
注意
垃圾回收器线程就是一种守护线程
Tomcat 中的 Acceptor
和
Poller
线程都是守护线程,所以
Tomcat
接收到
shutdown
命令后,不会等待它们处理完当前请求
线程六种状态
【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU
调度执行
【运行状态】指获取了 CPU
时间片运行中的状态
当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
【阻塞状态】
如果调用了阻塞 API,如
BIO
读写文件,这时该线程实际不会用到
CPU
,会导致线程上下文切换,进入
【阻塞状态】
等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑 调度它们
【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
临界区 Critical Section
一个程序运行多个线程本身是没有问题的
问题出在多个线程访问共享资源
多个线程读共享资源其实也没有问题
在多个线程对共享资源读写操作时发生指令交错,就会出现问题
一段代码块内如果存在对共享资源
的多线程读写操作,称这段代码块为
临界区