Java 线程
项目配置
pom.xml
xml
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
logback.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration
xmlns="http://ch.qos.logback/xml/ns/logback"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback logback.xsd">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date{HH:mm:ss} [%t] %logger - %m%n</pattern>
</encoder>
</appender>
<logger name="c" level="debug" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>
<root level="ERROR">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
多线程概述
进程与线程
程序:一组静态的指令集
进程:程序运行的一个实例
线程:进程中的一个执行单元
并行与并发
并发 concurrent:单个核心通过任务调度器,线程轮流使用 CPU
并行 parallel:多个核心同时运行
同步与异步
同步:需要等待返回
异步:不需要等待返回
I/O 操作不占用 CPU
创建线程
Thread 类
java
@Slf4j(topic = "c.App")
public class App {
public static void main(String[] args) {
Thread t = new Thread(() -> log.info("Thread t"));
t.setName("t");
t.start();
log.info("Thread main");
}
}
Runable 接口
将任务和线程分离,代码更灵活
组合优于基础
java
@Slf4j(topic = "c.App")
public class App {
public static void main(String[] args) {
Runnable func = () -> log.info("Thread t");
Thread t = new Thread(func, "t");
t.start();
log.info("Thread main");
}
}
Callable 接口 & FutureTask 类
java
@Slf4j(topic = "c.App")
public class App {
public static void main(String[] args)
throws ExecutionException, InterruptedException {
FutureTask<Integer> task = new FutureTask<>(() -> {
log.info("Thread t running");
Thread.sleep(1000);
return 114514;
});
new Thread(task, "t").start();
Integer res = task.get();
log.info("Thread main - {}", res);
}
}
线程运行的原理
栈与栈帧
每个线程都有一个栈,栈内有多个栈帧,每个栈帧对定一个方法。
每个线程只有一个活动帧(栈顶的栈帧),对应正在执行的方法。
线程上下文切换
导致 cpu 切换运行其他线程的原因:
- 线程时间片用完
- 垃圾回收
- 有更高优先级进程
- 线程调用了 sleep、wait 等方法
线程上下文切换时需要保存进程状态,并恢复另一个线程状态。
Thread 类方法
API
方法 | 说明 |
---|---|
public void start() | 启动一个新线程,Java虚拟机调用此线程的 run 方法 |
public void run() | 线程启动后调用的方法 |
public void setName(String name) | 给当前线程取名字 |
public void getName() | 获取当前线程的名字 默认名称:子线程是 Thread-索引,主线程是 main |
public static Thread currentThread() | 获取当前线程对象,代码在哪个线程中执行 |
public static void sleep(long time) | 让当前线程休眠多少毫秒再继续执行 Thread.sleep(0) : 让操作系统立刻重新进行一次 CPU 竞争 |
public static native void yield() | 提示线程调度器让出当前线程对 CPU 的使用 |
public final int getPriority() | 返回此线程的优先级 |
public final void setPriority(int priority) | 更改此线程的优先级,常用 1 5 10 |
public void interrupt() | 中断这个线程,异常处理机制 |
public static boolean interrupted() | 判断当前线程是否被打断,清除打断标记 |
public boolean isInterrupted() | 判断当前线程是否被打断,不清除打断标记 |
public final void join() | 等待这个线程结束 |
public final void join(long millis) | 等待这个线程死亡 millis 毫秒,0 意味着永远等待 |
public final native boolean isAlive() | 线程是否存活(是否运行完毕) |
public final void setDaemon(boolean on) | 将此线程标记为守护线程或用户线程 |
start() 和 run()
start()
:用于启动线程,不能被多次调用。线程启动前状态为 new,启动后变成 runable
run()
:线程启动后执行的代码
sleep()
- 睡眠当前线程,将当前线程状态将 running 变成 timed_waiting(阻塞状态)
- 其它线程可以使用
interrupt()
方法打断正在睡眠的线程,这时sleep()
方法会抛出InterruptedException
异常 - 建议使用
TimeUnit.SECOND.sleep()
代替Thread.sleep()
,提高可读性
在没有利用 cpu 来计算时,不要让 while(true) 空转浪费 cpu ,可以使用 yield() 或 sleep() 来让出 cpu 的使用权给其他程序。
yield()
- 让出当前线程 CPU 使用权,将当前线程状态将 running 变成 runable(就绪状态)
- 具体实现依赖于操作系统的任务调度区
join()
等待调用的线程结束,可以用于线程同步
java
@Slf4j(topic = "c.App")
public class App {
static int num = 0;
public static void main(String[] args) throws InterruptedException {
log.info("before start - {}", num);
Thread t = new Thread(() -> num = 1, "t");
t.start();
log.info("after start - {}", num);
t.join();
log.info("after join - {}", num);
}
}
17:18:57 [main] c.App - before start - 0
17:18:57 [main] c.App - after start - 0
17:18:57 [main] c.App - after join - 1
park()
使线程进入阻塞状态,类似于断点。遇到 interrupt() 会打断 park()
java
@Slf4j(topic = "c.App")
public class App {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
log.debug("before park()");
LockSupport.park();
log.debug("after park()");
};
Thread t = new Thread(runnable, "t");
t.start();
TimeUnit.SECONDS.sleep(1);
t.interrupt();
}
}
23:45:16 [t] c.App - before park()
23:45:17 [t] c.App - after park()
interrupt()
1、打断阻塞(sleep、wait、join、park)的线程,会清除打断状态。
java
@Slf4j(topic = "c.App")
public class App {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
log.debug("sleep");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
log.debug(e.getMessage());
}
}, "t");
t.start();
// 避免主线程先打断
TimeUnit.SECONDS.sleep(1);
log.info("interrupt");
t.interrupt();
// 等待标记被清除
TimeUnit.SECONDS.sleep(1);
log.info("t.isInterrupted() = {}", t.isInterrupted());
}
}
21:21:47 [t] c.App - sleep
21:21:48 [main] c.App - interrupt
21:21:48 [t] c.App - sleep interrupted
21:21:49 [main] c.App - t.isInterrupted() = false
2、打断正常运行的线程,会存在打断状态。
java
@Slf4j(topic = "c.App")
public class App {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
log.debug("while()");
// 使用打断标记停止线程
while (!Thread.currentThread().isInterrupted()) ;
}, "t");
t.start();
// 避免主线程先打断
TimeUnit.SECONDS.sleep(1);
log.info("interrupt");
t.interrupt();
log.info("t.isInterrupted() = {}", t.isInterrupted());
}
}
21:21:14 [t] c.App - while (true)
21:21:15 [main] c.App - interrupt
21:21:16 [main] c.App - t.isInterrupted() = true
不推荐使用的 API
- stop():停止线程运行
- suspend():挂起(暂停)线程运行
- resume():恢复线程运行
如果线程持有锁,那么没有机会释放锁。
两阶段终止模式
错误思路
stop()
方法会直接停止线程。如果线程持有锁,那么没有机会释放锁。
流程分析
mermaid
graph TD
w("while(true)")-->a
a("有没有被打断?")--是-->b(料理后事)
b-->c((结束循环))
a--否-->d(睡眠 2s)
d--无异常-->e(执行监控记录)
d--有异常-->i(设置打断标记)
i-->w
e-->w
代码实现
java
@Slf4j(topic = "c.App")
public class App {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
while (true) {
Thread thread = Thread.currentThread();
// 监控资源
log.debug("running");
// 判断打断标记
if (thread.isInterrupted()) {
// 释放占用资源
log.debug("exit...");
break;
}
// 避免大量 CPU 占用
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
thread.interrupt();
}
}
};
Thread t = new Thread(runnable, "t");
t.start();
TimeUnit.SECONDS.sleep(3);
t.interrupt();
}
}
守护线程
守护线程:只要非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
垃圾回收器线程就是一个守护线程。
sql
@Slf4j(topic = "c.App")
public class App {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
while (true) {
log.debug("test");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
Thread t = new Thread(runnable, "t");
t.setDaemon(true);
t.start();
Thread.sleep(1000);
log.debug("main stop");
}
}
23:59:33 [t] c.App - test
23:59:33 [t] c.App - test
23:59:33 [t] c.App - test
23:59:34 [t] c.App - test
23:59:34 [t] c.App - test
23:59:34 [main] c.App - main stop
线程的状态
五种状态
从操作系统层面描述
- 初始状态:创建了线程对象
- 可运行状态(就绪状态):线程对象与操作系统线程关联,可以被调度
- 运行状态:线程正在被执行
- 阻塞状态:线程调用了阻塞 API
- 终止状态:线程已经执行结束
六种状态
根据 Java 中 Thread.State 枚举类划分
- new:线程被创建,未调用
start()
方法 - runnable:运行状态 + 可运行状态 + 操作系统的阻塞状态(BIO)
- terminated:终止状态
- blocked:等待 synchronized() 获取锁
- waiting:等待 join()
- timed_waiting:有时限的阻塞状态,等待 sleep()