2. 进程与线程
2.1 进程与线程
进程
-
进程是用来加载指令、管理内存、管理IO的,可以视为程序的一个实例,大部分程序可以同时运行多个实例进程。
-
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
线程
-
一个进程之内可以有多个线程
-
一个线程就是一个指令流,是Java中的最小调度单位。在Windows中进程不活动,作为线程的容器。
二者对比:
-
进程相互独立,拥有共享的资源,如内存空间,供内部线程共享。
-
进程间通信较复杂,不同计算机的进程通信需要通过网络,如HTTP
-
线程通信因为共享内存而相对简单,上下文切换成本比进程低。
2.2 并行与并发
-
线程轮流使用CPU称为并发(concurrent),指同一时间应对(dealing with)多件事情的能力
-
并行(parallel)是同一时间动手做(doing)多件事情的能力,即真正的同时运行
2.3 应用
应用之异步调用
-
从方法的调用角度来讲,如果需要等待结果返回,才能继续进行就是同步,反之是异步。
-
多线程可以让方法执行变为异步,如在读取磁盘文件时,可以避免阻塞主线程。
应用之提高效率
-
可以充分利用多核CPU的优势,将串行计算改为并行计算,花费时间只取决于最长线程运行时间,以及最后加上汇总时间。
-
单核虽然不能通过多线程提高效率,但可以让不同的线程轮流使用CPU ,避免被一个线程长时间占用。
-
IO操作不占用CPU,但阻塞IO(如拷贝文件)需要一直等待IO结束,不能充分利用线程。因此我们需要非阻塞IO和异步IO进行优化。
3. Java线程
3.1 创建和运行线程
Java默认有一个主线程在运行,如果还想创建线程,有三种方法。
方法一,直接使用Thread
例如:
// 构造方法的参数是给线程指定名字
Thread t1 = new Thread("t1") {
@Override
// run方法内实现了要执行的任务
public void run() {
log.debug("hello");
}
};
t1.start();
方法二,使用Runnable配合Thread
把线程和任务分开,Runnable表示可运行的任务(线程要执行的代码)
// 创建任务对象
Runnable task2 = new Runnable() {
@Override
public void run(){
log.debug("hello");
}
};
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();
Java 8 以后可以使用lambda精简代码,(有@FunctionInterface注解的接口)
Runnable task2 = () -> log.debug("hello");
用Runnable更容易与线程池等高级API配合,让任务类脱离了Thread继承体系,变为了组合关系。
方法三,FutureTask配合Thread
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况。
// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
log.debug("hello");
return 100;
});
new Thread(task3, "t3").start();
// get()会使主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);
输出
19:22:27 [t3] c.ThreadStarter - hello
19:22:27 [main] c.ThreadStarter - 结果是:100
3.2 查看进程线程的方法
Windows
-
任务管理器
-
tasklist
查看进程 -
taskkill
杀死进程
Linux
-
ps -fe
查看所有进程,输出的是静态的进程信息 -
ps -ft -p <PID>
查看某个进程的所有线程 -
kill
杀死进程 -
top
交互式的实时进程查看工具,按大写H切换是否显示进程 -
top -H -p <PID>
查看某个进程的所有线程
Java
-
jps
命令查看所有 Java 进程 -
jstack <PID>
查看某个 Java 进程(PID)的所有线程状态 -
jconsole
来查看某个 Java 进程中线程的运行情况(图形界面)
3.4 原理之线程运行
栈与栈帧
Java Virtual Machine Stacks (Java 虚拟机栈)
每个线程启动后,虚拟机就会为其分配一块栈内存。 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存。每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
线程上下文切换
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码:
线程的 cpu 时间片用完 垃圾回收 有更高优先级的线程需要运行
线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等 Context Switch 频繁发生会影响性能
3.5 常见方法
方法名 | static | 功能说明 | 注意 |
---|---|---|---|
start() | 启动一个新线 程,在新的线程 运行 run 方法 中的代码 | start 方法只是让线程进入就绪,里面代码不一定立刻 运行(CPU 的时间片还没分给它)。每个线程对象的 start方法只能调用一次,如果调用了多次会出现 IllegalThreadStateException | |
run() | 新线程启动后会 调用的方法 | 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象, 来覆盖默认行为 | |
join() | 等待线程运行结 束 | ||
join(long n) | 等待线程运行结 束,最多等待 n 毫秒 | ||
getId() | 获取线程长整型 的 id | id唯一 | |
getState() | 获取线程状态 | Java 中线程状态是用 6 个 enum 表示,分别为: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED | |
setPriority(int) | 修改线程优先级 | java中规定线程优先级是1~10 的整数,较大的优先级 能提高该线程被 CPU 调度的机率 | |
isInterrupted() | 判断是否被打 断, | 不会清除 打断标记 | |
isAlive() | 线程是否存活 (还没有运行完 毕) | ||
interrupt() | 打断线程 | 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记 ;如果打断的正在运行的线程,则会设置打断标记 ;park 的线程被打断,也会设置打断标记 | |
interrupted() | static | 判断当前线程是 否被打断 | 会清除 打断标记 |
currentThread() | static | 获取当前正在执 行的线程 | |
yield() | static | 提示线程调度器 让出当前线程对 CPU的使用 | 主要是为了测试和调试 |
Start与run的区别
直接调用 run 是在主线程中执行了 run,没有启动新的线程。
使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码。
sleep 与 yield
sleep
-
调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
-
其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
-
睡眠结束后的线程未必会立刻得到执行
-
建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
yield
-
调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程。
-
具体的实现依赖于操作系统的任务调度器。
3.6 join和interrupt方法详解
join()
方法用于等待线程的结束。当一个线程调用另一个线程的 join()
方法时,它会被阻塞,直到被调用的线程执行完成,或者达到了指定的超时时间。
作用:
-
等待其他线程完成任务:当一个线程需要依赖另一个线程的结果时,可以使用
join()
方法等待另一个线程执行完成,然后再继续执行。 -
协调线程执行顺序:通过
join()
方法可以协调多个线程的执行顺序,保证在某个线程执行完成后再执行下一个线程。 -
等待线程结束后进行资源回收:有时候需要在主线程等待子线程执行完成后再进行资源回收等操作,可以使用
join()
方法实现。
interrupt()
方法是 Java 中的一个线程方法,用于中断线程的执行。它会将目标线程的中断标志位设置为 true
,通常用于请求线程在合适的时候终止执行。
作用:
-
请求线程中断:
interrupt()
方法向目标线程发送一个中断请求,通常用于请求线程在合适的时候终止执行。 -
唤醒被阻塞的线程:如果目标线程因为阻塞状态而被挂起,调用
interrupt()
方法会唤醒它并抛出InterruptedException
异常。抛出异常后,会清空打断标记,即设为false。例如打断 sleep,wait,join 等线程。 -
检查线程是否被中断:可以通过调用
Thread.currentThread().isInterrupted()
方法来检查当前线程的中断状态,以确定是否需要中断线程的执行。
打断 park 线程
打断 park 线程, 不会清空打断状态
private static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
}, "t1");
t1.start();
sleep(0.5);
t1.interrupt();
}
输出
21:11:52.795 [t1] c.TestInterrupt - park...
21:11:53.295 [t1] c.TestInterrupt - unpark...
21:11:53.295 [t1] c.TestInterrupt - 打断状态:true
如果打断标记已经是 true, 则 park 会失效。可以使用 Thread.interrupted() 清除打断状态,将标记设为false。
3.7 不推荐的方法
还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁
-
stop() 停止线程运行
-
suspend() 挂起(暂停)线程运行
-
resume() 恢复线程运行
3.8 主线程与守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。
有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。垃圾回收器线程就是一种守护线程
3.8 线程状态
操作系统层面的五种状态:![](https://i-blog.csdnimg.cn/direct/a8b2a880797f4312a09441c8e781cc0c.png)
【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
【运行状态】指获取了 CPU 时间片运行中的状态 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
【阻塞状态】 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
Java API层面的六种状态(Thread.State)
NEW 线程刚被创建,但是还没有调用 start() 方法
RUNNABLE 当调用了start() 方法之后,Java API 层面的 RUNNABLE状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行) BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
TERMINATED 当线程代码运行结束