简介
一、进程与线程
参考:进程和线程
1、进程:
- 进程是由指令和数据组成的,但这些指令要运行,数据要读写,就必须将指令加载到CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络设备。进程就是用来加载指令、管理I/O、管理内存
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就启动了一个进程
- 进程可以视为程序的一个运行实例(程序是静态的,进程是动态的),大部分程序可以运行多个进程(浏览器、记事本等),也有的程序只运行一个进程(360等)
2、线程:
- 一个进程中可以有一到多个进程
- 一个进程就是一段指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
- 在Java中,线程作为最小的调度单位,进程作为资源分配的最小单位。(Windows中进程是不活动的,只是作为线程的容器)
- 线程会共享进程范围内的资源,例如内存句柄和文件句柄,但每个线程都有各自的程序计数器(Program Counter)、栈以及局部变量等
3、二者对比:
- 进程基本上相互独立,而线程存在于进程中,是进程的一部分
- 进程拥有共有资源,如内存空间等,供其内部分的线程共享
- 进程间通信较为复杂
- 同一台计算器中的进程通信称为IPC(Inter Process Communication)
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议例如HTTP
- 线程通信相对的简单,因为他们共享进程中的内存,一个例子是多个线程可以访问同一个共同变量
- 线程更加轻量,线程上下文切换成本一般低于进程的上下文切换。
4、并发与并行:
单核CPU下,线程实际上还是串行执行的。操作系统中有一个组件叫做任务调度器,将CPU的时间片(Windows下默认是最小15毫秒)分给不同的线程使用,只是由于CPU在线程之间的切换非常快,人们感觉是同时运行的。总结一句话是:微观串行,宏观并行
一般将这种线程轮流使用CPU的做法叫做并发(concurrent)。
多核CPU下,每个核(core)都可以调度运行线程,这时候线程可以是**并行(Parallel)**的
引用Rob Pike的一段描述:
- 并发(Concurrent)是同一时间应对(deal with)多件事情的能力
- 并行(Parallel)是同一时间动手做(doing)多件事情的能力
二、线程的应用场景
1、应用之异步调用
从方法调用的角度来讲,如果
- 需要等待方法调用结束,才能继续运行称为同步
- 不需要等待方法调用结束,就能继续运行称为异步
注意:同步在多线程中还有一层意思是让多个线程步调一致
多线程可以让方法执行变成异步的(即不需要等待),比如说读取磁盘文件时,假设读取操作花费5秒,如果没有多线程调度机制,这5秒什么都干不了,其他代码都需要暂停
例子:
- UI程序中,开启线程运行费时操作,避免阻塞UI线程
- Tomcat的异步Servlet也有类似的目的,让用户线程处理耗时较长的操作,避免阻塞Tomcat的工作线程
2、应用之提升效率
多线程能够充分利用多核CPU的优势,提高运行效率。对下面的场景:执行三个计算,最后将计算结果汇总
计算1花费10ms
计算2花费12ms
计算3花费9ms
汇总花费1ms
- 如果是串行执行,那么总共花费 10+12+9+1=32ms
- 如果是四核CPU,各个核心分别使用线程123运行计算123,那么三个线程是并行的,花费的时间只取决于最长那个线程运行的时间即12ms,最后加上汇总的时间总共13ms
注意:需要在多核CPU下才能提高执行效率,单核仍然是轮流执行,效率可能更低
结论:
- 单核CPU下,多线程并不能实际提高程序运行效率,只是为了能够在不同任务之间切换,不同线程轮流使用CPU。同时线程切换需要一定的成本。
- 多核CPU可以并行运行多个线程,但是能否提高运行效率还是要分情况
- 有些任务可以进行拆分,并行执行进而提高执行效率,但不是所有任务都可以拆分(参考后文【阿姆达尔法则】)
- 也不是所有任务都需要拆分
- IO操作不占用CPU,只是一般情况下使用的是阻塞IO,此时需要等待IO结束才能继续运行线程。所以才有了后文介绍的非阻塞IO和异步IO优化
Java线程
一、创建运行线程
1、方法一:直接使用Thread匿名内部类
//创建线程对象,创建时最好传入线程的名称
Thread t1 = new Thread("t1"){
//run方法内实现要执行的方法
@Override
public void run(){
//要执行的任务
}
};
//启动线程
t1.start();
2、使用Runnable配合Thread
把【线程】和【任务】(要执行的代码)分开
- Thread代表线程
- Runnable代表可执行任务(线程要执行的代码)
//创建可执行任务对象
Runnable runnable = new Runnable() {
@Override
public void run() {
//要执行的任务
}
};
//创建线程对象,参数1是可执行任务,参数2是线程的名称
Thread t1 = new Thread(runnable, "t1");
//运行线程
t1.start();
Java8之后可以使用lambda简化代码
Runnable runnable = () -> {
//要执行的任务
};
Thread t1 = new Thread(runnable, "t1");
t1.start();
Thread和Runnable之间的关系:
第二种方法中,传入Thread类构造方法的runnable对象会赋值给,Thread类中的成员变量target
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
...
this.target = target;
...
}
而Thread类继承了只有一个抽象方法的Runnable类:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
public
class Thread implements Runnable {
...
}
并在run()方法中进行了如下重写,如果发现target不为空(也就是传入了可执行任务runnable对象),则执行该runnable对象的run方法,也就是第二种方法。而第一种方式就是直接重写了Thread类的run()方法。
@Override
public void run() {
if (target != null) {
target.run();
}
}
总结:
- 方法1把线程和执行任务合并到一起,方法2则把线程和任务分开了
- 用Runnable更容易与线程池等高级API配合
- 用Runnable让任务脱离了Thread继承体系,更加灵活
2、FutureTask配合Thread
FutureTask能够接收Callable类型的参数,用来处理有返回结果的情况
//创建任务对象
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
//执行任务,并返回结果
return 1;
}
});
//传入任务对象和线程名,并运行线程
Thread t1 = new Thread(task, "t1");
t1.start();
//主线程阻塞,同步等待task执行完毕的结果
Integer result = task.get();
二、查看进程线程的方法
1、Windows
- 任务管理器可以查看进程和线程数,也可以用来杀死线程
- tasklist 查看进程
- taskkill 杀死进程
2、Linux
ps -ef
:查看所有进程ps -fT -p <PID>
:查看某个进程(PID)所有的线程kill PID
:杀死进程top
:动态的查看进程运行情况top -H -p <PID>
:查看某个进程(PID)的所有线程
3、Java
jps
查看所有的Java进程jstack <PID>
:查看某个进程(PID)的所有线程的状态jconsole <PID>
:使用图形界面查看当前进程允许的状况
三、线程运行的原理
1、栈和栈桢
Java Virtual Machine Stack(Java虚拟机栈)
JVM中由堆、栈、方法区等组成。每个线程启动之后,虚拟机就会为其分配一块栈内存。
- 每个栈由多个栈桢(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程每个时刻只能有一个活动栈桢,对应着当前正在执行的那个方法
- 栈桢在栈中的进进出出对应着方法的调用
对于下面这段这段代码,其单线程执行情况可以用下图表示:
public class FrameTest {
public static void main(String[] args) {
method1(10);
}
private static void method1(int x){
int y = x+1;
Object obj = method2();
System.out.println(obj);
}
private static Object method2() {
Object obj = new Object();
return obj;
}
}
2、线程的上下文切换(Thread Context Switch)
下面的一些情况会导致CPU不再执行当前的线程,转而执行另外的线程,即现场的上下文切换
- 线程的CPU时间片用完
- 垃圾回收的STW
- 有更高优先级的线程需要执行
- 线程自己调用sleep、yield、wait、join、park、synchronized、lock等方法
当上下文切换发生时,需要保存当前线程的状态,并恢复另一个线程的状态
- 其中状态包括:程序计数器、虚拟机栈中每个栈桢的信息,如局部变量表、操作数栈、返回地址等
- 上下文切换如果频繁发生会影响性能
下图展示了线程的上下文切换,CPU核心转向了t1线程,并对main线程的内存进行了保存(灰色)
四、线程相关常见方法
1、Start与Run
start方法:启动一个新的线程,并在新的线程中运行run方法
- start方法只是让线程进入就绪,里面代码不一定立刻执行(CPU时间片还没有分给他)
- 每个线程的Start方法只能调用一次,如果多次调用会出现
IllegalThreadStateException
异常
run方法:线程启动后会调用的方法
- Thread对象直接调用run方法不会开启新线程,run方法中内容虽然可以运行但是还是在主线程中执行
- 如果构造Thread对象时参入了Runnable对象,则调用的时Runnable的run方法。否则默认不执行任何操作,但是可以创建Thread的子类对象来重写默认行为
2、sleep与yield
sleep方法:让当前执行的线 程休眠n毫秒, 休眠时让出 cpu 的时间片给其它线程
- 调用sleep方法会让当前线程从Running状态进入Time Waiting状态
- 其他线程可以使用interrupt方法打断正在睡眠的线程,这时被打断的线程会抛出
InterruptedException
- 睡眠结束后的线程未必立刻得到执行,需要等到CPU分给他时间片
- 建议使用TimeUnit的sleep方法代替Thread的Sleep方法,可以获得更好的可读性
TimeUnit.SECONDS.sleep(1); //还有DAYS等其他时间
yield方法:提示线程调度器让出当前线程对CPU的使用
- 调用yield方法会让当前线程从Running运行状态转变为Runnable就绪状态,然后调用其他线程
- 具体情况依赖于操作系统的任务调度器。如果当前没有其他线程,那仍然会给此线程分配CPU的时间片,让其继续执行。而相对于Sleep在计时完成之前,调度器不会为其分配CPU的时间片。
线程优先级:
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略
- 如果CPU比较忙,那么优先级高的线程可以获得更多的时间片,但是CPU闲的时候,优先级影响效果不大
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
案例-防止CPU占用100%
在没有利用CPU来计算时,不要用while(true)空转浪费CPU。否则单核CPU下,CPU占用率可能达到近100%。这时可以使用sleep或者yield方法来让出CPU给其他程序
while(true){
try{
Thread.sleep(50);
} catch(InterruptedException){
e.printStackTrace();
}
}
- 可以使用wait或条件变量达到类似的效果,不同是,后两种需要加锁,并且需要相应的唤醒操作,一般适用于需要同步的场景
- sleep适用于无需锁同步的场景
3、join方法
join方法:等待线程运行结束,之后才会运行后面的代码。可以用于同步。
对于下面的代码,r的值会输出为多少呢?
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
System.out.println("开始");
Thread t1 = new Thread(() -> {
try {
System.out.println("开始");
sleep(1);
System.out.println("结束");
r = 10;
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1");
t1.start();
// t1.join();
System.out.println("结果为:"+ r);
System.out.println("结束");
}
结果是0,原因很简单,因为线程t1中的run方法还没执行结束,r的值就已经被输出了。那么要怎么做到等到r赋值完成之后在进行输出呢?
只要把第18行的注释去掉,调用线程t1的join方法。之后的代码就会等到t1线程执行完成之后才会执行。则此r的值已经被赋值为了10:
join(long n)方法:等待线程运行结束,最多等待 n 毫秒。
- 有时限的等待
- 如果 n 毫秒之前,线程就已经结束了,那么不会继续等待
4、interrupt方法
interrupt()方法:打断线程
- 如果被打断线程正在 sleep,wait,join,会导致被打断的线程抛出
InterruptedException
,并清除打断标记 - 如果打断的正在运行的线程,则会设置打断标记
- park 的线程被打断,也会设置打断标记
isInterrupted()方法:判断当前线程是否被打断,之后不会清除打断标记
interrupted() 方法:判断当前线程是否被打断,并清除打断标记
1)、打断线程正在 sleep,wait,join的例子:
private static void test1() throws InterruptedException {
Thread t1 = new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(" 打断状态: " + Thread.currentThread().isInterrupted());
}
}, "t1");
t1.start();
Thread.sleep(500);
t1.interrupt();
}
结果是:触发了InterruptedException
异常,同时也清除了打断标记
2)、打断的正在运行的线程:
private static void test2() throws InterruptedException {
Thread t1 = new Thread(()->{
while (true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if (interrupted) {
System.out.println(" 打断状态: " + Thread.currentThread().isInterrupted());
System.out.println("被打断之后,可以再做一些其他事情之后再停止");
break;
}
}
}, "t1");
t1.start();
Thread.sleep(500);
t1.interrupt();
}
结果如下图所示,调用interrupt()
方法之后interrupted
被标记为true,但是线程没有像之前那样抛出异常结束,而是继续运行。如果想要结束线程,可以在线程中判断interrupted的状态进行结束,这种方式更加优雅,可以在结束之前进行一些善后工作。
5、两阶段终止模式
引入:在一个线程T1中如何“优雅”终止线程T2?这里的“优雅”是指给T2一个料理后事的机会。
错误做法:
- 使用线程对象的stop方法停止线程
- stop方法会真正的杀死进程,如果这时线程锁住了共享资源,那么它被杀死后就再也没有机会释放资源锁,那么其他线程也永远无法获得该资源的锁
- 使用
System.exit(init)
方法停止线程- 这种做法会使整个程序终止。
正确做法:使用两阶段终止模式
假设当前需要不断每2秒钟进行一次监控信息获取,并且在不需要的时候可以进行打断。使用两阶段终止模式实现的整体流程如下图所示:
具体实现框架可以参考下面的代码:
/**
* 两阶段终止框架
*/
public class TwoPhaseTermination {
//监控线程
private static Thread monitor = null;
public static void main(String[] args) throws InterruptedException {
start();
Thread.sleep(7000);
stop();
}
//开始监控线程
private static void start(){
monitor = new Thread(new Runnable() {
@Override
public void run() {
while (true){
if (monitor.isInterrupted()){
//运行中检测到Interrupte,进行善后工作,并结束
System.out.println("善后工作");
break;
}
try {
//每隔2秒执行一次检测程序
Thread.sleep(2000);
System.out.println("执行检测程序");
} catch (InterruptedException e) {
//睡眠中被打断会抛出该异常,并继续执行
e.printStackTrace();
//手动打断线程
monitor.interrupt();
}
}
}
}, "monitor");
monitor.start();
}
//停止监控线程
private static void stop(){
monitor.interrupt();
}
}
执行结果如下图所示:
6、不推荐使用多个的方法
这些方法已经过时,容易破坏同步代码块,造成线程死锁
- stop方法:停止线程运行
- suspend方法:挂起(暂停)线程
- resume方法:恢复线程运行
五、线程其他相关内容
1、主线程和守护线程
默认情况下,Java进程需要等到所有线程都结束才会停止。但是有一种特殊的线程称为守护线程,只要当其他非守护线程都结束,即使守护线程的代码没有执行完,也会强制停止。
public static void main(String[] args) {
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 命令后,不会等 待它们处理完当前请求
2、线程的五种状态(操作系统层面)
线程的五种状态是从操作系统层面上描述的:
- 初始状态:仅是在语言层面上的对象创建,还未于操作系统线程关联
- 可运行状态(就绪状态):指该线程已经被创建(与操作系统线程关联),可以由CPU执行调度
- 运行状态:指获取到了CPU时间片,正在运行中的状态。
- 当CPU时间片用完,会从【运行状态】转变到【可运行状态】,对应着线程上下文的切换
- 阻塞状态:
- 如果调用了阻塞API,如BIO读取文件,这时线程实际上不会用到CPU,会导致线程上下文切换,进入【阻塞状态】
- 等待BIO操作完成,会由操作系统唤醒阻塞的线程,转换为【可运行状态】
- 【阻塞状态】和【可运行状态】的区别是,对于【阻塞状态】的线程只要他们一直不唤醒,调度器就一直不会考虑调度他们
- 终止状态:表示线程已经执行完毕,生命周期已经结束,不会在转为其他状态
3、线程的六种状态(Java语言层面)
这是从 Java API 层面来描述的 Thread.State 枚举,线程分为六种状态:
- NEW:线程对象刚创建,还没有调用start方法。与操作系统层面上的初始状态对应
- RUNNABLE:当调用了start方法之后。,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)
- BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节 详述
- TERMINATED 当线程代码运行结束