并发编程②Java线程
Java 线程
1、Thread
Thread 类实现了 Runnable 接口。
1.1、创建线程
通常以匿名内部类的形式创建线程,且可以用 lambda 表达式简化。
建议为每个线程指定线程名。
1.1.1、Thread
创建 Thread 匿名内部类,实现 run() 方法。
Thread t1 = new Thread("t1") {
@Override
public void run() {
System.out.println("hello");
}
};
1.1.2、Runnable & Thread(!)
将线程和任务分开创建()
-
任务:创建 Runnable 匿名内部类,实现 run() 方法
-
线程:创建 Thread 类,参数依次为 Runnable 任务,线程名。
// 任务 Runnable target = new Runnable() { @Override public void run() { System.out.println("hello"); } }; // 线程 Thread t2 = new Thread(target, "t2");
lambda 简化
// 简化
target = () -> System.out.println("hello");
t2 = new Thread(target, "t2");
// 更简化
t2 = new Thread(() -> System.out.println("hello"), "t2");
浅看源码:run()
private Runnable target;
@Override
public void run() {
if (target != null) {
target.run();
}
}
target 是 Runnable 类型的成员变量。
- target 非空:创建线程时传入了 Runnable 类型参数,执行 Runnable 的方法。
- target 为空:创建线程时没有传 Runnable 参数,而是实现了 run() 方法(实际上是重写)
结论:无论如何,Thead 都会执行我们编写的 run() 方法逻辑。
1.1.3、FutureTask & Thread
FutureTask 用于处理有返回结果的情况
也将线程与任务分开创建(的体现)
-
FutureTask 类实现了 RunnableFuture 接口,间接实现了 Runnable 接口。
-
因此,也可以作为 Thread 的构造方法参数传入。
public class FutureTaskimplements RunnableFuture { ... } public interface RunnableFuture extends Runnable, Future { void run(); }
Callable 接口
FutureTask 在实例化时需要传入一个 Callable 类型的参数。
Callable 与 Runnable 相似,区别在于 Callable 具有返回值。
@FunctionalInterface
public interface Callable {
V call() throws Exception;
}
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
示例
泛型 FutureTask 指定方法返回值类型。
Callable callable = new Callable() {
@Override
public String call() throws Exception {
return "hello";
}
};
FutureTask task = new FutureTask<>(callable);
Thread t3 = new Thread(task, "t3");
t3.start();
// 主线程种获取task执行结果
String result = task.get();
1.2、查看进程、线程
1.2.1、Java
jps命令:查看所有 Java 进程。jstack:查看指定 Java 进程的所有线程。命令 jconsole:查看指定 Java 进程中线程的运行情况(可视化界面)
1.2.2、Windows
- 任务管理器
tasklist命令:查看所有进程taskkill命令:杀进程
1.2.3、Linux
ps -fe:查看所有进程。ps -fT -p:查看指定进程的所有线程。top -H -p:查看指定进程的所有线程。kill:杀进程。
1.3、线程运行原理
涉及 JVM 知识
- :虚拟机栈、程序计数器
1.3.1、虚拟机栈
- 虚拟机栈: 每个线程运行时所需的内存,由多个栈帧(Frame)组成。
- 栈帧:对应每个方法调用时所占内存。
- 存储局部变量表、操作数栈、动态链接、方法出口等信息
- 每个线程只能有一个活动栈帧,对应当前正在执行的方法。
1.3.2、线程上下文切换
Thread Context Switch
内核在 CPU 上对线程进行切换,即任务切换
可能的原因
- 线程的 CPU 时间片用完(回顾 ,线程轮流执行)
- :GC 线程会触发 STW(stop-the-world),暂停所有用户线程。
- 有更高优先级的线程需要运行。
- 线程自身调用
sleep、yield、wait、join、park、synchronized、lock等方法。
当 Context Switch 发生时,操作系统需要保存当前线程的状态,并恢复另一个线程的状态。
状态:包括程序计数器、每个栈帧的信息等
对应 Java 的程序计数器:
- 若当前执行的是 JVM 的方法,程序计数器保存当前执行指令的地址。
- 若当前执行的是 native 方法,程序计数器为空。
Context Switch 频繁发生会影响性能。
1.4、守护线程
通常 Java 进程会等待所有线程结束后停止。
守护线程(daemon thread):所有其它非守护线程结束时,守护进程随即停止(即使还有代码没执行完)
举例
- 垃圾回收线程
- Tomcat 中的 Acceptor 和 Poller 线程
2、Thread API 常用方法
2.0、获取属性
getId():获取线程 ID(ID 具有唯一性)getName():获取线程名setName(String):修改线程名getState():获取线程状态- Java 规定 6 个线程状态,以枚举类的形式表示。
NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED
isAlive():判断线程是否存活(线程执行完任务则死亡)currentThread():static,表示当前线程。
2.1、start & run
说明
- start():启动一个新线程(进入就绪状态)。
- run():线程被调度后,执行任务。
示例:调用 start() 和 run() 的区别
-
线程 t1:先休眠 2 秒,再打印输出语句。
-
主线程:分别调用 start() 和 run(),方法调用后在主线程打印输出语句。
private static final Logger logger = Logger.getLogger(Join.class); public static void foo() { Thread t1 = new Thread(() -> { try { LogUtils.debug("sleeping..."); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } LogUtils.debug("run()执行"); }, "t1"); // 调用两个方法之一 // t1.start() // t1.run() LogUtils.debug("主线程方法执行"); }
结果
-
start()
-
启动线程 t1,由线程 t1 执行 run() 方法。
-
run() 方法异步调用(主线程无需等待 run() 执行结束)

-
-
run()
-
没有启动线程 t1,主线程直接调用 run() 方法。
-
run() 方法同步调用(主线程需等待 run() 执行结束)

-
2.2、sleep & yield
2.2.1、sleep
sleep() 是静态本地方法,抛出
InterruptedException异常。
作用:使当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 睡眠中的线程:被打断时抛出
InterruptedException异常(之后讲解) - 线程睡眠结束:进入就绪状态。
JUC 的 TimeUnit 类
可以理解为 :每个枚举相当于一个状态,执行当前状态的不同操作。
-
枚举类:定义了不同时间单位的枚举(纳秒、微秒、毫秒、秒、分钟、小时、天)
-
该类内部定义了基于时间的使用方法,不同枚举对应的方法实现不同。
-
建议使用 TimeUnit 的
sleep()代替 Thread 的sleep(),可读性高。Thread.sleep(1); Thread.sleep(1000); Thread.sleep(1000 * 60); Thread.sleep(1000 * 60 * 60); // 可读性更高 TimeUnit.MILLISECONDS.sleep(1); TimeUnit.SECONDS.sleep(1); TimeUnit.MINUTES.sleep(1); TimeUnit.HOURS.sleep(1);
工具类
若频繁使用 sleep() 功能,建议封装工具类,提前捕获方法异常。
public class SleepUtils {
public static void sleepSeconds(int n) {
Thread thread = Thread.currentThread();
try {
TimeUnit.SECONDS.sleep(n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void sleepMillis(int n) {
Thread thread = Thread.currentThread();
try {
TimeUnit.MILLISECONDS.sleep(n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.2.2、yield
yield() 是静态本地方法
使当前线程从 Running 进入 Runnable (就绪),让出 CPU 时间片
- 调用 yield() 的线程可能又抢到了时间片。
- 可能没有其它线程,yield() 无效。
2.3、优先级
prioritiy:提示(hint)调度器优先调度该线程
-
仅仅是一个提示,调度器可以忽略它
-
优先级高的线程,有较高几率获得 CPU 时间片
-
范围:1 到 10 的整数,默认 5。
-
获取:
getPriority() -
设置:
setPriority(int)private int priority; public final static int MIN_PRIORITY = 1; public final static int NORM_PRIORITY = 5; public final static int MAX_PRIORITY = 10; public final int getPriority() { return priority; } public final void setPriority(int newPriority) { ... }
2.4、join
2.4.1、说明
join() 抛出
InterruptedException异常
-
重载方法
join(long):等待线程死亡,指定最大等待毫秒数,0 表示永远等待。join():等待,直到线程死亡。
public final synchronized void join(long millis) throws InterruptedException { ... } public final void join() throws InterruptedException { join(0); } -
join 状态的线程被打断时,抛出
InterruptedException异常。 -
可读性:TimeUnit 类的
timedJoin(Thread, long)方法
(注意:此方法的0表示不等待,与 Thread 的 join() 有所区别)
2.4.2、场景
没有 join() 的情况
以下代码打印 r = 0,而不是 20。
static int r = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
SleepUtils.sleepSeconds(2);
r = 20;
}, "t1");
t1.start();
System.out.println("r = " + r);
}
如果不使用 join(),可以通过让主线程 sleep() 的方式来等待 t1 线程执行结束。
缺点:不确定 t1 线程的任务执行多久,无法提前确定睡眠时间。
使用 join()
主线程等待 t1 线程死亡之后才执行。
static int r = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
SleepUtils.sleepSeconds(2);
r = 20;
}, "t1");
t1.start();
try {
TimeUnit.SECONDS.timedJoin(t1, 0);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("r = " + r);
}
2.5、interrupt
Java 中断机制:设置中断标志以提示线程 t 即将被中断,由 t 自身进行中断。
(而不是直接中断)
2.5.1、中断
interrupt()
设置中断标志,提示线程即将被中断。
- 正常:中断处于运行(含
park)的线程 - 异常:中断处于
sleep、wait、join的线程 t,会导致 t 抛出InterruptedException异常,并将设置好的中断标记清除。
测试:分别调用 t1() 和 t2(),中断线程并查看中断标志。
主线程需休眠一会,否则与线程 t 并发执行,看不到效果。
public static void t1() {
Thread t1 = new Thread(() -> {
while (true) {
}
}, "t1");
t1.start();
SleepUtils.sleepMillis(10);
// 中断
t1.interrupt();
LogUtils.debug(t1.isInterrupted());
}
public static void t2() {
Thread t2 = new Thread(() -> SleepUtils.sleepSeconds(10), "t2");
t2.start();
SleepUtils.sleepMillis(10);
// 中断
t2.interrupt();
LogUtils.debug(t2.isInterrupted());
}
结果
-
运行的 t1:正常,设置中断标志为 true(告知 t1 即将被打断)

-
睡眠的 t2:异常,清除中断标志(设为 false,非运行状态的线程不需要被打断)

2.5.2、判断是否中断
判断当前线程是否被中断
isInterrupted():不清除中断标志。interrupted():静态方法,会清除中断标志。
源码:二者都调用了带参的本地方法,参数为
ClearInterrupted。
顾名思义,ClearInterrupted 就是清除中断标志。
public boolean isInterrupted() {
return isInterrupted(false);
}
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
private native boolean isInterrupted(boolean ClearInterrupted);
2.5.3、InterruptedException
强调:sleep、wait、join 的线程 t 被中断会抛出 InterruptedException 异常,并清除中断标记。
通常,只要声明为抛出 InterruptedException 的方法,都可以被 interrupt 打断。
2.6、过时方法
废弃方法:存在安全性问题,容易破坏同步代码块。
stop():停止线程suspend():暂停线程resume():恢复线程
3、两阶段终止模式
3.1、说明
Two Phase Termination(Java 中断机制)
简单来说,不是直接中断某个线程,而是 “优雅” 地中断。
- 设置中断标志,以提示线程 t 即将被中断。
- 线程 t 判断中断标志,得知即将被中断,处理完相关工作后终止(料理后事,比如释放锁)。
实现方式
本文先介绍第一种方式,之后会讲解第二种。
- 利用
isInterrupted() volatile+ 停止标记
3.2、isInterrupted() 实现
3.2.1、流程分析
案例:每 1 秒打印一次输出语句(或是打印当前时间、处理业务等)
流程如下:开始循环
-
判断中断标志:即
isInterruptrd()判断是否被打断- 是:处理后事(保存结果、释放锁等),结束线程
- 否:执行任务
-
执行任务:执行结束后睡眠 1 秒,捕获睡眠期间的异常
- 正常:开始下一轮循环
- 异常:睡眠线程被打断会清除中断标志,需要重新设置中断标志,进入下一轮循环
-
判断中断标志为 true,处理后事并结束线程。

3.2.2、实现
获取当前线程,进入循环
-
判断中断标志:若被打断则处理后事并结束进程。
-
执行任务:执行结束后睡眠 1 秒,捕获睡眠期间的异常
-
睡眠期间被中断:中断标志被清除,需要重新设置中断标志。
public class TwoPhaseTermination { private Thread thread; public void turnOn() { thread = new Thread(() -> { Thread current = Thread.currentThread(); while (true) { if (current.isInterrupted()) { // 模拟:处理后事 LogUtils.debug("terminating..."); // 处理完毕后退出 LogUtils.debug("Exit"); break; } // 执行任务 LogUtils.debug("working..."); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { // 设置中断标志 current.interrupt(); e.printStackTrace(); } } }, "t"); thread.start(); } public void turnOff() { thread.interrupt(); } }
3.3.3、main() 测试
在主方法中进行测试
开启线程,5 秒后关闭线程。
public static void main(String[] args) {
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.turnOn();
SleepUtils.sleepSeconds(5);
tpt.turnOff();
}

3.3.4、JUnit 测试(!)
在 JUnit 中进行测试
效果相同
@Test
public void test() {
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.turnOn();
SleepUtils.sleepSeconds(5);
tpt.turnOff();
}
但是,JUnit 测试多线程存在一个问题:system.exit()
将以上代码中的 sleep(5) 去掉,方法会直接结束,线程 t1 不会按预期运行。

分析:
org.junit.runner.JUnitCore
JUnitCore 类是 JUnit 测试的启动者,所有的 JUnit 测试都从 JUnit 的主方法开始。
public static void main(String... args) {
Result result = (new JUnitCore()).runMain(new RealSystem(), args);
System.exit(result.wasSuccessful() ? 0 : 1);
}
大致理解:
-
执行了测试代码的逻辑,获得执行结果。
-
判断是否成功执行,执行
System.exit()-
作用:终止当前虚拟机。
-
参数:0 表示正常终止,非零表示异常终止。
-
结论
- JUnit 执行完测试代码逻辑后,虚拟机被终止,线程 t1 也被强制终止。
- 加入了
sleep(5),主线程会休眠 5 秒,休眠结束后才会执行测试方法中接下来的逻辑。
4、统筹应用:洗壶烧水泡茶
4.1、说明
案例:泡茶,两个人负责前期工作
要求工作总耗时最短。
-
工作:洗水壶(1min)、茶壶(1min)、茶杯(1min)、拿茶叶(1min)、烧开水(10min)。
-
工序:洗水壶完成后才能烧开水,所有工序完成才能泡茶。
-
思路:使用 2 个线程模拟 2 个人,使用
sleep()模拟工作耗时。-
线程 t1:洗水壶、烧开水(有序)
-
线程 t2:洗茶壶茶杯、拿茶叶(无序)
-
线程 t1 或 t2 泡茶。

-
注意
- 线程 t1 开始烧水后也可以做 t2 的部分工作,甚至可以一个线程完成,此处仅探讨两个线程的简单情况。
- 本题有多种解法:join、wait/notify、第三者协调。
4.2、join 实现
t2 使用 join() 等待 t1 执行结束
private static void makeTea() {
Thread t1 = new Thread(() -> {
LogUtils.debug("洗水壶");
SleepUtils.sleepSeconds(1);
LogUtils.debug("烧开水");
SleepUtils.sleepSeconds(10);
}, "t1");
Thread t2 = new Thread(() -> {
LogUtils.debug("洗茶壶茶杯、拿茶叶");
SleepUtils.sleepSeconds(3);
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
LogUtils.debug("泡茶");
}, "t2");
t1.start();
t2.start();
}
结果
