start 与 run
调用run方法
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
FileReader.read(Constants.MP4_FULL_PATH);
}
};
t1.run();
log.debug("do other things...");
}
输出:
19:39:14 [main] c.TestStart - main
19:39:14 [main] c.FileReader - read [1.mp4] start ...
19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms
19:39:18 [main] c.TestStart - do other things ...
程序仍在 main 线程运行, FileReader.read() 方法调用还是同步的
调用 start
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
FileReader.read(Constants.MP4_FULL_PATH);
}
};
t1.start();
log.debug("do other things...");
}
输出
19:41:30 [main] c.TestStart - do other things ...
19:41:30 [t1] c.TestStart - t1
19:41:30 [t1] c.FileReader - read [1.mp4] start ...
19:41:35 [t1] c.FileReader - read [1.mp4] end ... cost: 4542 ms
程序在 t1 线程运行, FileReader.read() 方法调用是异步的
小结
-
run:称为线程体,包含了要执行的这个线程的内容,方法运行结束,此线程随即终止。直接调用 run 是在主线程中执行了 run,没有启动新的线程,需要顺序执行
-
start:使用 start 是启动新的线程,此线程处于就绪(可运行)状态,通过新的线程间接执行 run 中的代码(start方法只能调用一次)
说明:线程控制资源类
-
run() 方法中的异常不能抛出,只能 try/catch
- 因为父类中没有抛出任何异常,子类不能比父类抛出更多的异常
- 异常不能跨线程传播回 main() 中,因此必须在本地进行处理
sleep 与 yieId
调用sleep方法
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();
}
输出:
16:18:03.496 c.Test7 [t1] - enter sleep...
16:18:04.499 c.Test7 [main] - interrupt...
16:18:04.499 c.Test7 [t1] - wake up...
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at cn.itcast.test.Test7$1.run(Test7.java:14)
推荐使用TimeUnit来设置线程睡眠时间可读性更好
TimeUnit.SECONDS.sleep(1);
调用yieId方法
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();
}
线程优先级
线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
不能依靠线程优先级来判断线程执行的先后顺序
设置线程优先级,最小是1,最大是10,默认是5
t1.setPriority(Thread.MIN_PRIORITY); //1
t2.setPriority(Thread.MAX_PRIORITY); //10
小结
sleep:
- 调用 sleep 会让当前线程从
Running
进入Timed Waiting
状态(阻塞) - sleep() 方法的过程中,线程不会释放对象锁
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行,需要抢占 CPU
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
yield:
- 调用 yield 会让提示线程调度器让出当前线程对 CPU 的使用
- 调用 yield 会让当前线程从
Running
进入Runnable
状态(就绪状态),然后调度执行其他线程。(如果没有其他线程可以运行,任务调度器还是会把时间片分给 yieId 线程。) - 具体的实现依赖于操作系统的任务调度器
- 会放弃 CPU 资源,锁资源不会释放
join方法
为什么需要join方法
不使用join方法
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("结束");
}
结果:
17:15:14.079 c.Test10 [main] - 开始
17:15:14.176 c.Test10 [t1] - 开始
17:15:14.176 c.Test10 [main] - 结果为:0
17:15:14.179 c.Test10 [main] - 结束
17:15:15.181 c.Test10 [t1] - 结束
分析
- 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10
- 而主线程一开始就要打印 r 的结果,所以只能打印出 r=0
将代码注释打开
运行结果:
17:23:35.557 c.Test10 [main] - 开始
17:23:35.649 c.Test10 [t1] - 开始
17:23:36.662 c.Test10 [t1] - 结束
17:23:36.662 c.Test10 [main] - 结果为:10
17:23:36.665 c.Test10 [main] - 结束
以调用方角度来讲,如果
- 需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
等待多个结果
问,下面代码 cost 大约多少秒?
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(() -> {
sleep(1);
r1 = 10;
});
Thread t2 = new Thread(() -> {
sleep(2);
r2 = 20;
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
分析如下
- 第一个 join:等待 t1 时, t2 并没有停止, 而在运行
- 第二个 join:1s 后, 执行到此, t2 也运行了 1s, 因此也只需再等待 1s
如果颠倒两个 join 呢?
最终都是输出
17:35:36.412 c.Test10 [main] - r1: 10 r2: 20 cost: 2009
有时效的join
等够时间
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test3();
}
public static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(1);
r1 = 10;
});
long start = System.currentTimeMillis();
t1.start();
// 线程执行结束会导致 join 结束
t1.join(1500);
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
输出:
17:39:08.615 c.Test10 [main] - r1: 10 r2: 0 cost: 1013
没等够时间
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test3();
}
public static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(2);
r1 = 10;
});
long start = System.currentTimeMillis();
t1.start();
// 线程执行结束会导致 join 结束
t1.join(1500);
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
输出:
17:40:28.601 c.Test10 [main] - r1: 0 r2: 0 cost: 1503
原理
public final void join():等待这个线程结束
原理:调用者轮询检查线程 alive 状态,t1.join() 等价于:
public final synchronized void join(long millis) throws InterruptedException {
// 调用者线程进入 thread 的 waitSet 等待, 直到当前线程运行结束
while (isAlive()) {
wait(0);
}
}
-
join 方法是被 synchronized 修饰的,本质上是一个对象锁,其内部的 wait 方法调用也是释放锁的,但是释放的是当前的线程对象锁,而不是外面的锁
-
当调用某个线程(t1)的 join 方法后,该线程(t1)抢占到 CPU 资源,就不再释放,直到线程执行完毕
线程同步:
- join 实现线程同步,因为会阻塞等待另一个线程的结束,才能继续向下运行
- 需要外部共享变量,不符合面向对象封装的思想
- 必须等待线程结束,不能配合线程池使用
- Future 实现(同步):get() 方法阻塞等待执行结果
- main 线程接收结果
- get 方法是让调用线程同步等待
使用Future实现同步
Future 接口可以通过调用其 get() 方法来实现同步。这个方法会阻塞当前线程,直到异步任务完成并返回结果。
import java.util.concurrent.*;
public class FutureExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 创建一个线程池
ExecutorService executor = Executors.newFixedThreadPool(1);
// 提交一个异步任务
Future<Integer> futureResult = executor.submit(() -> {
// 模拟一个耗时计算任务
Thread.sleep(1000);
return 42;
});
// 等待任务完成并获取结果
int result = futureResult.get();
System.out.println("Result: " + result);
// 关闭线程池
executor.shutdown();
}
}
在这个示例中,创建了一个线程池,并使用 executor.submit() 方法提交了一个异步任务。然后,调用了 futureResult.get() 方法来等待任务完成,并获取结果。由于调用了 get() 方法,主线程会阻塞直到异步任务完成,因此这是一种同步的方式来执行异步任务。
需要注意的是,使用 Future 的同步方式可能会阻塞当前线程,降低程序的并发性能。因此,应该根据实际情况选择是否使用同步方式来执行异步任务。
interrupt方法
打断线程
interrupt 打断线程有两种情况,如下:
- 如果一个线程在在运行中被打断,打断标记会被置为 true 。
- 如果是打断因sleep wait join 方法而被阻塞的线程,会将打断标记置为 false 。
public void interrupt()
:打断这个线程,异常处理机制
public static boolean interrupted()
:判断当前线程是否被打断,打断返回 true,清除打断标记,连续调用两次一定返回 false
public boolean isInterrupted()
:判断当前线程是否被打断,不清除打断标记
打断的线程会发生上下文切换,操作系统会保存线程信息,抢占到 CPU 后会从中断的地方接着运行(打断不是停止)
- sleep、wait、join 方法都会让线程进入阻塞状态,打断线程会清空打断状态(false)
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("sleep...");
try {
Thread.sleep(5000); // wait, join
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
log.debug("打断标记:{}", t1.isInterrupted());
}
输出:
21:20:23.812 c.Test11 [t1] - sleep...
21:20:24.814 c.Test11 [main] - interrupt
21:20:24.814 c.Test11 [main] - 打断标记:false
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at cn.itcast.test.Test11.lambda$main$0(Test11.java:12)
at java.lang.Thread.run(Thread.java:750)
- 打断正常运行的线程:不会清空打断状态(true)
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();
log.debug("打断标记:{}", t1.isInterrupted());
}
输出:
21:26:36.806 c.Test12 [main] - interrupt
21:26:36.811 c.Test12 [t1] - 被打断了, 退出循环
21:26:36.811 c.Test12 [main] - 打断标记:true
打断 park
park 作用类似 sleep,打断 park 线程,不会清空打断状态(true)
private static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
}, "t1");
t1.start();
sleep(1);
t1.interrupt();
}
public static void main(String[] args) throws InterruptedException {
test3();
}
输出:
23:37:18.177 c.Test14 [t1] - park...
23:37:19.176 c.Test14 [t1] - unpark...
23:37:19.176 c.Test14 [t1] - 打断状态:true
如果打断标记已经是 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.currentThread().isInterrupted());
}
});
t1.start();
sleep(1);
t1.interrupt();
}
public static void main(String[] args) throws InterruptedException {
test4();
}
输出:
23:46:09.784 c.Test14 [Thread-0] - park...
23:46:10.795 c.Test14 [Thread-0] - 打断状态:true
23:46:10.799 c.Test14 [Thread-0] - park...
23:46:10.799 c.Test14 [Thread-0] - 打断状态:true
23:46:10.799 c.Test14 [Thread-0] - park...
23:46:10.799 c.Test14 [Thread-0] - 打断状态:true
23:46:10.799 c.Test14 [Thread-0] - park...
23:46:10.799 c.Test14 [Thread-0] - 打断状态:true
23:46:10.800 c.Test14 [Thread-0] - park...
23:46:10.800 c.Test14 [Thread-0] - 打断状态:true
可以修改获取打断状态方法,使用 Thread.interrupted()
,获取打断状态后清除打断标记,当清除打断标记后,LockSupport.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());
}
});
t1.start();
sleep(1);
t1.interrupt();
}
public static void main(String[] args) throws InterruptedException {
test4();
}
输出:
23:48:36.016 c.Test14 [Thread-0] - park...
23:48:37.022 c.Test14 [Thread-0] - 打断状态:true
23:48:37.026 c.Test14 [Thread-0] - park...
LockSupport 类在 同步 → park-un 详解
isInterrupted() 与 interrupted() 比较,如下:
首先,isInterrupted 是实例方法,interrupted 是静态方法,它们的用处都是查看当前打断的状态,但是 isInterrupted 方法查看线程的时候,不会将打断标记清空,也就是置为 false,interrupted 查看线程打断状态后,会将打断标志置为 false,也就是清空打断标记,简单来说,interrupt() 方法类似于 setter 设置中断值,isInterrupted() 类似于 getter 获取中断值,interrupted() 类似于 getter + setter 先获取中断值,然后清除标志。
/**
* 测试 isInterrupted 与 interrupted
*/
@Slf4j(topic = "c.Code_14_Test")
public class Code_14_Test {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.info("park");
LockSupport.park();
log.info("unpark");
// log.info("打断标记为:{}", Thread.currentThread().isInterrupted());
log.info("打断标记为:{}", Thread.interrupted());
// 使用 Thread.currentThread().isInterrupted() 查看打断标记为 true, LockSupport.park() 失效
/**
* 执行结果:
* 11:54:17 [t1] c.Code_14_Test - park
* 11:54:18 [t1] c.Code_14_Test - unpark
* 11:54:18 [t1] c.Code_14_Test - 打断标记为:true
* 11:54:18 [t1] c.Code_14_Test - unpark
*/
// 使用 Thread.interrupted() 查看打断标记为 true, 然后清空打断标记为 false, LockSupport.park() 不失效
/**
* 执行结果:
* 11:58:12 [t1] c.Code_14_Test - park
* 11:58:13 [t1] c.Code_14_Test - unpark
* 11:58:13 [t1] c.Code_14_Test - 打断标记为:true
*/
LockSupport.park();
log.info("unpark");
}, "t1");
t1.start();
Thread.sleep(1000); // 主线程休眠 1 秒
t1.interrupt();
}
}
终止模式之两阶段终止模式
终止模式之两阶段终止模式:Two Phase Termination
目标:在一个线程 T1 中如何优雅终止线程 T2?优雅指的是给 T2 一个后置处理器
错误思想:
- 使用线程对象的 stop() 方法停止线程:stop 方法会真正杀死线程,如果这时线程锁住了共享资源,当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁
- 使用 System.exit(int) 方法停止线程:目的仅是停止一个线程,但这种做法会让整个程序都停止
两阶段终止模式图示:
打断线程可能在任何时间,所以需要考虑在任何时刻被打断的处理方法:
/**
* 使用 interrupt 进行两阶段终止模式
*/
@Slf4j(topic = "c.Code_13_Test")
public class Code_13_Test {
public static void main(String[] args) throws InterruptedException {
TwoParseTermination twoParseTermination = new TwoParseTermination();
twoParseTermination.start();
Thread.sleep(3500);
twoParseTermination.stop();
}
}
@Slf4j(topic = "c.TwoParseTermination")
class TwoParseTermination {
private Thread monitor;
// 启动监控线程
public void start() {
monitor = new Thread(() -> {
while (true) {
Thread thread = Thread.currentThread();
if(thread.isInterrupted()) { // 调用 isInterrupted 不会清除标记
log.info("料理后事 ...");
break;
} else {
try {
Thread.sleep(1000); // 睡眠
log.info("执行监控的功能 ..."); // 在此被打断不会异常
} catch (InterruptedException e) { // 在睡眠期间被打断,进入异常处理的逻辑
log.info("设置打断标记 ...");
// 重新设置打断标记,打断 sleep 会清除打断状态
thread.interrupt();
e.printStackTrace();
}
}
}
}, "monitor");
monitor.start();
}
// 终止线程
public void stop() {
monitor.interrupt();
}
}
输出:
23:27:16.170 c.TwoParseTermination [monitor] - 执行监控的功能 ...
23:27:17.177 c.TwoParseTermination [monitor] - 执行监控的功能 ...
23:27:18.193 c.TwoParseTermination [monitor] - 执行监控的功能 ...
23:27:18.665 c.TwoParseTermination [monitor] - 设置打断标记 ...
23:27:18.666 c.TwoParseTermination [monitor] - 料理后事 ...
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at cn.itcast.test.TwoParseTermination.lambda$start$0(Test13.java:30)
at java.lang.Thread.run(Thread.java:750)
过时方法
不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁:
-
public final void stop()
:停止线程运行废弃原因:方法粗暴,除非可能执行 finally 代码块以及释放 synchronized 外,线程将直接被终止,如果线程持有 JUC 的互斥锁可能导致锁来不及释放,造成其他线程永远等待的局面
-
public final void suspend()
:挂起(暂停)线程运行废弃原因:如果目标线程在暂停时对系统资源持有锁,则在目标线程恢复之前没有线程可以访问该资源,如果恢复目标线程的线程在调用 resume 之前会尝试访问此共享资源,则会导致死锁
-
public final void resume()
:恢复线程运行
主线程和守护线程
默认情况下,java进程需要等待所有的线程结束后才会停止,但是有一种特殊的线程,叫做守护线程,在其他线程全部结束的时候即使守护线程还未结束代码未执行完java进程也会停止。普通线程t1可以调用 t1.setDeamon(true); 方法变成守护线程。
public final void setDaemon(boolean on)
:如果是 true ,将此线程标记为守护线程
线程启动前调用此方法:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
break;
}
}
log.debug("t1 结束");
}, "t1");
t1.setDaemon(true);
t1.start();
Thread.sleep(1000);
log.debug("main 结束");
}
输出:
只有main结束,没有t1结束
00:00:18.316 c.Test15 [main] - main 结束
用户线程:平常创建的普通线程
守护线程:服务于用户线程,只要其它非守护线程运行结束了,即使守护线程代码没有执行完,也会强制结束。守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免在执行的过程中的信息在终端上显示
说明:当运行的线程都是守护线程,Java 虚拟机将退出,因为普通线程执行完后,JVM 是守护线程,不会继续运行下去
常见的守护线程:
- 垃圾回收器线程就是一种守护线程
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求。
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) | 将此线程标记为守护线程或用户线程 |