提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
本章内容
- 创建和运行线程
- 查看线程
- 线程 API
- 线程状态
提示:以下是本篇文章正文内容,下面案例可供参考
一、创建和运行线程
方法一,直接使用 Thread
// 创建线程对象
Thread t = new Thread() {
public void run() {
// 要执行的任务
}
};
// 启动线程
t.start();
或
// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1") {
@Override
// run 方法内实现了要执行的任务
public void run() {
log.debug("hello");
}
};
t1.start();
示例:
@Slf4j(topic = "c.Test1")
public class Test1 {
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
log.debug("running");
}
};
t.setName("t1");
t.start();
log.debug("running");
}
}
13:10:35.380 [t1] DEBUG c.Test1 - running
13:10:35.376 [main] DEBUG c.Test1 - running
方法二,使用 Runnable 配合 Thread
把【线程】和【任务】(要执行的代码)分开
- Thread 代表线程
- Runnable 可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable() {
public void run(){
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();
或
// 创建任务对象
Runnable task2 = new Runnable() {
@Override
public void run() {
log.debug("hello");
}
};
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();
示例:
@Slf4j(topic = "c.Test2")
public class Test2 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
log.debug("running");
}
};
Thread thread = new Thread(runnable,"t2");
thread.start();
log.debug("running");
}
}
13:12:14.164 [main] DEBUG c.Test2 - running
13:12:14.177 [t2] DEBUG c.Test2 - running
使用lambda简化:因为Runnable是一个函数式接口
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
因此通常这样写,简化代码:
Runnable runnable1=()->{
log.debug("running");
};
原理之 Thread 与 Runnable 的关系
- Thread实现了Runnable接口。
- 在初始化Thread的时候,如果使用的是有参构造器
- 会把Runnable的实现作为target传入方法,最终会赋值给Thread的成员变量target
- Thread的对象调用run方法时,其实是target调用run方法,也就是之前传进来的Runnable的实现
小结
方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
- 用 Runnable 更容易与线程池等高级 API 配合
- 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
方法三,FutureTask 配合 Thread
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
log.debug("hello");
return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);
示例:
@Slf4j(topic = "c.Test3")
public class Test3 {
public static void main(String[] args) throws Exception{
FutureTask task = new FutureTask(new Callable() {
@Override
public Integer call() throws Exception {
log.debug("running");
Thread.sleep(1000);
return 100;
}
});
Thread thread = new Thread(task,"t1");
thread.start();
log.debug("{}",task.get());//主线程就会阻塞在这,等待t1线程的任务task返回结果
}
}
观察多个线程同时运行
测试代码
@Slf4j(topic = "c.Test4")
public class Test4 {
public static void main(String[] args) {
new Thread(()->{
while (true){
log.debug("running");
}
},"t1").start();
new Thread(()->{
while (true){
log.debug("running");
}
},"t2").start();
}
}
现象:交替执行,谁先谁后,不由我们控制
13:23:37.483 [t2] DEBUG c.Test4 - running
13:23:37.483 [t1] DEBUG c.Test4 - running
13:23:37.489 [t2] DEBUG c.Test4 - running
13:23:37.489 [t1] DEBUG c.Test4 - running
13:23:37.489 [t2] DEBUG c.Test4 - running
13:23:37.489 [t1] DEBUG c.Test4 - running
13:23:37.489 [t2] DEBUG c.Test4 - running
13:23:37.489 [t1] DEBUG c.Test4 - running
13:23:37.489 [t2] DEBUG c.Test4 - running
13:23:37.489 [t2] DEBUG c.Test4 - running
13:23:37.489 [t1] DEBUG c.Test4 - running
13:23:37.489 [t2] DEBUG c.Test4 - running
13:23:37.490 [t1] DEBUG c.Test4 - running
13:23:37.490 [t2] DEBUG c.Test4 - running
13:23:37.490 [t1] DEBUG c.Test4 - running
13:23:37.490 [t2] DEBUG c.Test4 - running
13:23:37.490 [t1] DEBUG c.Test4 - running
13:23:37.490 [t2] DEBUG c.Test4 - running
13:23:37.490 [t1] DEBUG c.Test4 - running
13:23:37.490 [t2] DEBUG c.Test4 - running
13:23:37.490 [t1] DEBUG c.Test4 - running
13:23:37.510 [t2] DEBUG c.Test4 - running
13:23:37.511 [t2] DEBUG c.Test4 - running
13:23:37.511 [t2] DEBUG c.Test4 - running
13:23:37.511 [t2] DEBUG c.Test4 - running
13:23:37.511 [t2] DEBUG c.Test4 - running
13:23:37.511 [t2] DEBUG c.Test4 - running
13:23:37.511 [t2] DEBUG c.Test4 - running
...
二、查看进程线程的方法
jconsole 远程监控配置
如果是本地查看,直接在命令行窗口如果jconsole
或者找到你JDK的安装路径,在bin中找到这个命令
最中就是这样
这里面可以查看很多东西,比如,是否线程死锁、内存占用等等,是一种JDK自带的监控工具
你也可以远程监控:
在远程服务器上这样运行你的java类
java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -
Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -
Dcom.sun.management.jmxremote.authenticate=是否认证 java类
三、原理之线程运行
栈与栈帧
Java Virtual Machine Stacks (Java 虚拟机栈)
我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟
机就会为其分配一块栈内存。
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
public class TestFrames {
public static void main(String[] args) {
new Thread(()->{
method1(20);
},"t1").start();
method1(10);
}
private static void method1(int x){
int y= x+1;
Object o = method2();
System.out.println(o);
}
private static Object method2(){
Object n = new Object();
return n;
}
}
调整debug模式:Thread
一个栈帧就对应线程中的一个方法,栈帧按照线程中方法的执行顺序入栈,先进后出,每当一个方法执行完毕后,其对应的栈帧也就会弹出。
多线程的工作工作原理:JVM会对每个线程都分配一个私有的栈。
#四、 线程上下文切换(Thread Context Switch)
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
- 线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch 频繁发生会影响性能
五、线程中的方法
start()和run()
run()
run方法并不能启动线程,它定义的是线程对应的业务逻辑
看下面代码:
测试1:
package cn.itcast.text;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test5")
public class Test5 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
log.debug(Thread.currentThread().getName());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t1");
System.out.println(thread.getState());
thread.run();
System.out.println(thread.getState());
}
}
NEW
15:01:44.912 [main] DEBUG c.Test5 - main
NEW
直接调用run,其实是主线程(当前线程)调用的,就是对象调用普通方法,调用前后,t1线程一直处于新建状态。
start()
测试2:把run改成start
start():启动线程,线程进入就绪态,等待CPU分配时间片,并执行run方法()
@Slf4j(topic = "c.Test5")
public class Test5 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
log.debug(Thread.currentThread().getName());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t1");
System.out.println(thread.getState());
thread.start();
System.out.println(thread.getState());
}
}
NEW
RUNNABLE
15:03:58.519 [t1] DEBUG c.Test5 - t1
测试2可以发现,start调用前后,t1的线程的状态由新建(NEW)变为运行(RUNNABLE)
其实调用start()后,线程是进入就绪状态,等待CPU分配时间片,真正分配时间片后才执行
sleep()和yield()
sleep()
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行 (需要等待CPU分配时间片)
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
- Thread.sleep在哪个线程里就是让哪个睡眠
TimeUnit.SECONDS.sleep(1);//可读性更好
测试1:
package cn.itcast.text;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test6")
public class Test6 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
log.debug(Thread.currentThread().getName());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t1");
thread.start();
System.out.println(thread.getState());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getState());
}
}
RUNNABLE
15:17:23.423 [t1] DEBUG c.Test6 - t1
TIMED_WAITING
这里对RUNNABLE进行个解释:因为主线程不会阻塞在【thread.start();】方法这,它会继续执行,而t1未必执行到了【 Thread.sleep(10000);】,t1和主线程是异步的。如果让主线程等待一下【 Thread.sleep(500);】,就能看到TIMED_WAITING
测试2:打断sleep
@Slf4j(topic = "c.Test6")
public class Test6 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
log.debug("enter sleep...");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
log.debug("wake up ");
e.printStackTrace();
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt t1");
t1.interrupt();
}
}
15:23:15.413 [t1] DEBUG c.Test6 - enter sleep...
15:23:16.422 [main] DEBUG c.Test6 - interrupt t1
15:23:16.422 [t1] DEBUG c.Test6 - wake up
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at cn.itcast.text.Test6$1.run(Test6.java:13)
at java.lang.Thread.run(Thread.java:748)
yield()
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
就绪状态还有机会等待CPU分配时间片
- 具体的实现依赖于操作系统的任务调度器
首先不使用yield
public class Test7 {
public static void main(String[] args) {
Runnable task1 = () -> {
int count = 0;
for (;;) {
System.out.println("---->1 " + count++);
}
};
Runnable task2 = () -> {
int count = 0;
for (;;) {
System.out.println(" ---->2 " + count++);
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
t1.start();
t2.start();
}
}
---->1 48614
---->1 48615
---->1 48616
---->1 48617
---->1 48618
---->1 48619
---->1 48620
---->1 48621
---->1 48622
---->1 48623
---->2 37304
---->2 37305
---->2 37306
---->2 37307
---->2 37308
---->2 37309
---->2 37310
---->2 37311
---->2 37312
---->2 37313
---->2 37314
---->2 37315
---->2 37316
---->2 37317
---->2 37318
---->2 37319
---->2 37320
---->1 48624
---->1 48625
---->1 48626
---->1 48627
---->1 48628
使用yield
---->1 16386
---->1 16387
---->1 16388
---->1 16389
---->1 16390
---->1 16391
---->1 16392
---->1 16393
---->1 16394
---->1 16395
---->1 16396
---->1 16397
---->1 16398
---->1 16399
---->1 16400
---->1 16401
---->2 40
---->1 16402
---->1 16403
---->1 16404
---->1 16405
还是能明显看出,线程2的执行次数比线程1少的,这是因为每次线程2在执行前都会让出CPU的执行权,进入就绪
Sleep与yield的区别:
1.sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会
2.yield()方法只会给相同优先级或更高优先级的线程以运行的机会
3.线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态
4.sleep()方法声明会抛出InterruptedException,而yield()方法没有声明任何异常
5.sleep()方法比yield()方法具有更好的移植性(跟操作系统CPU调度相关)
线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
我们给上述代码设置一下优先级
public class Test7 {
public static void main(String[] args) {
Runnable task1 = () -> {
int count = 0;
for (;;) {
System.out.println("---->1 " + count++);
}
};
Runnable task2 = () -> {
int count = 0;
for (;;) {
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();
}
}
---->2 4064
---->2 4065
---->2 4066
---->2 4067
---->2 4068
---->2 4069
---->2 4070
---->2 4071
---->2 4072
---->2 4073
---->2 4074
---->1 6654
---->1 6655
---->1 6656
---->1 6657
---->1 6658
Sleep应用——防止CPU占用100%
在没有利用CPU来计算时,不要让while空转浪费CPU,这时可以使用yield或sleep来让出CPU的使用权给其他程序
while (true){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
测试不要sleep
public class Test7 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while (true){
}
}
}).start();
}
}
用JDK自带的jvisualvm监控看下
jvisualvm比jconsole ,功能更全,也是JDK自带的,多用于JVM性能监控,是JVM调优的好把手,详细的使用可以学习我JVM调优的博客,后面会介绍更多的调优工具,以及调优案例:https://blog.csdn.net/m0_45364328/article/details/124641883
测试使用sleep:
public class Test7 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while (true){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
很明显线程的CPU占用就低了很多,sleep可以让出当前CPU的使用权,不霸占CPU,否则,一个死循环监听功能势必造成其他业务线程的阻塞
- 可以用wait或调价变量达到类似的效果
- 不同的是,后两种都需要加锁,并且需要响应的唤醒操作,一般适用于要进行同的场景
- sleep适用于无需锁同步的场景
join()
为什么需要 join?下面的代码执行,打印 r 是什么?
@Slf4j(topic = "c.TestJoin")
public class TestJoin {
static int r = 0;
public static void main(String[] args) {
test1();
}
private static void test1() {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("结束");
r = 10;
});
t1.start();
log.debug("结果为:{}", r);
log.debug("结束");
}
}
17:26:07.040 [main] DEBUG c.TestJoin - 开始
17:26:07.128 [main] DEBUG c.TestJoin - 结果为:0
17:26:07.131 [main] DEBUG c.TestJoin - 结束
17:26:07.132 [Thread-0] DEBUG c.TestJoin - 开始
17:26:07.134 [Thread-0] DEBUG c.TestJoin - 结束
打印结果是0,原因是因为主线程和Thread-0是异步的,主线程在打印r 时,Thread-0线程还没给r赋上值,可以让主线程Sleep一段时间再打印r,但是sleep多久合适呢,因为我们不知道Thread-0需要执行多久,因此join的作用就体现了
使用join:
@Slf4j(topic = "c.TestJoin")
public class TestJoin {
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("开始");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("结束");
r = 10;
});
t1.start();
t1.join();
log.debug("结果为:{}", r);
log.debug("结束");
}
}
17:30:31.290 [main] DEBUG c.TestJoin - 开始
17:30:31.386 [Thread-0] DEBUG c.TestJoin - 开始
17:30:31.388 [Thread-0] DEBUG c.TestJoin - 结束
17:30:31.388 [main] DEBUG c.TestJoin - 结果为:10
17:30:31.390 [main] DEBUG c.TestJoin - 结束
在主线程中使用 t1.join();
主线程就会阻塞在join处,等待t1执行完,才放行。
应用之同步(案例1)
以调用方角度来讲,如果
- 需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
等待多个结果
问,下面代码 cost 大约多少秒?
@Slf4j(topic = "c.TestJoin")
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 {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r2 = 20;
});
t1.start();
t2.start();
long start = System.currentTimeMillis();
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);
}
}
17:38:37.889 [main] DEBUG c.TestJoin - join begin
17:38:38.894 [main] DEBUG c.TestJoin - t1 join end
17:38:39.890 [main] DEBUG c.TestJoin - t2 join end
17:38:39.890 [main] DEBUG c.TestJoin - r1: 10 r2: 20 cost: 2005
分析如下
- 第一个 join:等待 t1 时, t2 并没有停止, 而在运行
- 第二个 join:1s 后, 执行到此, t2 也运行了 1s, 因此也只需再等待 1s
也就说由多个join阻塞的时候,它的时间使用阻塞在第一个join开始,如果计算在第二个join阻塞的时间的时候,也要算上在前面join的阻塞时间
如果颠倒两个 join 呢?
log.debug("join begin");
t2.join();
log.debug("t2 join end");
t1.join();
log.debug("t1 join end");
17:41:37.012 [main] DEBUG c.TestJoin - join begin
17:41:39.023 [main] DEBUG c.TestJoin - t2 join end
17:41:39.023 [main] DEBUG c.TestJoin - t1 join end
17:41:39.023 [main] DEBUG c.TestJoin - r1: 10 r2: 20 cost: 2016
因为主线程已经在t2阻塞了两秒,而t1只需要阻塞1秒,已经包含在之前阻塞的时间里面了,也可以认为在主线程阻塞在等待t2运行结束的时候,t1已经执行结束了。
有时效的 join
Join的也可以设置等待时间
@Slf4j(topic = "c.TestJoin")
public class TestJoin {
static int r1 = 0;
public static void main(String[] args) throws InterruptedException {
test2();
}
private static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
t1.start();
t1.join(1000);
log.debug("r1: {}", r1);
}
}
17:47:16.689 [main] DEBUG c.TestJoin - r1: 0
因为t1 线程的执行需要2秒,而主线程只阻塞等待t1执行完毕1秒,所以t1不能给r1赋上值
修改主线程等待t1时间:
t1.join(3000);
17:48:46.082 [main] DEBUG c.TestJoin - r1: 10
这样就赋上值了,也就是说我们可以设置一个合理的阻塞时,如果时间内,t1线程不能执行完,我们就不阻塞当前线程了。
interrupt 方法
打断 sleep,wait,join 的线程(打断阻塞线程)
这几个方法都会让线程进入阻塞状态
打断 sleep 的线程, 会清空打断状态,以 sleep 为例,同时抛出InterruptedException异常,打断标记为false
@Slf4j(topic = "c.TestInterrupt")
public class TestInterrupt {
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
Thread.sleep(500);
t1.interrupt();
log.debug(" 打断状态: {}", t1.isInterrupted());
}
}
18:37:55.844 [main] DEBUG c.TestInterrupt - 打断状态: false
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at cn.itcast.text.TestInterrupt.lambda$main$0(TestInterrupt.java:12)
at java.lang.Thread.run(Thread.java:748)
打断正常运行的线程
不会抛出InterruptedException异常,打断标记为true
@Slf4j(topic = "c.TestInterrupt")
public class TestInterrupt {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()){//被打断,退出循环,终止线程
log.debug("被打断了,退出循环");
break;
}
}
}, "t1");
t1.start();
Thread.sleep(500);
t1.interrupt();
log.debug(" 打断状态: {}", t1.isInterrupted());
}
}
18:41:27.523 [t1] DEBUG c.TestInterrupt - 被打断了,退出循环
18:41:27.523 [main] DEBUG c.TestInterrupt - 打断状态: true
可以通过打断标记判断要做其他的事,比如终止线程
两阶段终止模式
什么是两阶段终止模式?
在一线程T1中“优雅”终止线程T2,这里的优雅指的是,给T2一个料理后事的机会
1、错误的思路
- 使用线程对象的stop()方法停止线程
stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它杀死后,就再也没有机会释放锁,其他线程将永远无法获取锁
- 使用System.exit(int)方法停止线程
目的仅仅是停止一个线程,而这种做法会让整个程序都停止
2、两阶段终止模式设计模型
@Slf4j(topic = "c.Test10")
public class Test10 {
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(new Runnable() {
@Override
public void run() {
while (true) {
Thread thread = Thread.currentThread();
if (thread.isInterrupted()) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);//处于阻塞状态的线程,如果被打断,打断标记会设置为true,同时抛出异常
log.debug("执行监控记录");//线程处于正常运行状态,被打断设置为true,不会抛出异常
} catch (InterruptedException e) {
e.printStackTrace();
thread.interrupt();//因为是阻塞状态被打断,所以打断标记为false,需要重置打段标记为ture,
}
}
}
});
monitor.start();
}
public void stop() {
monitor.interrupt();
}
}
19:03:07.655 [Thread-0] DEBUG c.TwoPhaseTermination - 执行监控记录
19:03:08.668 [Thread-0] DEBUG c.TwoPhaseTermination - 执行监控记录
19:03:09.689 [Thread-0] DEBUG c.TwoPhaseTermination - 执行监控记录
19:03:10.159 [Thread-0] DEBUG c.TwoPhaseTermination - 料理后事
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at cn.itcast.text.TwoPhaseTermination$1.run(Test10.java:40)
at java.lang.Thread.run(Thread.java:748)
isInterrupted()和Interrupted()的区别:Interrupted()也可以用来判断当前线程是否被打断,但同时会清除打断标记(ture置为fasle),前者不会清除打断标记。
测试:
isInterrupted:
@Slf4j(topic = "c.TestInterrupt")
public class TestInterrupt {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
Thread t = Thread.currentThread();
if (t.isInterrupted()){//被打断,退出循环,终止线程
log.debug("被打断了,退出循环");
log.debug(" 打断状态: {}", t.isInterrupted());
break;
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
t.interrupt();
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
}
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at cn.itcast.text.TestInterrupt.lambda$main$0(TestInterrupt.java:21)
at java.lang.Thread.run(Thread.java:748)
19:33:16.284 [t1] DEBUG c.TestInterrupt - 被打断了,退出循环
19:33:16.288 [t1] DEBUG c.TestInterrupt - 打断状态: true
interrupted:
@Slf4j(topic = "c.TestInterrupt")
public class TestInterrupt {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
Thread t = Thread.currentThread();
if (Thread.interrupted()){//被打断,退出循环,终止线程
log.debug("被打断了,退出循环");
log.debug(" 打断状态: {}", t.isInterrupted());
break;
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
t.interrupt();
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
}
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at cn.itcast.text.TestInterrupt.lambda$main$0(TestInterrupt.java:21)
at java.lang.Thread.run(Thread.java:748)
19:33:42.537 [t1] DEBUG c.TestInterrupt - 被打断了,退出循环
19:33:42.544 [t1] DEBUG c.TestInterrupt - 打断状态: false
注意:二者的调用都不一样。
打段park
看一下park是个啥:
@Slf4j(topic = "c.TestInterruptPark")
public class TestInterruptPark {
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();
}
}
19:39:20.421 [t1] DEBUG c.TestInterruptPark - park...
t1 阻塞是在【 LockSupport.park();】
如果我们打断正处于park阻塞的线程
@Slf4j(topic = "c.TestInterruptPark")
public class TestInterruptPark {
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();
}
}
19:42:20.001 [t1] DEBUG c.TestInterruptPark - park...
19:42:20.006 [t1] DEBUG c.TestInterruptPark - unpark...
19:42:20.007 [t1] DEBUG c.TestInterruptPark - 打断状态:true
注意:如果打断标记已经是 true, 则 park 会失效
@Slf4j(topic = "c.TestInterruptPark")
public class TestInterruptPark {
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();
log.debug("unpark...");
}, "t1");
t1.start();
Thread.sleep(1);
t1.interrupt();
}
}
发现unpark…打印
19:43:09.919 [t1] DEBUG c.TestInterruptPark - park...
19:43:09.928 [t1] DEBUG c.TestInterruptPark - unpark...
19:43:09.928 [t1] DEBUG c.TestInterruptPark - 打断状态:true
19:43:09.932 [t1] DEBUG c.TestInterruptPark - unpark...
改进,把isInterrupted()改为interrupted(),因为后者会情况打断标记,把true置为fasle
@Slf4j(topic = "c.TestInterruptPark")
public class TestInterruptPark {
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();
}
}
19:45:08.262 [t1] DEBUG c.TestInterruptPark - park...
19:45:08.269 [t1] DEBUG c.TestInterruptPark - unpark...
19:45:08.269 [t1] DEBUG c.TestInterruptPark - 打断状态:true
不推荐的方法
六、主线程与守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
@Slf4j(topic = "c.TestDaemon")
public class TestDaemon {
public static void main(String[] args) throws Exception{
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
log.debug("开始运行...");
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("运行结束...");
}, "daemon");
t1.start();
Thread.sleep(1000);
log.debug("运行结束...");
}
}
把t1线程设置为守护线程: t1.setDaemon(true)
@Slf4j(topic = "c.TestDaemon")
public class TestDaemon {
public static void main(String[] args) throws Exception{
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
log.debug("开始运行...");
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("运行结束...");
}, "daemon");
//把t1线程设置为守护线程
t1.setDaemon(true);
t1.start();
Thread.sleep(1000);
log.debug("运行结束...");
}
}
七 、线程的状态
从操作系统层面来说,五种状态
-
【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
-
【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
-
【运行状态】指获取了 CPU 时间片运行中的状态
当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
-
【阻塞状态】
- 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入
【阻塞状态】 - 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
- 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑
调度它们
- 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入
-
【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
从 Java API 层面来描述,根据 Thread.State 枚举,分为六种状态
- NEW 线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的
【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为
是可运行) - BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述
- TERMINATED 当线程代码运行结束
@Slf4j(topic = "c.TestState")
public class TestState {
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() {
}
};
t3.start();
Thread t4 = new Thread("t4"){
@Override
public void run() {
synchronized (TestState.class){
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t4.start();
Thread t5 = new Thread("t5") {
@Override
public void run() {
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t5.start();
Thread t6 = new Thread("t6") {
@Override
public void run() {
synchronized (TestState.class){
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t6.start();
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());
}
}
20:15:57.951 [main] DEBUG c.TestState - t1 state NEW
20:15:57.953 [t3] DEBUG c.TestState - running...
20:15:57.957 [main] DEBUG c.TestState - t2 state RUNNABLE
20:15:57.957 [main] DEBUG c.TestState - t3 state TERMINATED
20:15:57.957 [main] DEBUG c.TestState - t4 state TIMED_WAITING
20:15:57.957 [main] DEBUG c.TestState - t5 state WAITING
20:15:57.957 [main] DEBUG c.TestState - t6 state BLOCKED