Java并发编程学习笔记2——Java线程
目录
5.2、线程上下文切换(Thread Context Switch)
6.4.1、打断sleep(),wait(),join()的线程
1、创建和运行线程
1.1、方法1——直接使用Thread
// 创建线程对象
Thread t = new Thread() {
public void run() {
// 要执行的任务
}
};
// 启动线程
t.start();
例如:
// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1") {
public void run() {
log.debug("hello");
}
};
t1.start();
1.2、方法2——使用Runnable配合Thread
把【线程】和【任务】(要执行的代码)分开。
- Thread代表线程;
- Runnable可运行的任务(线程要执行的代码);
Runnable runnable = new Runnable() {
@Override
public void run() {
log.debug("hello");
}
};
// 参数1是任务对象,参数2是线程名字,推荐
Thread t2 = new Thread(runnable, "t2");
t2.start();
Java8以后可以使用lambda精简代码。
// 创建任务对象
Runnable task = () -> log.debug("hello");
// 参数1是任务对象,参数2是线程名字,推荐
Thread t3 = new Thread(task, "t3");
t3.start();
1.3、方法3——FutureTask配合Thread
FutureTask能够接收Callable类型的参数,用来处理有返回结果的情况。
// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("hello");
return 100;
}
});
// 参数1 是任务对象;参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程调用get()方法导致自身阻塞,同步等待task执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);
结果:
18:47:17.417 [t3] DEBUG com.bfbc.test.Test2 - hello
18:47:17.420 [main] DEBUG com.bfbc.test.Test2 - 结果是:100
2、原理之Thread与Runnable的关系
- 方法1是把线程和任务合并在了一起,方法2是吧线程和任务分开了;
- 用Runnable更容易与线程池等高级API配合;
- 用Runnable让任务类脱离了Thread继承体系,更灵活。
3、观察多个线程同时运行
new Thread(() -> {
while(true) {
log.debug("running");
}
}, "t1").start();
new Thread(() -> {
while(true) {
log.debug("running");
}
}, "t2").start();
4、查看进程线程的方法
4.1、Windows
- 任务管理器可以查看进程和线程数,也可以用来杀死进程;
- tasklist查看进程;
- taskkill杀死进程。
4.2、Linux
- ps -fe:查看所有进程;
- ps -fT -p <PID>:查看某个进程(PID)的所有线程;
- kill:杀死进程;
- top:按大写H切换是否显示进程;
- top -H -p <PID>:查看某个进程(PID)的所有线程。
4.3、Java
- jps命令查看所有Java进程;
- jstask <PID>:查看某个Java进程(PID)的所有线程状态;
- jconsole:查看某个Java进程中线程的运行情况(图形界面)。
jconsole远程监控配置:
- 需要以如下方式运行你的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类
- 修改/etc/hosts文件将127.0.0.1映射至主机名。
5、原理之线程运行
5.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 o = new Object();
return o;
}
}
5.2、线程上下文切换(Thread Context Switch)
因为以下一些原因导致CPU不再执行当前的线程,转而执行另一个线程的代码。
- 线程的CPU时间片用完;
- 垃圾回收;
- 有更高优先级的线程需要运行;
- 线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法。
当Context Switch发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条JVM指令的执行地址,是线程私有的。
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等;
- Context Switch频繁发生会影响性能。
6、线程中的常见方法
方法名 | static | 功能说明 | 注意 |
---|---|---|---|
start() | 启动一个新线程,在新的线程运行run()方法中的代码 | start()方法只是让线程进入就绪,里面代码不一定立刻运行(CPU的时间片还没分给它)。每个线程对象的start()方法只能调用一次,如果调用了多次会出现IllegalThreadStateException。 | |
run() | 新线程启动后会调用的方法 | 如果在构造Thread对象时传递了Runnable参数,则 线程启动后会调用Runnable中的run()方法,否则默认不执行任何操作,但可以创建Thread的子类对象,来覆盖默认行为。 | |
join() | 等待线程运行结束 | ||
join(long n) | 等待线程运行结束,最多等待n毫秒 | ||
getId() | 获取线程长整型的id | id唯一。 | |
getName() | 获取线程名 | ||
setName() | 修改线程名 | ||
getPriority() | 获取线程优先级 | ||
setPriority() | 修改线程优先级 | Java中规定线程优先级是1-10的整数,较大的优先级能提高该线程被CPU调度的几率。 | |
getState() | 获取线程状态 | Java中线程状态使用6个enum表示,分别为:NEW,RUNNABLE,BLOCKED,WAITING ,TIMED_WAITING,TERMINATED | |
isInterrupted() | 判断是否被打断 | 不会清除打断标记 | |
isAlive() | 线程是否存活(还没有运行完毕) | ||
interrupt() | 打断线程 | 如果打断线程正在sleep、wait、join会导致被打断的线程抛出InterruptException,并清除打断标记;如果打断的正在运行的线程,则会设置打断标记;park的线程被打断,也会设置打断标记。 | |
interrupted() | static | 判断当前线程是否被打断 | 会清除打断标记 |
currentThread() | static | 获取当前正在执行的线程 | |
sleep(long n) | static | 让当前执行的线程休眠n毫秒,休眠时间让出CPU的时间片给其他线程 | |
yield() | static | 提示线程调度器让出当前线程对CPU的使用 | 主要是为了测试和调试 |
6.1、start()与run()
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
}
};
t1.run();
}
结果:
19:24:07.580 [main] DEBUG com.bfbc.test.Test5 - running...
public static void main(String[] args) {
Thread t2 = new Thread() {
@Override
public void run() {
log.debug("running...");
}
};
System.out.println(t2.getState());
t2.start();
System.out.println(t2.getState());
}
结果:
NEW
RUNNABLE
19:24:49.309 [Thread-0] DEBUG com.bfbc.test.Test6 - running...
6.2、sleep()与yield()
sleep:
- 调用sleep会让当前线程从Running进入Timed Waiting状态;
- 其他线程可以使用interrupt()方法打断正在睡眠的线程,这时sleep()方法会抛出InterruptException;
- 睡眠结束后的线程未必会立刻得到执行;
- 建议用TimeUnit的sleep()来代替Thread的sleep()来获得更好的可读性。
yield:
- 调用yield会让当前线程从Running状态进入Runnable状态,然后调度执行其他同优先级的线程,如果这时没有同优先级的线程,那么不能保证当前线程暂停的效果;
- 具体的实现依赖于操作系统的任务调度器。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
Thread.sleep(500);
log.debug("t1 state : {}", t1.getState());
}
结果:
19:32:06.399 [main] DEBUG com.bfbc.test.Test7 - t1 state : TIMED_WAITING
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@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);
// TimeUnit.SECONDS.sleep(1); // 同Thread.sleep(1000)效果一样
log.debug("interrupt...");
t1.interrupt();
}
结果:
19:36:22.727 [Thread-0] DEBUG com.bfbc.test.Test8 - enter sleep...
19:36:23.725 [main] DEBUG com.bfbc.test.Test8 - interrupt...
19:36:23.725 [Thread-0] DEBUG com.bfbc.test.Test8 - wake up...
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.bfbc.test.Test8$1.run(Test8.java:13)
6.2.1、线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它;
- 如果CPU比较忙,那么优先级高的线程会获得更多的时间片,但CPU空闲时,优先级几乎没作用。
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();
}
6.3、join()
6.3.1、为什么需要join()
private 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("结束");
}
结果:
10:22:00.870 [main] DEBUG com.bfbc.test.Test12 - 开始
10:22:00.928 [Thread-0] DEBUG com.bfbc.test.Test12 - 开始
10:22:00.928 [main] DEBUG com.bfbc.test.Test12 - 结果为:0
10:22:00.930 [main] DEBUG com.bfbc.test.Test12 - 结束
10:22:00.930 [Thread-0] DEBUG com.bfbc.test.Test12 - 结束
分析:
- 因为主线程和线程t1是并行执行的,t1线程需要1秒之后才能算出r=10;
- 而主线程一开始就要打印r的结果,所以只能打印出r=0。
解决方法:用join(),加载t1.start()之后即可。
private 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(); // 主线程等待t1运行结束
log.debug("结果为:{}", r);
log.debug("结束");
}
结果:
10:26:17.260 [main] DEBUG com.bfbc.test.Test12 - 开始
10:26:17.310 [Thread-0] DEBUG com.bfbc.test.Test12 - 开始
10:26:17.325 [Thread-0] DEBUG com.bfbc.test.Test12 - 结束
10:26:17.325 [main] DEBUG com.bfbc.test.Test12 - 结果为:10
10:26:17.325 [main] DEBUG com.bfbc.test.Test12 - 结束
private static void test2() {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r = 10;
});
t1.start();
Thread t2 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r = 20;
});
t2.start();
try {
log.debug("join begin");
t1.join();
log.debug("t1 join end");
t2.join();
log.debug("t2 join end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
结果:
10:37:12.509 [main] DEBUG com.bfbc.test.Test12 - join begin
10:37:13.510 [main] DEBUG com.bfbc.test.Test12 - t1 join end
10:37:14.510 [main] DEBUG com.bfbc.test.Test12 - t2 join end
6.3.2、有时效的join()
private static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
Thread t1 = new Thread(()-> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r = 10;
});
t1.start();
//线程执行结束会导致join()结束
log.debug("join begin");
t1.join(1500);
log.debug("{}", r);
}
结果:
10:43:50.235 [main] DEBUG com.bfbc.test2.Test1 - join begin
10:43:51.739 [main] DEBUG com.bfbc.test2.Test1 - 0
6.4、interrupt()
6.4.1、打断sleep(),wait(),join()的线程
打断sleep()的线程,会清空打断状态(打断标记为false),以sleep()为例。
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(1000);
log.debug("interrupt...");
t1.interrupt();
log.debug("打断标记:{}", t1.isInterrupted());
}
结果:
10:50:40.342 [t1] DEBUG com.bfbc.test2.Test2 - sleep...
10:50:41.354 [main] DEBUG com.bfbc.test2.Test2 - interrupt...
10:50:41.354 [main] DEBUG com.bfbc.test2.Test2 - 打断标记:false
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.bfbc.test2.Test2.lambda$main$0(Test2.java:11)
at java.lang.Thread.run(Thread.java:748)
6.4.2、打断正常运行的线程
打断正常运行的线程,不会清空打断状态(打断标记仍为true)。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(!Thread.currentThread().isInterrupted()) {
log.debug("running...");
}
log.debug("被打断了,退出循环...");
}, "t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt...");
t1.interrupt();
log.debug("打断标记:{}", t1.isInterrupted());
}
结果:
...
10:57:34.985 [t1] DEBUG com.bfbc.test2.Test2 - running...
10:57:34.985 [t1] DEBUG com.bfbc.test2.Test2 - running...
10:57:34.985 [t1] DEBUG com.bfbc.test2.Test2 - running...
10:57:34.993 [main] DEBUG com.bfbc.test2.Test2 - interrupt...
10:57:34.994 [main] DEBUG com.bfbc.test2.Test2 - 打断标记:true
10:57:34.996 [t1] DEBUG com.bfbc.test2.Test2 - 被打断了,退出循环...
6.4.3、模式之两阶段终止
两阶段终止模式(Two Phase Termination):在一个线程T1中如何“优雅”地终止线程T2?这里的“优雅”指的是给T2一个料理后事的机会。
错误思路:
- 使用线程对象的stop()方法停止线程。stop()方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其他线程将永远无法获取锁。
- 使用System.exit(int)方法停止线程。目的仅是停止一个线程,但这种做法会让整个程序都停止。
正确思路:
package com.bfbc.test2;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Test3 {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination twoPhaseTermination = new TwoPhaseTermination();
twoPhaseTermination.start();
Thread.sleep(3500);
twoPhaseTermination.stop();
}
}
@Slf4j()
class TwoPhaseTermination {
private Thread monitor;
// 启动监控线程
public void start() {
monitor = new Thread(()-> {
while(true) {
Thread thread = Thread.currentThread();
if (thread.isInterrupted()) {
log.debug("料理后事...");
break;
}
try {
Thread.sleep(1000); // 情况1
log.debug("执行监控记录..."); // 情况2
} catch (InterruptedException e) {
e.printStackTrace();
// 重新设置打断标记
thread.interrupt();
}
}
});
monitor.start();
}
//停止监控线程(设置打断标记)
public void stop() {
monitor.interrupt();
}
}
结果:
11:24:13.268 [Thread-0] DEBUG com.bfbc.test2.TwoPhaseTermination - 执行监控记录...
11:24:14.269 [Thread-0] DEBUG com.bfbc.test2.TwoPhaseTermination - 执行监控记录...
11:24:15.269 [Thread-0] DEBUG com.bfbc.test2.TwoPhaseTermination - 执行监控记录...
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.bfbc.test2.TwoPhaseTermination.lambda$start$0(Test3.java:30)
at java.lang.Thread.run(Thread.java:748)
11:24:15.770 [Thread-0] DEBUG com.bfbc.test2.TwoPhaseTermination - 料理后事...
6.4.4、打断park线程
打断park线程,不会清空打断状态(打断标记仍为true)。
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(1000);
t1.interrupt(); // t1停下来之后,打断t1,t1继续执行后面的代码
Thread.sleep(1000);
t1.interrupt(); // t1停下来之后,打断t1,t1继续执行后面的代码
}
结果:
11:36:55.081 [t1] DEBUG com.bfbc.test2.Test4 - park...
11:36:56.096 [t1] DEBUG com.bfbc.test2.Test4 - unpark...
11:36:56.096 [t1] DEBUG com.bfbc.test2.Test4 - 打断状态:true
11:36:57.115 [t1] DEBUG com.bfbc.test2.Test4 - unpark
6.5、不推荐的方法
这些方法已经过时,容易破坏同步代码块,造成线程死锁。
方法名 | static | 功能说明 |
---|---|---|
stop() | 停止线程运行 | |
suspend() | 挂起(暂停)线程运行 | |
resume() | 恢复线程运行 |
6.6、主线程与守护线程
默认情况下,Java进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
public static void main(String[] args) throws InterruptedException {
log.debug("开始运行...");
Thread t1 = new Thread(()->{
log.debug("开始运行...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("运行结束");
}, "daemon");
t1.setDaemon(true);
t1.start();
Thread.sleep(1000);
log.debug("运行结束...");
}
结果:
13:46:15.764 [main] DEBUG com.bfbc.test2.Test5 - 开始运行...
13:46:15.815 [daemon] DEBUG com.bfbc.test2.Test5 - 开始运行...
13:46:16.828 [main] DEBUG com.bfbc.test2.Test5 - 运行结束...
注意:
- 垃圾回收器线程就是一种守护线程;
- Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdown命令后,不会等待它们处理完当前请求。
6.7、线程的五种状态
这是从操作系统层面来描述的。
- 【初始状态】:仅是在语言层面创建了线程对象,还未与操作系统线程关联;
- 【可运行状态(就绪状态)】:指该线程已经被创建(与操作系统线程关联),可以由CPU调度执行;
- 【运行状态】:指获取了CPU时间片运行中的状态。当CPU时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程上下文切换,进入【阻塞状态】;
- 【阻塞状态】:如果调用了阻塞API,如BIO读写文件,这时该线程实际不会用到CPU,会导致线程上下文切换,进入【阻塞状态】;等BIO操作完毕,会由操作系统唤醒阻塞进程,转换至【可运行状态】;与【可运行状态】的区别是,对【阻塞状态】的线程来说,只要它们一直不唤醒,调度器就一直不会考虑调度它们;
- 【终止状态】:表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态。
6.8、线程的六种状态
这是从Java API层面来描述的。根据Thread.State枚举,分为六种状态:
- 【NEW】:线程刚被创建,但是还没有调用start()方法;
- 【RUNNABLE】:当调用了start()方法后,注意Java API层面的RUNNABLE状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【阻塞状态】(由于BIO导致的线程阻塞,在Java里无法区分,仍然认为是可运行);BLOCKED、WAITING、TIMED_WAITING都是Java API层面对【阻塞状态】的细分。
- 【TERMINATED】:当线程代码运行结束,进入此状态。
@Slf4j
public class Test6 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("running...");
}, "t1");
Thread t2 = new Thread(() -> {
while (true) {
// 运行 RUNNABLE 状态
}
}, "t2");
t2.start();
Thread t3 = new Thread(() -> {
log.debug("running...");
}, "t3");
t3.start();
Thread t4 = new Thread(() -> {
synchronized (Test6.class) {
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t4");
t4.start();
Thread t5 = new Thread(()->{
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t5");
t5.start();
Thread t6 = new Thread(()->{
synchronized (Test6.class) { // BLOCKED
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t6");
t6.start();
Thread.sleep(500);
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());
}
}
结果:
14:20:37.523 [t3] DEBUG com.bfbc.test2.Test6 - running...
14:20:38.025 [main] DEBUG com.bfbc.test2.Test6 - t1 state NEW
14:20:38.026 [main] DEBUG com.bfbc.test2.Test6 - t2 state RUNNABLE
14:20:38.026 [main] DEBUG com.bfbc.test2.Test6 - t3 state TERMINATED
14:20:38.026 [main] DEBUG com.bfbc.test2.Test6 - t4 state TIMED_WAITING
14:20:38.026 [main] DEBUG com.bfbc.test2.Test6 - t5 state WAITING
14:20:38.026 [main] DEBUG com.bfbc.test2.Test6 - t6 state BLOCKED
7、案例
7.1、案例1——防止CPU占用100%
7.1.1、sleep实现
在没有利用CPU来讲计算时,不要让while(true)空转浪费CPU,这时可以使用yield或sleep来让出CPU的使用权给其他程序。
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
}
- 可以用wait或条件变量达到类似的效果;
- 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景;
- sleep适用于无需锁同步的场景。
7.1.2、wait实现
7.2、案例2——应用之同步
以调用方角度来讲,如果:
- 需要等待结果返回,才能继续运行就是同步;
- 不需要等待结果返回,就能继续运行就是异步。