Java 线程
项目配置
pom.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 version="1.0" encoding="UTF-8"?>
<configurationxmlns="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 类
@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 接口
将任务和线程分离,代码更灵活
组合优于基础
@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 类
@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()
等待调用的线程结束,可以用于线程同步
@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()
@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)的线程,会清除打断状态。
@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、打断正常运行的线程,会存在打断状态。
@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()
方法会直接停止线程。如果线程持有锁,那么没有机会释放锁。
流程分析
代码实现
@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();}
}
守护线程
守护线程:只要非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
垃圾回收器线程就是一个守护线程。
@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()