概念
进程与线程
进程
- 程序由指令和数据组成,当一个程序被运行,从磁盘加载这个程序的的代码到内存,这是就开启了一个进程
- 进程可以视为程序的一个实例
线程
- 一个进程之类可以分为一个或多个线程
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
两者对比
- Java中,线程作为最小的调度单位,进程作为资源分配的最小单位
- 在Windows中进程是不活动的,只是作为线程的容器
- 进程基本上相互独立,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间,供其内部的线程共享
- 进程间通信较为复杂
- 同一台计算机的进程通信成为IPC(Inter-process communication)
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如HTTP
- 线程通信较为简单,因为它们共享进程的内存
- 线程更轻量,线程上下文切换成本一般要比继承上下文切换低
并行与并发
并发(concurrent):线程轮流使用CPU(微观串行,宏观并发)
并发(parrel):多核CPU下,每个核都可以调度运行线程(微观并行)
同步与异步
同步与异步的概念是从方法调用的角度来讲的:
- 同步:调用者需要等待返回结果
- 异步:调用者不需要等待返回结果
栈与栈帧
- 每个栈有多个栈帧组成,对应着每次方法调用所占用的内存
- 每个线程只能由一个活动栈帧,对应着当前执行的那个方法
线程上下文切换
以下原因会导致cpu不再执行当前线程,转而执行另一个线程
- 线程的CPU时间片用完了
- 垃圾回收
- 有更高优先级的线程需要执行
- 线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法。
jvm保存当前当前线程状态的工具是程序计数器,它的作用是记住下一条jvm指令的执行地址,属于线程私有。
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
频繁地上下文切换会影响性能,因此线程数并不是越多越好。
创建和启动线程
方法1
直接创建一个新的Thread类,并覆盖其run()
方法
// 创建线程
Thread hello = new Thread(){
@Override
public void run() {
log.debug("hello");
}
};
hello.setName("hello");
// 启动线程
hello.start();
log.debug("hello");
日志打印结果如下
21-05-11 15:36:16.557 [DEBUG][hello] i.k.e.c.e.CreatThread : hello
21-05-11 15:36:16.557 [DEBUG][main] i.k.e.c.e.CreatThread : hello
方法2
先创建一个Runnable实例,在实例化Thread时将Runnable对象传入。
通过Runnable对象,可以将【任务】与【线程】分开
Runnable runnable = () -> log.debug("hello");
Thread hello = new Thread(runnable,"hello");
hello.start();
log.debug("hello");
日志打印结果如下
21-05-11 15:46:33.081 [DEBUG][main] i.k.e.c.e.CreatThread : hello
21-05-11 15:46:33.081 [DEBUG][hello] i.k.e.c.e.CreatThread : hello
方法2的原理
在创建Thread对象是,如果传入了Runnable对象,则会执行到Thread私有的构造方法private Thread(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals)
,其中参数target
就是我们传入的Runnable对象,在该方法中有
this.target = target;
也就是说,我们传入的Runnable对象会与创建的Thread对象进行组合。
而在Thread的run()
方法中,其逻辑如下,
public void run() {
if (target != null) {
target.run();
}
}
因此,run()
方法实际上会执行到我们传入的Runnable对象本身的run()
方法。
在方法1中,我们会直接覆盖Thread的run()
方法,让他执行我们期望的逻辑。
方法3
使用FutureTask创建任务对象,并且会得到任务的返回结果。创建FutureTask对象前需要先创建一个Callable对象,并且实现其中的call()
方法。
FutureTask<Integer> task = new FutureTask<>(() -> {
log.debug("future-task");
Thread.sleep(1000);
return 100;
});
Thread thread = new Thread(task,"task");
thread.start();
// 通过get方法得到返回结果
Integer i = task.get();
log.debug("ret: {}",i);
日志打印结果如下
21-05-11 15:48:52.822 [DEBUG][task] i.k.e.c.e.CreatThread : future-task
21-05-11 15:48:53.826 [DEBUG][main] i.k.e.c.e.CreatThread : ret: 100
方法3的原理
首先,FutureTask本身实现了Runnable接口,因此在创建Theard对象时,传入FutureTask对象会被视为传入了Runnable对象,根据上面的分析,线程启动最终会调用FutureTask的run()
方法。
在FutureTask的run()
方法中,其执行逻辑的核心代码如下
// 获取传入的Callable对象
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
// 执行Callable对象的call方法,并拿到结果
result = c.call();
// 标记call方法顺利执行
ran = true;
} catch (Throwable ex) {
// 标记call方法抛出异常,并记录异常
result = null;
ran = false;
setException(ex);
}
// 如果call方法顺利执行,则将结果保存到类中,方便后续取用
if (ran)
set(result);
}
在调用FutureTask的get()
方法时,如果方法未执行结束,则会发生阻塞,如果程序运行运行结束,则会返回运行结果或者运行出现的异常。
线程的状态
五种状态(操作系统层面)
- 初始状态:仅仅是语言层面创建了线程对象,还没有与操作系统关联
- 可运行状态(就绪状态):指该线程已经被创建(与操作系统关联),可以由CPU调度执行
- 运行状态:指获取了CPU时间片运行中的状态
- 当CPU时间片用完,会从运行状态切换到可运行状态,会导致上下文切换
- 阻塞状态:
- 如果调用了阻塞API,如BIO读写文件,这时该线程实际不会用到CPU,会导致上下文切换
- 当BIO等阻塞API操作完毕,会由操作系统唤醒阻塞的线程,转换到可运行状态
- 终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
六种状态(Java层面)
六种状态是从Java API层面来描述的,根据Thread.State枚举类,分为六种状态。
- NEW:线程刚刚被创建,但是还没有调用
start()
方法,对应操作系统的初始状态 - RUNNABLE:当线程调用了
start()
方法后会进入RUNNABLE状态- Java API层面的RUNNABLE状态涵盖了操作系统层面的可运行状态、运行状态和阻塞状态
- BLOCKED、WAITING、TIMED_WAITING是Java种的阻塞状态,也属于操作系统的阻塞状态
- TERMINATED:终止状态,表示线程已经运行完毕,对应操作系统的终止状态
线程的常见方法
常用方法一览
方法名 | 功能 | 注意 |
---|---|---|
start() | 启动一个新线程,在新的线程运行 run() 方法中的代码 | start() 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start() 方法只能调用一次,如果调用了多次会出现IllegalThreadStateException |
run() | 新线程启动后会调用的方法 | |
join() | 等待线程运行结束 | |
join(long n) | 等待线程运行结束,最多等待 n毫秒 | |
getId() | 获取线程长整型的 id | id 唯一 |
getName() | 获取线程名 | |
setName(String) | 修改线程名 | |
getPriority() | 获取线程优先级 | |
setPriority(int) | 修改线程优先级 | java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率 |
getState() | 获取线程状态 | |
isAlive() | 线程是否存活(还没有运行完毕) | |
isInterrupted() | 判断是否被打断 | 不会清除打断标记 |
interrupt() | 打断线程 | |
static interrupted() | 判断当前线程是否被打断 | 会清除打断标记 |
static currentThread() | 获取当前正在执行的线程 | |
static sleep(long n) | 让当前执行的线程休眠n毫秒,休眠时让出 cpu的时间片给其它线程 | |
static yield() | 提示线程调度器让出当前线程对CPU的使用 |
sleep与yield
sleep
- 调用
sleep()
方法会将线程从RUNNABLE变成TIMED_WAITTING状态(阻塞) - 其它线程可以使用
interrupt()
方法打断正在睡眠的方法,此时sleep()
方法会抛出InterruptedException
异常 - 睡眠结束后的线程未必就会立即执行
- 建议使用TimUnit的
sleep()
方法代替Thread的sleep()
方法,以提供更好地可读性
yield
- 调用```yield()``方法会让出当前线程的使用权,线程状态从运行状态变成可运行状态
- 其具体的实现依赖于操作系统的任务调度器
setPriority
调用setPriority()
方法会修改线程的优先级(范围为1~10,默认为5)。
- 线程优先级会提示调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果CPU比较忙,那么优先级高的的线程会获得更多的时间片,但CPU闲时,优先级机会没作用
对yield和setPriority进行代码测试
在测试程序中,我们创建了两个线程"t1"与"t2",这两个线程分别在死循环中打印输出各自的cnt
变量,且每打印一次,cnt
就自增。主线程在外部维持1秒后强制退出。
private static final String NORMAL_MODE = "normal";
private static final String YIELD_MODE = "yield";
private static final String PRIORITY_MODE = "priority";
private static void fun(String mode) throws InterruptedException {
Thread t1 = new Thread(() -> {
int cnt = 0;
while (true) {
log.debug("1------>{}", cnt++);
}
}, "t1");
Thread t2;
if (YIELD_MODE.equalsIgnoreCase(mode)){
t2 = new Thread(() -> {
int cnt = 0;
while (true) {
Thread.yield();
log.debug(" 2------>{}", cnt++);
}
}, "t2");
}else {
t2 = new Thread(() -> {
int cnt = 0;
while (true) {
log.debug(" 2------>{}", cnt++);
}
}, "t2");
}
if (PRIORITY_MODE.equalsIgnoreCase(mode)){
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);
}
t1.start();
t2.start();
TimeUnit.SECONDS.sleep(1);
System.exit(1);
}
在不做干预的情况下,程序退出时,两个线程中的cnt
应该是相近的。
根据函数参数mode
为YIELD_MODE
时,线程"t2"会在打印前调用yield()
方法让出cpu时间片;当mode
为PRIORITY_MODE
时,线程"t1"会被设定为高优先级,线程"t2"会被设定为低优先级。理论上说,以上两种方法都会使得线程"t1"获得的cpu时间片多余线程"t2",从而使得线程"t1"的cnt
大于线程"t2"。
当mode
为NORMAL_MODE
时,程序尾部的打印如下:
21-05-11 19:05:04.487 [DEBUG][t1] i.k.e.c.e.YieldAndPriorityDemo : 1------>23032
21-05-11 19:05:04.487 [DEBUG][t2] i.k.e.c.e.YieldAndPriorityDemo : 2------>23520
21-05-11 19:05:04.487 [DEBUG][t1] i.k.e.c.e.YieldAndPriorityDemo : 1------>23033
21-05-11 19:05:04.487 [DEBUG][t2] i.k.e.c.e.YieldAndPriorityDemo : 2------>23521
此时,线程"t1"的cnt
为23033,线程"t2"的cnt
为23521,两者较为接近。
当mode
为YIELD_MODE
时,程序尾部的打印如下:
21-05-11 19:24:24.319 [DEBUG][t2] i.k.e.c.e.YieldAndPriorityDemo : 2------>11955
21-05-11 19:24:24.320 [DEBUG][t1] i.k.e.c.e.YieldAndPriorityDemo : 1------>21168
21-05-11 19:24:24.320 [DEBUG][t1] i.k.e.c.e.YieldAndPriorityDemo : 1------>21169
此时,线程"t1"的cnt
为21169,线程"t2"的cnt
为11955,线程"t1"的cnt
明显大于线程"t2"。
当mode
为PRIORITY_MODE
时,程序尾部的打印如下:
21-05-11 19:25:26.042 [DEBUG][t2] i.k.e.c.e.YieldAndPriorityDemo : 2------>15346
21-05-11 19:25:26.043 [DEBUG][t1] i.k.e.c.e.YieldAndPriorityDemo : 1------>25420
21-05-11 19:25:26.043 [DEBUG][t1] i.k.e.c.e.YieldAndPriorityDemo : 1------>25421
此时,线程"t1"的cnt
为21169,线程"t2"的cnt
为11955,线程"t1"的cnt
明显大于线程"t2"。
join
当一个线程运行时,如果调用其join()
方法,则会等待该线程运行结束。
通过join()
方法,可以实现线程之间的同步。
join()
方法可以传入一个long类型的参数表示最长等待时间。
代码示例
测试代码的主函数中创建了一个线程"t1",该线程会在启动后1秒修改静态变量r
的值。
在主函数中启动线程"t1"后会立即打印静态变量r
的值。
private static int r = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("线程醒来");
r = 10;
}, "t1");
t1.start();
log.info("r = {}",r);
}
打印日志如下
21-05-11 20:31:14.146 [INFO][main] i.k.e.c.ex.JoinDemo : r = 0
21-05-11 20:31:15.145 [INFO][t1] i.k.e.c.ex.JoinDemo : 线程醒来
可以发现,主线程打印时,静态变量r
的值仍然为0,因为此时线程"t1"还没有醒来,也就没有改变r
的值。
下面的测试代码中,我在主函数打印静态变量r
的值之前,加上了t1.join()
,此时主函数会首先等待线程"t1"执行完成,然后打印数值。
private static int r = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("线程醒来");
r = 10;
}, "t1");
t1.start();
t1.join();
log.info("r = {}",r);
}
打印日志如下
21-05-11 20:45:55.792 [INFO][t1] i.k.e.c.ex.JoinDemo : 线程醒来
21-05-11 20:45:55.795 [INFO][main] i.k.e.c.ex.JoinDemo : r = 10
interrupt
调用interrupt()
方法,既可以打断处于阻塞状态的线程,也可以打断正在运行的线程。
打断阻塞状态的线程
打断处于阻塞状态的线程时,该线程会抛出异常,同时清除打断标记。
示例代码如下
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.info("线程启动");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
log.error(e.getMessage());
}
log.info("线程醒来");
}, "t1");
t1.start();
TimeUnit.SECONDS.sleep(1);
log.info("打断开始");
t1.interrupt();
log.info("t1的打断标记:{}",t1.isInterrupted());
}
日志如下
21-05-11 21:01:50.709 [INFO][t1] i.k.e.c.e.InterruptWaitingDemo : 线程启动
21-05-11 21:01:51.707 [INFO][main] i.k.e.c.e.InterruptWaitingDemo : 打断开始
21-05-11 21:01:51.707 [ERROR][t1] i.k.e.c.e.InterruptWaitingDemo : sleep interrupted
21-05-11 21:01:51.707 [INFO][t1] i.k.e.c.e.InterruptWaitingDemo : 线程醒来
21-05-11 21:01:51.707 [INFO][main] i.k.e.c.e.InterruptWaitingDemo : t1的打断标记:false
打断正常运行的线程
当其它线程调用正常运行的interrupt()
方法时,线程不会诊断被打断,而是依然正常运行,但此时它的打断标记会被置为true
,线程可以通过判断它的打断标记来观察外部是否有线程在打断它。
示例代码如下
在下面的示例代码中,主线程调用interrupt()
方法预打断正在执行的线程"t1",但是线程"t1"依然能够运行完毕。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.info("线程启动");
long i = 0;
while (i < Integer.MAX_VALUE){
i ++;
}
log.info("线程结束:{}",i);
}, "t1");
t1.start();
log.info("打断开始");
t1.interrupt();
log.info("t1的打断标记:{}",t1.isInterrupted());
}
日志如下
21-05-11 21:20:30.758 [INFO][t1] i.k.e.c.e.InterruptRunningDemo : 线程启动
21-05-11 21:20:30.857 [INFO][main] i.k.e.c.e.InterruptRunningDemo : 打断开始
21-05-11 21:20:30.857 [INFO][main] i.k.e.c.e.InterruptRunningDemo : t1的打断标记:true
21-05-11 21:20:31.353 [INFO][t1] i.k.e.c.e.InterruptRunningDemo : 线程结束:2147483647
而在下面的代码中,线程"t1"会检查自己的打断标记,如果打断标记为真,则会立即退出循环
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.info("线程启动");
long i = 0;
while (i < Integer.MAX_VALUE){
if (Thread.currentThread().isInterrupted()){
break;
}
i ++;
}
log.info("线程结束:{}",i);
}, "t1");
t1.start();
TimeUnit.MILLISECONDS.sleep(100);
log.info("打断开始");
t1.interrupt();
log.info("t1的打断标记:{}",t1.isInterrupted());
}
日志如下
21-05-11 21:23:36.782 [INFO][t1] i.k.e.c.e.InterruptRunningDemo : 线程启动
21-05-11 21:23:36.881 [INFO][main] i.k.e.c.e.InterruptRunningDemo : 打断开始
21-05-11 21:23:36.881 [INFO][main] i.k.e.c.e.InterruptRunningDemo : t1的打断标记:true
21-05-11 21:23:36.881 [INFO][t1] i.k.e.c.e.InterruptRunningDemo : 线程结束:104598873
打断park线程
当线程进入park状态时,如果其它线程调用了该线程的interrupt()
方法,则会使线程解除park状态并继续运行。此时该线程的打断标记为真,之后的park()
方法在打断标记为真时会失效。可以通过interrupted()
方法清除打断标记。
两阶段中止模式
针对的问题:如果在一个线程"t1"中【优雅】地中止线程"t2"?【优雅】指的是给线程"t2"一个料理后事的机会。
错误的做法
- 调用
stop()
方法:stop()
方法会真正厦时线程,如果线程运行中锁住了共享资源,那么它被杀死后就再也没有机会释放锁(并且stop()
方法已经被列为废弃方法) - 调用
System.exit()
方法:会停止整个进程
正确的做法
注:如果在应用逻辑中存在阻塞逻辑,则需要在阻塞逻辑被打断时设置打断标记。
代码示例
public class TwoPhaseTermination {
public static class App{
Thread app;
public void start(){
app = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
if (current.isInterrupted()) {
log.debug("执行退出逻辑");
break;
}
try {
log.debug("执行任务(上)");
log.debug("陷入阻塞");
TimeUnit.MILLISECONDS.sleep(400);
log.debug("执行任务(下)");
} catch (InterruptedException e) {
log.warn("阻塞时被打断");
current.interrupt();
}
}
}, "app");
app.start();
}
public void stop(){
app.interrupt();
}
}
public static void main(String[] args) throws InterruptedException {
App app = new App();
app.start();
TimeUnit.SECONDS.sleep(1);
app.stop();
}
}
运行日志如下
21-05-11 21:56:28.529 [DEBUG][app] i.k.e.c.p.TwoPhaseTermination$App : 执行任务(上)
21-05-11 21:56:28.531 [DEBUG][app] i.k.e.c.p.TwoPhaseTermination$App : 陷入阻塞
21-05-11 21:56:28.933 [DEBUG][app] i.k.e.c.p.TwoPhaseTermination$App : 执行任务(下)
21-05-11 21:56:28.933 [DEBUG][app] i.k.e.c.p.TwoPhaseTermination$App : 执行任务(上)
21-05-11 21:56:28.933 [DEBUG][app] i.k.e.c.p.TwoPhaseTermination$App : 陷入阻塞
21-05-11 21:56:29.333 [DEBUG][app] i.k.e.c.p.TwoPhaseTermination$App : 执行任务(下)
21-05-11 21:56:29.333 [DEBUG][app] i.k.e.c.p.TwoPhaseTermination$App : 执行任务(上)
21-05-11 21:56:29.333 [DEBUG][app] i.k.e.c.p.TwoPhaseTermination$App : 陷入阻塞
21-05-11 21:56:29.528 [WARN][app] i.k.e.c.p.TwoPhaseTermination$App : 阻塞时被打断
21-05-11 21:56:29.528 [DEBUG][app] i.k.e.c.p.TwoPhaseTermination$App : 执行退出逻辑
不推荐使用的方法
这些方法已经过时,容易破坏同步代码块,造成线程死锁
方法名 | 功能 |
---|---|
stop() | 停止线程运行 |
suspend() | 挂起(暂停)线程运行 |
resume() | 恢复线程运行 |
守护线程
Java的线程可以分为守护线程和非守护线程。顾名思义,守护线程是用来保护其他线程执行的。当Java进程中只有守护进程时,即使守护线程的代码没有执行完,也会强制结束。
主函数线程为非守护线程。我们代码中创建的线程默认也为非守护线程。除此之外的线程(例如垃圾回收线程)均为守护线程。
通过setDaemon()
方法可以设置该线程是否为守护线程。
代码示例
代码中,线程"t1"被设置为了守护线程,它本身的逻辑为被打断时才会退出循环,结束自身。但是在主线程中并没有打断线程"t1",而是在睡眠1秒后直接退出。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
break;
}
}
log.info("结束");
}, "t1");
t1.setDaemon(true);
t1.start();
TimeUnit.SECONDS.sleep(1);
log.info("结束");
}
打印日志如下
21-05-12 15:28:24.860 [INFO][main] i.k.e.c.e.DaemonDemo : 结束
程序在启动1秒后直接退出,说明守护线程"t1"被强制中止。
注:Tomcat中的Acceptor和Poller线程都是守护线程,因此Tomcat接收到shutdown命令后,不会等待它们处理完当前请求。