Java线程
3.1 创建和运行线程
方法一,直接使用Thread
import lombok.extern.slf4j.Slf4j;
/**
* 使用匿名内部类创建线程
* @author xc
* @date 2023/4/30 16:19
*/
@Slf4j
public class Test1 {
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run() {
log.debug("开启的新线程");
}
};
thread.setName("子线程");
thread.start();
log.debug("main线程");
}
}
方法二,Runable
import lombok.extern.slf4j.Slf4j;
/**
* Runnable创建线程
* @author xc
* @date 2023/4/30 16:26
*/
@Slf4j
public class Test2 {
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
log.debug("子线程运行");
}
};
Thread thread = new Thread(r);
thread.setName("子线程");
thread.start();
log.debug("主线程运行");
}
}
看到有该注解,表示该接口是函数式注解
就可以使用Lambda表达式简化代码
import lombok.extern.slf4j.Slf4j;
/**
* Runnable创建线程
* @author xc
* @date 2023/4/30 16:26
*/
@Slf4j
public class Test2 {
public static void main(String[] args) {
Runnable r = () -> log.debug("子线程运行");
Thread thread = new Thread(r);
thread.setName("子线程");
thread.start();
log.debug("主线程运行");
}
}
小结
- 方法一是把线程和任务合并在一起,方法二是把线程和任务分开
- 用Runnable更容易与线程池等高级API配合
- 用Runnable让任务脱离了Thread继承体系,更灵活
方法三,FutureTask和Thread配合
实现
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @author xc
* @date 2023/4/30 16:43
*/
@Slf4j
public class Test3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("子线程运行");
//打印完休息1s
Thread.sleep(1000);
return 100;
}
});
Thread t = new Thread(task);
t.start();
log.debug("主线程运行");
// 阻塞一直等到结果返回
log.error("子线程返回结果是"+task.get());
}
}
结果
3.2 观察多个线程同时运行
- 交替执行
- 谁先谁后,不由我们控制
代码
import lombok.extern.slf4j.Slf4j;
/**
* @author xc
* @date 2023/4/30 16:49
*/
@Slf4j
public class Test4 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
for (int i = 0; i < 100; i++) {
log.debug("子线程{}",i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
for (int i = 0; i < 100; i++) {
log.debug("主线程{}",i);
Thread.sleep(500);
}
}
}
结果
3.3 查看进程线程的方法
Windows
- 任务管理器
- tasklist 查看进程
- taskkill 杀死进程
Linux
- ps -ef 查看所有进程
- ps -fT -p 查看某个进程的所有线程
- kill 杀死进程
- top 按大写H切换是否显示线程
- top -H -P 查看某个进程的所有线程
Java
- jps 命令查看所有Java进程
- jstack 查看某个Java进程的所有线程状态
- jconsole 来查看某个Java进程中线程的运行情况(图形界面)
3.4 原理之线程运行
栈与栈帧
我们都知道JVM由堆、栈、方法区组成,其中栈内存是给谁用的呢?每个线程启动后,虚拟机会自动分配一块栈内存
- 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动的栈帧,对于这当前正在执行的方法
栈帧图解
多线程中每个线程有自己的栈内存,又有多个栈帧,互不干扰
主线程
子线程
线程上下文切换
因为以下一些原因导致cpu不再执行当前线程,转而执行另一个线程的代码
- 线程的cpu时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了sleep、yield、wait、join,park,synchronized、lock方法
当Context Switch发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,java中对应的就是程序计数器,它的作用就是记住下一条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唯一 | |
getName() | 获取线程名 | ||
setName() | 修改线程名 | ||
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() | static | 判断当前线程是否被打断 | 会清除打断标记 |
currentThread() | static | 获取当前正在执行的线程 | |
sleep(long n) | static | 让当前执行的线程休眠n毫秒,休眠时让出cpu的时间片给其它线程 | |
yield() | static | 提示线程调度器让出当前线程对cpu的使用 | 主要是为了测试和调试 |
3.6 start与run
直接调用run方法跟调用普通方法一样
测试
@Slf4j
public class Test5 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("runing");
});
thread.run();
log.debug("主线程运行");
}
}
结果
调用start
线程状态
/**
* @author xc
* @date 2023/4/30 19:48
*/
@Slf4j
public class Test6 {
public static void main(String[] args) {
Thread t = new Thread(()->log.debug("子线程运行"));
log.debug("线程状态{}",t.getState());
t.start();
log.debug("线程状态{}",t.getState());
}
}
不能调用两次start
3.7 sleep与yield
sleep
- 调用sleep会让当前线程从Running进入Timed Waiting状态(阻塞)
/**
* @author xc
* @date 2023/4/30 19:54
*/
@Slf4j
public class Test7 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
thread.start();
Thread.sleep(5000);
log.debug("子线程状态{}",thread.getState());
}
}
- 其它线程可以使用interrupt方法打断正在睡眠的线程,这是sleep方法会抛出InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性
TimeUnit.SECONDS.sleep(5);
yield(礼让)
- 具体yield会让当前线程从Running进入Runnable就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器
线程优先级
- 线程优先级提示调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但cpu闲时,优先级几乎没有用
案例-防止CPU占用100%
sleep实现
- 在没有利用cpu来计算时,不要让while(true)空转浪费cpu,这时可以使用yield或sleep来让出cpu的使用权给其他程序
while(true){
try{
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 可以用wait或条件变量达到类似的效果
- 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
- sleep适用于无需锁同步的场景
3.8 join方法详解
为什么需要join
下面代码执行,打印r是什么?
import lombok.extern.slf4j.Slf4j;
/**
* @author xc
* @date 2023/5/5 9:57
*/
@Slf4j
public class Test10 {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
log.debug("开始");
Thread t = new Thread(()->{
log.debug("开始");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("结束");
r = 10;
});
t.start();
log.debug("结果为:{}",r);
log.debug("结束");
}
}
分析
- 因为主线程和线程t1是并行执行的,t1线程需要1秒之后才能算出r=10
- 而主线程一开始就要打印r的结果,所以只能打印出r=0
解决方法
-
用sleep行不行,为什么?
-
- 可以,但是你不知道一个线程要运行多长时间
-
用join,加在t1.start()之后即可
-
- 等待某个线程运行结束后才继续向下运行
应用之同步
以调用方角度来讲,如果
- 不要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
等待多个结果
问,下面代码cost大约多少秒?
import lombok.extern.slf4j.Slf4j;
import static java.lang.Thread.sleep;
/**
* @author xc
* @date 2023/5/5 9:57
*/
@Slf4j
public class Test10 {
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test2();
}
private static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
r1 = 10;
});
Thread t2 = new Thread(() -> {
try {
sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
r2 = 20;
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
}
结果
有时效的join
等够时间
import lombok.extern.slf4j.Slf4j;
import static java.lang.Thread.sleep;
/**
* @author xc
* @date 2023/5/5 9:57
*/
@Slf4j
public class Test10 {
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test2();
}
private static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
r1 = 10;
});
long start = System.currentTimeMillis();
t1.start();
t1.join(2000);
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
}
结果
没等够时间
import lombok.extern.slf4j.Slf4j;
import static java.lang.Thread.sleep;
/**
* @author xc
* @date 2023/5/5 9:57
*/
@Slf4j
public class Test10 {
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test2();
}
private static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
r1 = 10;
});
long start = System.currentTimeMillis();
t1.start();
t1.join(500);
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
}
结果
3.9 interrupt方法详解
打断sleep,wait,join的线程
这几个方法都会让线程进入阻塞状态
打断sleep的线程,会清空打断状态,sleep为例
import lombok.extern.slf4j.Slf4j;
/**
* @author xc
* @date 2023/5/5 10:34
*/
@Slf4j
public class Test11 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t.start();
Thread.sleep(1000);
t.interrupt();
log.debug("打断标记{}",t.isInterrupted());
}
}
结果
打断正常运行的线程
import lombok.extern.slf4j.Slf4j;
/**
* @author xc
* @date 2023/5/5 10:34
*/
@Slf4j
public class Test11 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while (true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if (interrupted) {
log.debug("被打断");
break;
}
}
});
t.start();
Thread.sleep(1000);
t.interrupt();
log.debug("打断标记{}",t.isInterrupted());
}
}
结果
两阶段终止模式
Two Phase Termination
在一个线程T1中如何“优雅”终止线程T2?这里的“优雅”指的是给T2一个料理后事的机会
1.错误思路
-
使用线程对象的stop方法停止线程
-
- stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它下次你将永远无法获取锁
-
使用System.exit(int) 方法停止线程
-
- 目的仅停止一个线程,但这种做法会让整个程序停止
2.两阶段终止模式
import lombok.extern.slf4j.Slf4j;
/**
* @author xc
* @date 2023/5/5 11:02
*/
@Slf4j
public class Test12 {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination t = new TwoPhaseTermination();
t.start();
Thread.sleep(10000);
t.stop();
}
}
@Slf4j
class TwoPhaseTermination{
private Thread monitor;
public void start(){
monitor = new Thread(()->{
while (true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if (interrupted) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(2000);
log.debug("记录日志");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
});
monitor.start();
}
public void stop(){
monitor.interrupt();
}
}
结果
打断park线程
打断park线程,不会清空打断状态
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
/**
* @author xc
* @date 2023/5/5 11:16
*/
@Slf4j
public class Test13 {
public static void main(String[] args) {
Thread t = new Thread(()->{
log.debug("park");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}",Thread.interrupted());
LockSupport.park();
log.debug("unpark...");
});
t.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
t.interrupt();
}
}
结果
3.10 不推荐的方法
方法名 | static | 功能说明 |
---|---|---|
stop() | 停止线程运行 | |
suspend() | 挂起(暂停)线程运行 | |
resume() | 恢复线程运行 |
3.11 主线程与守护线程
默认情况下,Java进程需要等待所有线程运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
log.debug("开始运行...");
sleep(2);
log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1);
log.debug("运行结束...");
注意:
- 垃圾回收线程是一种守护线程
- Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接受到shutdown命令后,不会等待他们处理完当前请求
3.12 五种状态
从操作系统层面来描述
-
初始状态:仅是在语言层面创建了线程对象,还未与操作系统线程关联
-
可运行状态:(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由cpu调度执行
-
运行状态:获取了cpu时间片运行中的状态
-
- 当cpu时间片用完,会从运行状态变成可运行状态,会导致线程上下文切换
-
阻塞状态:
-
- 如果调用阻塞API,如BIO读写文件,这时该线程实际不会用到cpu,会导致线程上下文切换,进入阻塞状态
- 等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至可运行状态
- 与可运行状态的区别是,对阻塞状态的线程来说只要 它们一直不唤醒,调度器就一直不会考虑调度它们
-
终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
3.13 六种状态
- NEW 线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)
- BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
- TERMINATED 当线程代码运行结束
3.14 本章小结
-
线程创建
-
线程重要api,如start、run、sleep、join、interrupt等
-
线程状态
-
应用方面
-
- 异步调用:主线程执行期间,其它线程异步执行耗时操作
- 提高效率:并行计算,缩短运算时间
- 同步等待:join
- 统筹规划:合理使用线程,得到最优效果
-
原理方面
-
- 线程运行流程:栈、栈帧、上下文切换、程序计数器
- Thread两种创建方式源码
-
模式方面
作系统线程关联),可以由cpu调度执行
-
运行状态:获取了cpu时间片运行中的状态
-
- 当cpu时间片用完,会从运行状态变成可运行状态,会导致线程上下文切换
-
阻塞状态:
-
- 如果调用阻塞API,如BIO读写文件,这时该线程实际不会用到cpu,会导致线程上下文切换,进入阻塞状态
- 等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至可运行状态
- 与可运行状态的区别是,对阻塞状态的线程来说只要 它们一直不唤醒,调度器就一直不会考虑调度它们
-
终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
3.13 六种状态
[外链图片转存中…(img-j8AwIAQE-1683460701414)]
- NEW 线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)
- BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
- TERMINATED 当线程代码运行结束
3.14 本章小结
-
线程创建
-
线程重要api,如start、run、sleep、join、interrupt等
-
线程状态
-
应用方面
-
- 异步调用:主线程执行期间,其它线程异步执行耗时操作
- 提高效率:并行计算,缩短运算时间
- 同步等待:join
- 统筹规划:合理使用线程,得到最优效果
-
原理方面
-
- 线程运行流程:栈、栈帧、上下文切换、程序计数器
- Thread两种创建方式源码
-
模式方面
-
- 终止模式之两阶段终止