目录
- 创建和运行线程
- 查看进程线程
- 线程运行原理
- 常见方法了解
- start与run
- sleep与yield
- 线程优先级
- join
- interrupt
- 不推荐使用的方法
- 主线程与守护线程
- 线程状态
- 操作系统中的线程状态
- java中的线程状态
创建和运行线程
方法一: 直接使用 Thread
public class CreateThread01 {
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
// run 方法内实现了要执行的任务
System.out.println("线程执行啦");
}
};
// 设置名称
thread.setName("线程1");
// 运行
thread.start();
System.out.println("主线程结束");
}
}
方法二:使用 Runnable
配合 Thread
public class CreateThread02 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("线程执行啦");
}
};
// 创建线程
Thread thread = new Thread(runnable, "线程2");
// 执行
thread.start();
System.out.println("主线程结束");
}
}
Java 8 以后可以使用 lambda 精简代码:
public class CreateThread02 {
public static void main(String[] args) {
// 创建线程
Thread thread = new Thread(() -> System.out.println("线程执行啦"), "线程2");
// 执行
thread.start();
System.out.println("主线程结束");
}
这种方法把线程和任务分开了,用 Runnable 更容易与线程池等高级 API 配合。
方法三:FutureTask
配合Thread
FutureTask 能够接收 Callable 类型的参数,主要用来处理有返回结果的情况,后面还会详细讲解。
示例:
public class CreateThread03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建FutureTask对象
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("线程开始啦");
// 暂停1秒
Thread.sleep(1000);
return 100;
}
});
// 创建线程
Thread thread = new Thread(task, "线程1");
// 执行
thread.run();
// 获取线程的结果,会等到线程结果返回才执行
System.out.println(task.get());
System.out.println("主线程结束");
}
}
也可以使用lambda表达式简化:
public class CreateThread03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建FutureTask对象
FutureTask<Integer> task = new FutureTask<>(() -> {
System.out.println("线程开始啦");
// 暂停1秒
Thread.sleep(1000);
return 100;
});
// 创建线程
new Thread(task, "线程1").start();
// 获取线程的结果,会等到线程结果返回才执行
System.out.println(task.get());
System.out.println("主线程结束");
}
}
查看进程线程
linux
ps -fe 查看所有进程
ps -fT -p 查看某个进程(PID)的所有线程
kill 杀死进程
top 按大写 H 切换是否显示线程
top -H -p 查看某个进程(PID)的所有线程
Java
jps 命令查看所有 Java 进程
jstack 查看某个 Java 进程(PID)的所有线程状态
jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
线程运行原理
栈与栈帧
Java Virtual Machine Stacks (Java 虚拟机栈)
我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换(Thread Context Switch)
有时候线程并不是可以一直执行的,有一些原因会导致 cpu 不再执行当前的线程,转而执行另一个线程的代码:
- 线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当发生线程切换时,并不是直接换另一个线程运行那么简单。为了下次再运行相同的线程,就需要上下文切换,也就是需要由操作系统保存当前线程的状态,并恢复另一个线程的状态。Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等。
需要知道的是线程上下文切换太频繁发生会影响程序性能。
常见方法了解
方法 | static | 功能说明 | 注意 |
---|---|---|---|
start() | 启动一个新线程,在新的线程运行 run 方法中的代码 | start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现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 | ||
static | 判断当前线程是否被打断 | 会清除 打断标记 | |
currentThread() | static | 获取当前正在执行的线程 | |
sleep(long n) | static | 让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程 | |
yield() | static | 提示线程调度器让出当前线程对CPU的使用 | 主要是为了测试和调试 |
start与run
调用run
示例:
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CreateThread01 {
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
log.info("子线程执行啦");
}
};
thread.setName("线程1");
thread.run();
log.info("主线程结束");
}
}
结果如下:
2023-04-16 15:59:16,712 - 0 INFO [main] up.cys.chapter01.CreateThread01$1:18 - 子线程执行啦
2023-04-16 15:59:16,719 - 7 INFO [main] up.cys.chapter01.CreateThread01:27 - 主线程结束
根据打印的日志看到"子线程执行啦"虽然打印出来了,但是其执行是在[main]线程中。
调用start
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CreateThread01 {
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
log.info("子线程执行啦");
}
};
thread.setName("线程1");
thread.start();
log.info("主线程结束");
}
}
打印如下:
2023-04-16 16:01:53,905 - 0 INFO [main] up.cys.chapter01.CreateThread01:27 - 主线程结束
2023-04-16 16:01:53,905 - 0 INFO [线程1] up.cys.chapter01.CreateThread01$1:18 - 子线程执行啦
根据打印的日志看到"子线程执行啦"是在[线程1]中执行的,这才是真的开启开启一个线程了。
结论
- 直接调用 run 是在主线程中直接执行了 run方法,会把run 方法当成一个main 线程下的普通方法去执行,没有启动新的线程
- 使用 start 会启动一个线程并使线程进入了就绪状态,当分配到 时间片 后就可以开始运行了
一般开发人员使用start来启动线程。
sleep与yield
sleep
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
yield
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器
线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
join
join会使当前线程等待指定线程运行结束才继续向下运行。
前面我们使用过sleep来让主线程等待一定时间,如下:
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j
public class JoinTest01 {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.info("开始");
Thread t1 = new Thread(() -> {
log.info("开始");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("结束");
r = 10;
});
t1.start();
// 这里的等待2秒时为了等子线运行完程,这样拿到的r值才是被修改后的
TimeUnit.SECONDS.sleep(2);
log.info("结果为:{}", r);
log.info("结束");
}
}
但是这样有个坏处就是,我们不知道线程需要等多久。这时候就需要使用join:
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j
public class JoinTest01 {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.info("开始");
Thread t1 = new Thread(() -> {
log.info("开始");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("结束");
r = 10;
});
t1.start();
// 使用join让主线程等待子线程结束
t1.join();
log.info("结果为:{}", r);
log.info("结束");
}
}
注意:用 join,需加在 t1.start() 之后。
join还可以带一个参数,表示只等待多久,这里就不在演示。
interrupt
此方法用来打断线程。但是打断不同状态的线程效果是不一样的。
1.打断sleep,wait,join的线程
这几个方法都会让线程进入阻塞状态。打断 sleep 的线程, 会清空打断状态,以 sleep 为例。
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j
public class InterruptTest {
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
Thread t1 = new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
TimeUnit.SECONDS.sleep(1);
t1.interrupt();
log.info(" 打断状态: {}", t1.isInterrupted());
}
}
输出:
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at java.base/java.lang.Thread.sleep(Thread.java:339)
at java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)
at up.cys.chapter01.InterruptTest.lambda$test1$0(InterruptTest.java:21)
at java.base/java.lang.Thread.run(Thread.java:834)
2023-04-16 17:06:30,827 - 0 INFO [main] up.cys.chapter01.InterruptTest:29 - 打断状态: false
2.打断正常运行的线程
打断正常运行的线程, 不会清空打断状态:
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j
public class InterruptTest02 {
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
Thread t2 = new Thread(()->{
while(true) {
// 获取打断标记
Thread current = Thread.currentThread();
boolean interrupted = current.isInterrupted();
// 如果标记为true,停止
if(interrupted) {
log.info(" 打断状态: {}", interrupted);
break;
}
}
}, "t2");
t2.start();
Thread.sleep(500);
// 主线程打断子线程:注意并不会真正的打断,自线程还是会正常运行,需要自线程获取标记自己决定是否中断
log.info("打断");
t2.interrupt();
}
}
输出如下:
2023-04-16 17:20:48,575 - 0 INFO [main] up.cys.chapter01.InterruptTest02:34 - 打断
2023-04-16 17:20:48,588 - 13 INFO [t2] up.cys.chapter01.InterruptTest02:26 - 打断状态: true
不推荐使用的方法
还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁:
stop() 、suspend()、resume()
主线程与守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守
护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class DaemonTest {
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
log.info("开始运行...");
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
Thread.sleep(1);
log.info("运行结束...");
}
}
输出如下:
2023-04-17 00:09:48,359 - 0 INFO [daemon] up.cys.chapter01.DaemonTest:19 - 开始运行...
2023-04-17 00:09:48,359 - 0 INFO [main] up.cys.chapter01.DaemonTest:31 - 运行结束...
垃圾回收器线程就是一种守护线程;
Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
线程状态
操作系统中的线程状态
这是从 操作系统 层面来描述的。
- 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 【运行状态】指获取了 CPU 时间片运行中的状态,当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
- 【阻塞状态】如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入阻塞状态;等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】;与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
- 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
java中的线程状态
这是从 Java API 层面来描述的,根据 Thread.State
枚举,分为六种状态
- NEW :线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE :当调用了
start()
方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行) - BLOCKED , WAITING , TIMED_WAITING :都是 Java API 层面对【阻塞状态】的细分
- TERMINATED :当线程代码运行结束