一、Java 线程常用方法
| 方法名 | 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 | 判断当前线程是否被打断 | 会清除 打断标记 |
currentThread() | static | 获取正在执行的线程 | |
sleep(long n) | static | 让当前执行的线程休眠 n 毫秒, 休眠时让出 cpu 的时间片给其它 线程 | |
yield() | static | 提示线程调度器让出当前线程对 CPU的使用 | 主要是为了测试和调试 |
二、方法对比
2.1 run 和 start 方法
当创建一个线程后,直接调用 run 方法会出现什么现象呢?其实答案很简单,就是和我们创建一个普通类调用成员方法差不多,此时并没有开启一个新的线程来运行,线程调用的方法仍然实在 main 线程运行的。线程调用了 start 方法后,才是真正地开启了一个线程,但注意的是不能多次调用这个方法。
/**
* start 和 run 方法
* @author 落霞不孤
*/
@Slf4j(topic = "c.Test4")
public class Test4 {
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
// 读取一个 mp4 文件,需要一定的时间
FileReader.read(Constants.MP4_FULL_PATH);
}
};
// 直接调用 t1.run() 并没有开启线程, 此时是同步调用
t1.run();
// t1.start();
log.debug("do other things...");
}
}
输出
16:27:12.321 c.Test4 [main] - running...
16:27:12.334 c.FileReader [main] - read [316 01.冒泡、选择、堆排序.flv] start ...
16:27:13.646 c.FileReader [main] - read [316 01.冒泡、选择、堆排序.flv] end ... cost: 1312 ms
16:27:13.646 c.Test4 [main] - do other things...
从输出可以看出,结果还是同步的。只有 t1 调用 start 方法,结果才是异步的。
2.2 sleep 和 yield 方法
- 调用 sleep 会让当前线程从
Running进入Timed Waiting状态(阻塞)。调用 yield 让当前线程从Running进入Runnable状态,但还是有机会获得 CPU 的调度; - 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException;
- 睡眠结束后的线程未必会立刻得到执行;
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性。
/**
* sleep 的第 1 点演示
* @author 落霞不孤
*/
@Slf4j(topic = "c.Test6")
public class Test6 {
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
log.debug("t1 state: {}", t1.getState());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 state: {}", t1.getState());
}
}
/**
* sleep 的第 2 点演示
* @author 落霞不孤
*/
@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) {
log.debug("wake up...");
e.printStackTrace();
}
}
};
t1.start();
Thread.sleep(1000);
// 打断正在睡眠的线程
log.debug("interrupt...");
t1.interrupt();
}
}
/**
* sleep 的第 4 点演示:直观的线程睡眠方法的使用
* @author 落霞不孤
*/
@Slf4j(topic = "c.Test8")
public class Test8 {
public static void main(String[] args) throws InterruptedException {
log.debug("enter");
TimeUnit.SECONDS.sleep(1);
log.debug("end");
}
}
依次输出的结果
# Test6
16:38:15.018 c.Test6 [main] - t1 state: RUNNABLE
16:38:15.522 c.Test6 [main] - t1 state: TIMED_WAITING
# Test7
16:41:02.340 c.Test7 [t1] - enter sleep...
16:41:03.336 c.Test7 [main] - interrupt...
16:41:03.336 c.Test7 [t1] - wake up...
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at cn.itcast.test.Test7$1.run(Test7.java:18)
# Test8
16:41:50.976 c.Test8 [main] - enter
16:41:51.981 c.Test8 [main] - end
这两个方法可以让出 CPU 的使用权,当某个线程的工作繁忙时,可以让出使用权给其他线程,避免 CPU 一直被占用。
2.3 join 方法
下面的代码执行,打印 r 是 0,而我们想要的结果是 10。这时,join 方法就派上用场了,它的作用参考表格.
/**
* join 方法
* @author 落霞不孤
*/
@Slf4j(topic = "c.Test10")
public class Test10 {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
r = 10;
},"t1");
t1.start();
// t1.join();
log.debug("结果为:{}", r);
log.debug("结束");
}
}
输出
# 没有使用 join 方法
16:48:18.471 c.Test10 [main] - 开始
16:48:18.477 c.Test10 [t1] - 开始
16:48:18.475 c.Test10 [main] - 结果为:0
16:48:18.479 c.Test10 [main] - 结束
16:48:19.480 c.Test10 [t1] - 结束
# 使用了 join 方法
16:48:36.553 c.Test10 [main] - 开始
16:48:36.558 c.Test10 [t1] - 开始
16:48:37.560 c.Test10 [t1] - 结束
16:48:37.560 c.Test10 [main] - 结果为:10
16:48:37.562 c.Test10 [main] - 结束
小思考:之所以不用 sleep 方法来替代 join,是因为我们事前根本就不能知道 t1 的任务执行的时间,所以也就不知道究竟要等多久。join方法还有一个重载的方法,该方法的作用是等待超过的时间就不等了,CPU 执行其他的线程。
2.4 interrupt 方法
sleep,wait,join这几个方法都会让线程进入阻塞状态,interrupt 方法打断调用了这三个方法的线程,会清空打断状态(把打断状态赋值为 false),以 sleep 为例:
/**
* interrupt 方法打断 wait, join, sleep
* @author 落霞不孤
*/
@Slf4j(topic = "c.Test11")
public class Test11 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("sleep...");
try {
// wait, join, sleep 的线程被打断会清除打断标记
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
log.debug("打断标记:{}", t1.isInterrupted());
}
}
输出
16:58:46.903 c.Test11 [t1] - sleep...
16:58:47.899 c.Test11 [main] - interrupt
16:58:47.899 c.Test11 [main] - 打断标记:false
16:58:47.902 c.Test11 [main] - 线程状态:TERMINATED
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at cn.itcast.test.Test11.lambda$main$0(Test11.java:17)
at java.base/java.lang.Thread.run(Thread.java:834)
如果是打断正常运行的线程,不会清空打断状态。
/**
* interrupted 方法打断正常执行的线程
* @author 落霞不孤
*/
@Slf4j(topic = "c.Test12")
public class Test12 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted) {
log.debug("被打断了, 退出循环");
break;
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
// 被打断的线程并不会终止运行,可以通过打断标记让它自己终止
t1.interrupt();
}
}
输出
17:02:56.077 c.Test12 [main] - interrupt
17:02:56.082 c.Test12 [t1] - 被打断了, 退出循环
使用 interrupted 方法我们可以优雅地终止一个线程,优雅是指被打断的线程可以“料理后事”。
/**
* 两阶段终止模式
* @author 落霞不孤
*/
@Slf4j(topic = "c.TestTwoPhaseTermination")
public class TestTwoPhaseTermination {
public static void main(String[] args) throws InterruptedException {
TPTInterrupt t = new TPTInterrupt();
t.start();
Thread.sleep(3500);
log.debug("stop");
t.stop();
}
}
@Slf4j(topic = "c.TPTInterrupt")
class TPTInterrupt {
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
log.debug("将结果保存"); // 情况 2
} catch (InterruptedException e) {
e.printStackTrace();
// 重新设置打断标记
current.interrupt();
}
}
},"监控线程");
monitor.start();
}
public void stop() {
monitor.interrupt();
}
}
输出
17:17:15.115 c.TPTInterrupt [监控线程] - 将结果保存
17:17:16.118 c.TPTInterrupt [监控线程] - 将结果保存
17:17:17.119 c.TPTInterrupt [监控线程] - 将结果保存
17:17:17.612 c.TestTwoPhaseTermination [main] - stop
17:17:17.613 c.TPTInterrupt [监控线程] - 料理后事
打断 park 线程,不会清空打断状态:
/**
* interrupted 方法打断 park 线程
* @author 落霞不孤
*/
@Slf4j(topic = "c.Test14")
public class Test14 {
private static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
// park 方法的作用就是暂停线程的运行
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
}, "t1");
t1.start();
sleep(1);
// 打断 park 的线程,让它继续向下执行
t1.interrupt();
}
// 如果打断标记已经是 true, 则 park 会失效
private static void test4() {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
log.debug("park...");
LockSupport.park();
log.debug("打断状态:{}", Thread.interrupted());
LockSupport.park();
log.debug("unpark...");
}
});
t1.start();
sleep(1);
t1.interrupt();
}
public static void main(String[] args) throws InterruptedException {
test3();
}
}
输出
17:15:38.744 c.Test14 [t1] - park...
17:15:39.738 c.Test14 [t1] - unpark...
17:15:39.738 c.Test14 [t1] - 打断状态:true
如果打断标记已经是 true, 则 park 会失效。这时候线程会继续执行下去,可以使用 Thread.interrupted() 清除打断状态。调用 test4() 方法:
输出
17:18:28.569 c.Test14 [Thread-0] - park...
17:18:29.566 c.Test14 [Thread-0] - 打断状态:true
三、主线程和守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
/**
* 守护线程
* @author 落霞不孤
*/
@Slf4j(topic = "c.Test15")
public class Test15 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("t1 开始运行");
Sleeper.sleep(2);
log.debug("t1 运行结束");
}, "daemon");
// 设置线程为守护线程
log.debug("开始运行");
t1.setDaemon(true);
t1.start();
Sleeper.sleep(1);
log.debug("运行结束");
}
}
输出
19:40:53.777 c.Test15 [main] - 开始运行
19:40:53.780 c.Test15 [daemon] - t1 开始运行
19:40:54.781 c.Test15 [main] - 运行结束
注意
- 垃圾回收器线程就是一种守护线程
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
四、总结
还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁。
| 方法名 | static | 功能说明 |
|---|---|---|
stop() | 停止线程运行 | |
suspend() | 挂起(暂停)线程运行 | |
resume() | 恢复线程运行 |
通过这节课的学习,我对线程常用的 API 有了简单的认识。运用这些方法,也能解决一些简单的同步和异步的问题。