一、创建和运行线程
程序在启动时,默认就已经存在了一个主线程,如果想要在主线程之外创建线程,有以下几种方式:
1.1 直接使用 Thread
// 创建线程对象
Thread t = new Thread(){
@Override
public void run() {
// 要执行的任务
}
};
// 启动线程
t.start();
例如:
@Slf4j(topic = "c.test")
class test {
public static void main(String[] args) {
// 创建线程对象
Thread t = new Thread(){
@Override
public void run() {
// 要执行的任务
log.debug("running");
}
};
// 启动线程
t.setName("t");
t.start();
log.debug("running");
}
}
输出:
1.2 使用 Runnable 配合 Thread
把【线程】和【任务】(要执行的代码)分开
- Thread 代表线程
- Runnable 可运行的任务(线程要执行的代码)
Runnable r = new Runnable() {
@Override
public void run() {
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread(r);
// 启动线程
t.start();
例如:
@Slf4j(topic = "c.CreateThreadMethod")
public class CreateThreadMethod {
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
// 要执行的任务
log.debug("hello");
}
};
// 创建线程对象
Thread t = new Thread(r);
t.setName("t");
// 启动线程
t.start();
log.debug("hello");
}
}
输出:
分析 Thread 源码,理清 Thread 与 Runnable 之间的关系
public class Thread implements Runnable {
private Runnable target; // 成员变量
// 构造函数
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
g.checkAccess();
/*
* Do we have the required permissions?
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target; // 此处将传递过来的 Runnable 对象赋值给成员变量 target
setPriority(priority);
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
/**
* If this thread was constructed using a separate
* <code>Runnable</code> run object, then that
* <code>Runnable</code> object's <code>run</code> method is called;
* otherwise, this method does nothing and returns.
* <p>
* Subclasses of <code>Thread</code> should override this method.
*
* @see #start()
* @see #stop()
* @see #Thread(ThreadGroup, Runnable, String)
*/
@Override
public void run() {
if (target != null) {
target.run(); // 当 target 不为 null 时,调用 target 中的 run 方法。
}
}
}
继承 Thread 类的方式重写了 run 方法,那么 start 方法最终调用的就是子类中的 run 方法。
实现 Runnable 接口中的 run 方法的方式,通过源码分析发现,最终在调用 run 方法时,调用的也是 Runnable 接口的 run 方法。
小结:
- 方法 1 是把线程和任务合并在了一起,方法 2 是把线程和任务分开了
- 用 Runnable 更容易与线程池等高级 API 配合
- 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
1.3 FutureTask 配合 Thread
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
@Slf4j(topic = "c.FutureTaskTest")
class FutureTaskTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("hello");
return 100;
}
});*/
// 上述代码可使用 lambda 表达式进行简化,如下
FutureTask<Integer> task = new FutureTask<>(() -> {
log.debug("hello");
return 100;
});
Thread t = new Thread(task);
t.start(); // 启动线程,执行线程方法。这里最终调用的是 FutureTask 中的 run 方法
Integer integer = task.get(); // 通过 Future 类中的 get 方法等待获取 task 的执行结果
// 这里的等待意思是指只有 task 返回了结果,等待才会结束。即 task 会一直阻塞在这,直到拿到返回结果。
log.debug("结果是:{}", integer);
}
}
输出:
Runnable 接口本身是无法获取线程的处理结果的,FutureTask 类实现了 RunnableFuture
� 接口,而 RunnableFuture 接口继承了 Runnable 和 Future 这两个类,而 get 方法就来自于 Future 类。
所以 new Thread(task); 这段代码,执行时依然调用的是 Thread 类中的下面的构造函数:
public class Thread implements Runnable {
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
}
那么 start 方法最终调用的 run 方法也就是 FutureTask 中的 run 方法。
public class FutureTask<V> implements RunnableFuture<V> {
private Callable<V> callable;
/** The result to return or exception to throw from get() */
private Object outcome;
// 构造函数
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call(); // 调用 call 方法
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
private V report(int s) throws ExecutionException {
Object x = outcome;
if (s == NORMAL)
return (V)x;
if (s >= CANCELLED)
throw new CancellationException();
throw new ExecutionException((Throwable)x);
}
}
FutureTask 类在 run 方法中调用了 Callable 接口的 call 方法,并将结果赋值给成员变量 outcome。
当我们调用 get() 方法时,最终获取到 outcome 的结果。
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
Callable 接口中只有一个 call() 方法,同时该接口还标注有 @FunctionalInterface 接口,表明该接口可以通过 lambda 表达式进行简化书写 。
1.4 查看进程和线程
Windows
- 任务管理器可以查看进程和线程数,也可以用来杀死进程。
- tasklist 查看进程
- taskkill 杀死进程
Linux
- ps -fe 查看所有进程
- ps -fT -p 查看某个进程(PID)的所有线程
- kill 杀死进程
- top 按大写 H 切换是否显示线程
- top -H -p 查看某个进程(PID)的所有线程
Java
- jps 命令查看所有 Java 进程
- jstack 查看某个 Java 进程(PID)的所有线程状态
- jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
二、线程运行原理
2.1 栈与栈帧
Java Virtual Machine Stacks (Java虚拟机栈)
JVM由堆、栈、方法区所组成,其中栈内存就是给线程用的,每个线程启动后,虚拟机就会为其分配一块栈内存。
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
线程运行原理-栈帧图解
public class TestFrames {
public static void main(String[] args) {
method1(10);
}
private static void method1(int x) {
int y = x + 1;
Object m = method2();
System.out.println(m);
}
private static Object method2() {
Object n = new Object();
return n;
}
}
分析:
main 方法进栈,虚拟机会产生一个 String 的数组对象,然后将其引用地址赋值给 args。main 方法的返回地址即为退出地址。
程序计数器变更下调指令地址,即 method1 方法的指令地址。
method1 方法进栈,在创建栈帧时,将会进行内存分配,局部变量表的创建、返回地址的记录。
开始执行 method1 方法,包括 int y = x + 1;
程序计数器变更下一条指令地址,即 Object m = method2();
method2() 方法进栈,在创建栈帧时,将会进行内存分配,局部变量表的创建、返回地址的记录。
变更程序计数器中的下一条指令的地址。
开始执行代码 Obejct n = new Object();,将堆中的 Object 对象地址赋值给变量 n
变更程序计数器中的下一条指令的地址,return n。
执行 return n;,将 n 的值赋值给 m,释放掉 method2 栈帧
变更程序计数器中的下一条指令的地址,System.out.println(m);。
打印语句执行结束,method1 栈帧释放掉,根据返回地址找到主方法中需要继续执行的位置。
由于主方法中没有代码可继续执行,main 栈帧也将会出栈,程序执行结束。
线程运行原理-多线程
public class TestFrames {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
method1(20);
});
t1.setName("t1");
t1.start();
method1(10);
}
private static void method1(int x) {
int y = x + 1;
Object m = method2();
System.out.println(m);
}
private static Object method2() {
Object n = new Object();
return n;
}
}
设置程序断点如下:
断点模式选择 Thread(右击断点选择)
以 debug 模式运行程序
两个线程相互独立,各自拥有自己的栈内存。
2.2 线程上下文切换(Thread Context Switch)
因为以下一些原因导致 CPU 不再执行当前的线程,转而执行另一个线程的代码。
- 线程的 CPU 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 JVM 指令的执行地址,是线程私有的。
- 状态包括程序计数器、虚拟机栈中的每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch 频繁发生回影响性能
三、常见方法
方法名 | static | 功能说明 | 注意 |
---|---|---|---|
start() | 启动一个新线程,在新的线程运行run 方法中的代码 | start方法只是让线程进入就绪,里面的代码不一定立刻运行(CPU的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现 java.lang.IllegalThreadStateException | |
run() | 新线程启动后会调用的方法 | 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为 | |
join() | 等待线程运行结束 | ||
join(long n) | 等待线程运行结束,最多等待 n 毫秒 | ||
getId() | 获取线程长整型的 id | ID 唯一 | |
getName() | 获取线程名 | ||
setName(String) | 修改线程名 | ||
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.1 start与run
@Slf4j(topic = "c.ThreadMethodTest")
public class ThreadMethodTest {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("running...");
});
t1.run();
log.debug("do other thing...");
}
}
输出:
- 直接调用 run 是在主线程中执行了 run,没有启动新的线程
- 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
start() 方法调用前后线程的状态:
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test5")
public class Test5 {
public static void main(String[] args) {
Thread t1 = new Thread("t1"){
@Override
public void run() {
log.debug("running...");
}
};
System.out.println(t1.getState());
t1.start();
System.out.println(t1.getState());
}
}
运行结果:
start() 方法无法被多次调用,否则会报 IllegalThreadStateException 异常。线程只要变成了 RUNNABLE 状态就不能再 start 了。
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test5")
public class Test5 {
public static void main(String[] args) {
Thread t1 = new Thread("t1"){
@Override
public void run() {
log.debug("running...");
}
};
System.out.println(t1.getState());
t1.start();
t1.start();
System.out.println(t1.getState());
}
}
运行结果:
3.2 sleep 与 yield
sleep
1.调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
@Slf4j(topic = "c.ThreadMethodTest")
public class ThreadMethodTest {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start(); // 启动 t1 线程
log.debug("t1 state {}", t1.getState()); // 此时 t1 线程尚未休眠
try {
Thread.sleep(500); // 主线程休眠 500ms
} catch (InterruptedException e) {
e.printStackTrace();
}
// 主线程休眠结束后,此时 t1 线程也进入休眠
log.debug("t1 state {}", t1.getState());
}
}
2.其他线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test7")
public class Test7 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1"){
@Override
public void run() {
log.debug("enter sleep...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) { // 说明线程 sleep 的过程中可能会被打断
log.debug("wake up...");
e.printStackTrace();
}
}
};
t1.start();
Thread.sleep(1000); // 主线程睡眠 1 秒,等待 t1 线程睡眠
log.debug("interrupt...");
t1.interrupt();
}
}
运行结果:
3.睡眠结束后的线程未必会立刻得到执行
- 睡眠结束后的线程未必会立刻得到执行,这是因为此时 CPU 可能正在执行其他线程的代码,所以线程必须要等待任务调度器将时间片分配给线程后,才能继续运行。也就是说线程睡眠结束后,未必能够立刻获得 CPU 的使用权。
4.建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
- TimeUnit 带有时间单位,具有更好的可读性。其内部依然是调用的 Thread.sleep() 方法,只不过进行了一次时间单位的换算。
- Thread.sleep() 在哪个线程中被调用就是哪个线程睡眠
import java.util.concurrent.TimeUnit;
@Slf4j(topic = "c.Test")
public class Test8 {
public static void main(String[] args) throws InterruptedException {
log.debug("starting test");
TimeUnit.SECONDS.sleep(1);
log.debug("finished test");
// Thread.sleep(1000);
}
}
运行结果:
yield
1.调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其他线程
- 调用 yield 之后,当前线程会让出 CPU 的使用权,让其他线程有机会去使用 CPU。
2.具体的实现依赖于操作系统的任务调度器
- 虽然当前线程调用了 yield 方法,打算让出 CPU 的使用权,但是当前并 没有其他线程在运行,此时的任务调度器依然会把时间片分给当前线程。即想让没有让出去,这种情况非常常见。
Runnable(就绪)状态和 Timed Waiting(阻塞)状态的区别:
任务调度器只会将时间片分配给就绪状态的线程,是不会分配各阻塞状态的线程的,阻塞状态的线程只有在阻塞时间结束,线程醒过来以后才会分配时间片。即 sleep 中线程在结束 sleep 后,线程的状态会从阻塞变为就绪。
线程优先级
- 线程优先级会提示调度器有限调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果 CPU 比较忙,那么优先级高的线程会获得更多的时间片,但 CPU 闲时,优先级几乎没作用
| setPriority(int) | 修改线程优先级 | Java中规定线程优先级是 1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率 |
| — | — | — |
最小优先级为 1,最大优先级为 10,默认优先级为 5。 数字越大表示优先级越高。
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test")
public class Test9 {
public static void main(String[] args) {
Runnable task1 = () -> {
int count = 0;
for (;;) {
System.out.println("-------->1 " + count++);
}
};
Runnable task2 = () -> {
int count = 0;
for (;;) {
// Thread.yield();
System.out.println(" -------->2 " + count++);
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
// t1.setPriority(Thread.MIN_PRIORITY);
// t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
}
普通情况下的运行结果:
线程 t1 与 t2 的执行结果相差不多
测试 yield 方法,将 Thread.yield(); 前的注释放开,执行结果如下:
测试优先级,将 Thread.yield(); 注释,将优先级相关的代码注释放开,测试结果如下:
优先级在一定场景下可能会起作用,但不可靠不可控,仅仅只是对任务调度器的一个提示而已,调度器可以忽略它。
3.3 join 方法详解
3.3.1 join 方法介绍
为什么需要 join?
下面的代码执行,打印 r 是什么?
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j(topic = "c.Test")
public class Test10 {
static int r = 0;
public static void main(String[] args) {
test1();
}
private static void test1() {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("结束");
r = 10;
});
t1.start();
log.debug("结果为:{}", r);
log.debug("结束");
}
}
运行结果:
分析:
- 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10
- 而主线程一开始就要打印 r 的结果,所以只能打印出 r=0
解决方法:
- 用 sleep 行不行?为什么?
- 可以,但不推荐。因为我们并不知道线程 t1 从开始执行到结束总共花费了多长时间。
- 用 join,加载 t1.start() 之后即可。
- join:等待线程运行结束。哪个线程对象调用 join,就是等待该线程结束,即 t1.join(); 就是等待线程 t1 执行结束。
- 测试 join,修改代码如下:
package com.atheima.test;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j(topic = "c.Test")
public class Test10 {
static int r = 0;
public static void main(String[] args) {
test1();
}
private static void test1() {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("结束");
r = 10;
});
t1.start();
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("结果为:{}", r);
log.debug("结束");
}
}
运行结果:
3.3.2 应用之同步(案例 1)
以调用方角度来讲,如果
- 需要等待结果返回,才能继续运行就是同步
- 不需要等待结果范湖,就能继续运行就是异步
代码案例:
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j(topic = "c.Test")
public class TestJoin {
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 {
TimeUnit.SECONDS.sleep(1);
r1 = 10;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Thread t2 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
r2 = 20;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
log.debug("join begin");
t1.join();
log.debug("t1 join end");
t2.join();
log.debug("t2 join end");
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, (end - start));
}
}
运行结果:
分析:首先明确 join() 方法时等待调用该方法的线程执行结束。t1 线程开始执行时,t2 线程也开始了执行,当 t1 线程执行结束时,已经花费了一秒钟,即此时的 t2 线程也已经执行了 1 秒钟。所以从 t1.join 执行结束到 t2.join 执行结束总共只花费了 1 秒钟,而不是 2 秒钟。
那如果颠倒两个 join 呢?
最终输出的结果都是 2 秒。
可以看到 t1 和 t2 的 join 方法几乎是同时执行结束的。这是因为 t2 线程开始执行时,t1 线程也开始了执行,当 t2 线程执行结束时,已经花费了 2 秒钟,即此时的 t1 线程也已经执行结束,所以 t1.join 就无需再等待 t1 线程的执行结束。
3.3.3 join 限时同步
join(long n):等待线程运行结束,最多等待 n 毫秒。
当线程的执行时间小于设定的时间 n 时,以线程的执行时间为准。
当线程的执行时间大于设定的时间 n 时,以设定的时间 n 为准 ,即线程执行 n 毫秒后结束线程的运行。
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j(topic = "c.Test")
public class TestJoin {
static int r1 = 0;
public static void main(String[] args) throws InterruptedException {
test3();
}
private static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
r1 = 10;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
long start = System.currentTimeMillis();
t1.start();
log.debug("join begin");
t1.join(1500);
long end = System.currentTimeMillis();
log.debug("r1: {} cost: {}", r1, (end - start));
}
}
执行结果:
1s = 1000ms
17.208 - 15.701 = 16.1208 - 15.701 = 1.507s,大约为 1.5s。
如果将 t1.join(1500) 改为 t1.join(3000),执行结果如下:
执行时间大约为 27.814 - 25.809 = 2.005s,大约为 2s。即线程执行结束花费的总时间,而不是设定的 3 秒。
3.4 interrupt 方法详解
3.4.1 打断 sleep、wait、join 的线程
这几个方法都会让线程进入阻塞状态,打断 sleep 的线程,会清空打断状态,以 sleep 为例:
@Slf4j(topic = "c.InterruptTest")
public class InterruptTest {
public static void main(String[] args) throws InterruptedException {
Thread t1= new Thread(() -> {
log.debug("sleep...");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1");
t1.start();
Thread.sleep(3000); // 此处让主线程稍微等一下 t1 线程,否则主线程执行的要快一些,无法验证打断效果
debug("interrupt");
t1.interrupt();
log.debug("打断标记:{}", t1.isInterrupted()); // 获取打断标记
}
}
正常来讲, 线程被打断后,其打断状态应当会被标识为真,但是 sleep、join、wait 的线程在被打断后,会以异常的方式来表示被打断了,同时还会清空打断标记,即置为 false。
3.4.2 打断正常运行的线程
打断正常运行的线程,不会清空打断状态
public static void test2() throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted){ // 通过打断标记来判断是否退出循环
log.debug("被打断了,退出循环");
break;
}
}
}, "t1");
thread.start();
Thread.sleep(2000);
log.debug("interrupt");
thread.interrupt();
}
正常运行的线程,其打断标记会被置为 true,默认为 false。
3.4.3 终止模式之两阶段终止模式
Two Phase Termination
在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会。
1.错误思路
- 使用线程对象的 stop() 方法停止线程
- stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其他线程将永远无法获取锁。
- 使用 System.exit(int) 方法停止线程。目的仅是停止一个线程,但这种做法会让整个程序停止。
2.两阶段终止模式
@Slf4j(topic = "c.Test1")
public class Test1 {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.start();
Thread.sleep(3500);
tpt.stop();
}
}
@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination {
private Thread monitor;
// 启动监控线程
public void start(){
monitor = new Thread(() -> {
while(true){
Thread current = Thread.currentThread();
if (current.isInterrupted()) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000); // 情况1,在sleep过程中被打断
log.debug("执行监控记录"); // 情况2,正常执行时被打断
} catch (InterruptedException e) {
e.printStackTrace();
current.interrupt(); // 重新设置打断标记
}
}
});
monitor.start();
}
// 停止监控线程
public void stop(){
monitor.interrupt();
}
}
3.isInterrupted() 和 interrupted() 的区别
两个方法均可以判断当前线程是否被打断,但是 interrupted() 会清除打断标记 isInterrupted() 则不会清除。
3.3.4 打断 park 线程
LockSupport.park() 可以用来阻塞线程。
打断 park 线程,不会清除打断标记。
@Slf4j(topic = "c.TestInterrupt")
class TestInterrupt {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park(); // 线程 park 住了
log.debug("unpark...");
}, "t1");
t1.start();
}
}
输出:
线程 t1 发生阻塞
使用 interrupt 进行打断:
@Slf4j(topic = "c.TestInterrupt")
class TestInterrupt {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
}, "t1");
t1.start();
Thread.sleep(1);
t1.interrupt(); // 打断 park 线程,使其继续执行。不会清除打断标记。
}
}
输出:
问题一:
进行两次 park,观察输出。
@Slf4j(topic = "c.TestInterrupt")
class TestInterrupt {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
LockSupport.park(); // 重新进行park
log.debug("unpark...");
}, "t1");
t1.start();
Thread.sleep(1);
t1.interrupt();
}
}
输出:
第二次 park 后,程序直接结束,并未阻塞住。这是因为 interrup() 方法会重新设置打断标记,将 t1 线程的打断标记置为了 true,在打断标记为 true 的情况下调用 park() 方法,线程是无法阻塞住的。
Thread.interrupted() 方法会返回当前线程的打断状态,并会清除打断标记,重新置为false。
改进方法,修改打断标记的获取方式(Thread.interrupted()),清除打断标记,将其重新置为false。
@Slf4j(topic = "c.TestInterrupt")
class TestInterrupt {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.interrupted());
LockSupport.park();
log.debug("unpark...");
}, "t1");
t1.start();
Thread.sleep(1);
t1.interrupt();
}
}
输出:
总结
- LockSupport.park() 可以用来阻塞线程。
- park 住的线程在被 interrupted() 方法打断后,可以继续执行,但是被打断了 park 线程,其打断标记会被置为 true。
- 打断标记为真(true)时,执行 park 方法会失效。
3.4 过时方法
还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁。
方法名 | 功能说明 |
---|---|
stop() | 停止线程运行 |
suspend() | 挂起(暂停)线程运行 |
resume() | 恢复线程运行 |
3.5 主线程与守护线程
默认情况下,Java 进程需要等待所有线程运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其他非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
@Slf4j(topic = "c.TestDaemon")
class TestDaemon {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true){
if(Thread.currentThread().isInterrupted()){
break;
}
}
log.debug("结束");
}, "t1");
t1.start();
Thread.sleep(1000);
log.debug("结束");
}
}
输出:
主线程运行结束,但是,默认情况下,Java 进程需要等待所有线程运行结束,才会结束。而由于 t1 线程是一个死循环,所以整个程序无法结束。
将 t1 线程设置为守护线程:
@Slf4j(topic = "c.TestDaemon")
class TestDaemon {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true){
if(Thread.currentThread().isInterrupted()){
break;
}
}
log.debug("结束");
}, "t1");
t1.setDaemon(true); // 设置 t1 线程为守护线程
t1.start();
Thread.sleep(1000);
log.debug("结束");
}
}
�输出:
只要其他非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
:::warning
注意:
- 垃圾回收器线程就是一种守护线程
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求。
:::
四、线程状态
4.1 五种状态
这是从操作系统层面来描述的
- 初始状态:仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 可运行状态(就绪状态):指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 运行状态:指获取了 CPU 时间片运行中的状态
- 当 CPU 时间片用完,会从“运行状态”转换至“可运行状态”,会导致线程的上下文切换
- 阻塞状态
- 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入“阻塞状态”
- 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至“可运行状态”
- 与“可运行状态”的区别是,对“阻塞状态”的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
- 终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其他状态
4.2 六种状态
这是从 Java API 层面来描述的
根据 Thread.State 枚举,分为六种状态
- NEW:线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE:当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的“可运行状态”、“运行状态”和“阻塞状态”(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行的)
- BLOCKED、WAITING、TIMED_WAITING 都是 Java API 层面对“阻塞状态”的细分
- TERMINATED:当线程代码运行结束
4.3 线程状态演示
@Slf4j(topic = "c.TestThreadState")
public class TestThreadState {
public static void main(String[] args) {
Thread t1 = new Thread("t1"){
@Override
public void run() {
log.debug("running...");
}
};
Thread t2 = new Thread("t2"){
@Override
public void run() {
while(true){
}
}
};
t2.start();
Thread t3 = new Thread("t3"){
@Override
public void run() {
log.debug("running...");
}
};
t3.start();
Thread t4 = new Thread("t4"){
@Override
public void run() {
synchronized (TestThreadState.class){
try {
Thread.sleep(1000000); // Timed Waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t4.start();
Thread t5 = new Thread("t5"){
@Override
public void run() {
try {
t2.join(); // Waiting
// t2 无限循环,t5 需要等待 t2 执行结束,由于不知道 t2 什么时候执行结束,所以是 Waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t5.start();
Thread t6 = new Thread("t6"){
@Override
public void run() {
synchronized (TestThreadState.class){ // blocked,t4 线程先拿到锁,t6 拿不到,阻塞住了
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t6.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 state {}", t1.getState());
log.debug("t2 state {}", t2.getState());
log.debug("t3 state {}", t3.getState());
log.debug("t4 state {}", t4.getState());
log.debug("t5 state {}", t5.getState());
log.debug("t6 state {}", t6.getState());
}
}
输出: