1.11 手把手教你从多线程到线程池
多线程
一、概念
1、并发与并行
- 并行:指两个或多个事件在同一时刻发生(同时发生)。
- 并发:指两个或多个事件在同一个时间段内发生。
单CPU系统中: 每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。
多CPU系统中: 则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序。
2、线程与进程
根本区别:
进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
进程: 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多
个进程;
线程: 进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可
以有多个线程的,这个应用程序也可以称之为多线程程序。
进程和线程的关系
- Java对操作系统提供的功能进行封装,包括进程和线程。
- 运行一个程序会产生一个进程,进程包含至少一个线程。
- 每个进程对应一个JVM实例,多个线程共享JVM里的堆。
- Java采用单线程编程模型,程序会自动创建主线程。
- 主线程可以创建子线程,原则上要后于子线程完成执行。
二、创建多线程
Java语言提主要供了两种实现线程的方式:
- 继承Thread类创建线程类
- 实现Runnable接口创建线程类
1、继承Thread方式(extends Thread)
实现步骤:
- 定义类继承Thread类,并重写Thread类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象。
- 调用线程对象的start()方法来启动该线程。
自定义线程类:
public class MyThread extends Thread {
/**
* 利用继承中的特点 * 将线程名称传递 进行设置
*/
public MyThread(String name) {
super(name);
}
/**
* 重写run方法 * 定义线程要执行的代码
*/
public void run() {
for (int i = 0; i < 20; i++) {
//getName()方法 来自父亲
System.out.println(getName() + i);
}
}
}
测试类:
public class Demo {
public static void main(String[] args) {
System.out.println("这里是main线程");
MyThread mt = new MyThread("小强");
mt.start();//开启了一个新的线程
for (int i = 0; i < 20; i++) {
System.out.println("旺财:"+i);
}
流程图:
2、实现Runnable方式(implements Runnable)
定义实现类:
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
定义测试类:
public class Demo {
public static void main(String[] args) {
//创建自定义类对象 线程任务对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t = new Thread(mr, "小强");
t.start();
for (int i = 0; i < 20; i++) {
System.out.println("旺财 " + i);
}
3、Thread和Runnable的区别:
- Thread是一个类,实现Runnable接口,里面含有start( )方法。
- Runnable是一个接口,里面只有run( )方法。
- Thread是实现了Runnable接口的类,使得run支持多线程。
- 因类的单一继承原则,推荐多实用Runnable接口。
实现了Runable接口的有点:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
4、使用匿名内部类的方式实现Runnable接口:
public class NoNameInnerClassThread {
public static void main(String[] args) {
Runnable r = new Runnable(){
public void run(){
for (int i = 0; i < 20; i++) {
System.out.println("张宇:"+i);
};
new Thread(r).start();
for (int i = 0; i < 20; i++) {
System.out.println("费玉清:"+i);
}
5、实现Callable接口(重写call方法)
实现类
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("Start to work");
Thread.sleep(5000);
System.out.println("End done");
return "结束";
}
}
FutureTask方式开启新线程
import java.util.concurrent.FutureTask;
public class FutureTaskDemo {
public static void main(String[] args) throws Exception {
FutureTask<String> task = new FutureTask<String>(new MyCallable());
//启动线程
new Thread(task).start();
// 线程等待
if (!task.isDone()) {
System.out.println("等待中。。。");
}
System.out.println("返回结果为:" + task.get());
}
}
执行结果:
线程池(ThreadPool)方式开启新线程
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 1、创建线程池
ExecutorService pool = Executors.newCachedThreadPool();
// 2、获取返回的结果
Future<String> result = pool.submit(new MyCallable());
// 3、线程等待
if (!result.isDone()){
System.out.println("等待中。。。");
}
try {
// 4、获取返回值
System.out.println(result.get());
} catch (Exception e) {
e.printStackTrace();
}finally {
// 5、关闭线程池
pool.shutdown();
}
}
}
执行结果:
三、线程同步(三种方式):
1、同步代码块:
synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
synchronized(同步锁){
需要同步操作的代码
}
代码:
public class Ticket implements Runnable {
private int ticket = 100;
Object lock = new Object();
// 执行卖票操作
@Override
public void run() {
// 每个窗口卖票的操作
// 窗口 永远开启
while (true) {
synchronized (lock) {
if (ticket > 0) {//有票 可以卖
// 出票操作
// 使用sleep模拟一下出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
}
}
}
}
2、同步方法:
同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着
格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
代码:
public class Ticket implements Runnable {
private int ticket = 100;
//执行卖票操作
@Override
public void run() {
// 每个窗口卖票的操作
// 窗口 永远开启
while (true) {
sellTicket();
}
}
// 锁对象是谁调用这个方法就是谁
// 隐含锁对象就是 this
public synchronized void sellTicket() {
if (ticket > 0) {//有票 可以卖
// 出票操作
// 使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
}
}
3、Lock锁:(ReentrantLock)
格式:
public void lock() :加同步锁。
public void unlock() :释放同步锁
代码:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Ticket implements Runnable {
private int ticket = 100;
Lock lock = new ReentrantLock();
//执行卖票操作
@Override
public void run() {
// 每个窗口卖票的操作
// 窗口 永远开启
while (true) {
lock.lock();
if (ticket > 0) {//有票 可以卖
// 出票操作
// 使用sleep模拟一下出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
} lock.unlock();
}
}
}
四、线程状态
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。 |
Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
TimedWaiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep. |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
list.wait(); 进入到等待状态
list.notify();唤醒获取元素的线程
五、Thread常用方法:
1、构造方法:
public Thread() :分配一个新的线程对象。
public Thread(String name) :分配一个指定名字的新的线程对象。
public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字
2、常用方法:
public String getName() : 获取当前线程名称。
public void start() : 导致此线程开始执行; Java虚拟机调用此线程的run方法。
public void run() : 此线程要执行的任务在此处定义代码。
public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
public static Thread currentThread() :返回对当前正在执行的线程对象的引用。
getId() :返回该线程的唯一标识符。
3、isAlive()
方法isAlive()的功能是判断当前线程是否处于活跃状态。处于已经启动且尚未终止。
创建MyThread类
public class MyThread extends Thread{
@Override
public void run() {
System.out.println("run="+this.isAlive());
}
}
运行Test类中方法
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
System.out.println("begin =="+myThread.isAlive());
myThread.start();
System.out.println("end =="+myThread.isAlive());
Thread.sleep(1000);
System.out.println("timeOut =="+myThread.isAlive());
}
}
运行结果如图:
4、sleep()和wait()方法
区别:
- Sleep是Thread类的方法,wait是 Objects类中定义的方法
- Sleep方法可以在任何地方使用。
- wait方法只能在 synchronized方法或 synchronized块中使用
- Thread.sleep只会让出CPU,不会导致锁行为的改变。
- Object.wait不仅让出CPU,还会释放已经占有的同步资源锁。
void wait(): | 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。 |
---|---|
void wait(long timeout): | 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。 |
void wait(long timeout, int nanos): | 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待。 |
public class WaitSleepDemo {
public static void main(String[] args) {
final Object lock = new Object();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread A is wait to get lock");
// 1、获取锁
synchronized (lock) {
try {
// 2、休眠20ms
Thread.sleep(20);
System.out.println("thread A get lock");
// 3、调用wait,释放锁
lock.wait(1000);
System.out.println("thread A is done");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
5、notify()和notifyAll()
方法sleep()的作用是在指定的毫秒数让当前“当前正在执行的线程”休眠(暂停执行)。
锁池(EntryList):
假设线程A已经拥有了某个对象(不是类)的锁,而其它线程B、C想要调用这个对象的某个 synchronized方法(或者块),由于B、C线程在进入对象的synchronized方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正被线程A所占用,此时B、C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池(等待锁的池子)。
等待池(WaitSet):
假设线程A调用了某个对象的wait( )方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,进入到等待池中的线程不会去竞争该对象的锁。除非被 notify( )、notifyAll( ) 唤醒才行。
notify( )和notifyAll( )区别
notifyAll( ): 会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会。
notify( ): 只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会。
唤醒线程:
public class Test {
private static Test test = new Test();
public static void main(String[] args) throws InterruptedException {
// 1、线程等待wait()
Runnable runnable = new Runnable() {
@Override
public void run() {
test.go();
}
};
new Thread(runnable, "AA_01").start();
// 2、唤醒线程notify();
Thread.sleep(1000);
new Thread(new Runnable() {
@Override
public void run() {
test.goDown();
}
}, "notifyAll").start();
}
// 调用线程等待的wait()方法
private synchronized void go() {
System.out.println("start: " + Thread.currentThread().getName());
try {
this.wait();
System.out.println("end: " + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 调用唤醒线程的notify()方法
private synchronized void goDown() {
System.out.println("start: " + Thread.currentThread().getName());
notifyAll();
System.out.println("end: " + Thread.currentThread().getName());
}
}
6、yied()暂停线程
当调用Thread.yield( )函数时,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示。
7、Interrupt( )中断线程
已经被废弃的:
通过调用Stop( )方法停止线程。
通过调用suspend( )和resume( )方法
目前使用的方法:
- 调用 Interrupt( ),通知线程应该中断了。
- 如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常(sleep时候)。
- 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。
public static void main(String[] args) throws InterruptedException {
Runnable r = new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
执行任务
}
System.out.println("000");
}
};
Thread t1 = new Thread(r, "Start");
1、启动线程
t1.start();
Thread.sleep(100);
2、终止线程
t1.interrupt();
}
六、线程的入参和返回值
1、给run()方法传参
- 构造函数传参
- 成员变量传参
- 回调函数传参
2、处理线程的返回
- 主线程等待法
- 使用Thread类的join()阻塞当前线程等待子线程处理完毕
- 通过Callable接口:通过FutureTask或者线程池实现
1.主线程等待
当主线程没有获取到值时,主线程等待子线程完成赋值。、
优点:实现简单,
缺点:
- 自己需要实现循环等待逻辑,等待变量越多,代码异常臃肿。
- 循环多久是不确定的。
子线程:
public class CycleWait implements Runnable {
private String name;
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
name = "we have date now";
}
}
主线程:
// 主线程
public static void main(String[] args) {
CycleWait cw = new CycleWait();
Thread t = new Thread(cw);
t.start();
// 当辅助线程中name没有值时,主线程等待
while (cw.name == null) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(cw.name);
}
2.使用Thread类的join()阻塞当前线程等待子线程处理完毕
使用Thread里面的join( )方法,替代主线程等待法代码。
优点:更精准控制,代码更简单。
缺点:力不不够细,不能指定等待某个子线程完成再执行。
3.通过Callable接口:通过FutureTask或者线程池实现
子线程:
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
String value = "test";
System.out.println("Ready to work");
Thread.sleep(5000);
System.out.println("task done");
return value;
}
}
使用FutureTask方式:
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class FutureTaskDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> task = new FutureTask<String>(new MyCallable());
//启动线程
new Thread(task).start();
// 线程等待
if (!task.isDone()){
System.out.println("wait");
}
System.out.println("task return="+task.get());
}
}
使用线程池方式:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 1、创建线程池
ExecutorService pool = Executors.newCachedThreadPool();
// 2、获取返回的结果
Future<String> result = pool.submit(new MyCallable());
// 3、线程等待
if (!result.isDone()){
System.out.println("wait ..");
}
try {
// 4、获取返回值
System.out.println(result.get());
} catch (Exception e) {
e.printStackTrace();
}finally {
// 5、关闭线程池
pool.shutdown();
}
}
}
七、线程池
线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
三个优点:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用;
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。
execute和submit区别
执行给定的任务(command-可运行的任务)(无返回值)
void execute(Runnable command)
提交一个 Runnable 任务用于执行,(有返回值)
Future<?> submit(Runnable task)
线程池的结构:
1、可缓存线程池:Executors.newCacheThreadPool()
先查看池中有没有以前建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务。
当执行当前任务时上一个任务已经完成,会复用执行上一个任务的线程,而不用每次新建线程。
创建线程池:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
使用线程池中线程:
cachedThreadPool.execute(Runnable command);
代码:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class NewCachedThreadPoolTest {
public static void main(String[] args) {
// 创建一个可缓存线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
try {
// sleep可明显看到使用的是线程池里面以前的线程,没有创建新的线程
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
cachedThreadPool.execute(new Runnable() {
public void run() {
// 打印正在执行的缓存线程信息
System.out.println(Thread.currentThread().getName()
+ "正在被执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
2、固定个数的线程池:Executors.newFixedThreadPool(int n)
创建一个可重用固定个数的线程池,以共享的无界队列方式来运行这些线程。
定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()
创建线程池:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
使用线程池中线程:
fixedThreadPool.execute(Runnable command);
代码:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class NewFixedThreadPoolTest {
public static void main(String[] args) {
// 1、创建一个可重用固定个数的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
// 2、使用线程池中线程
fixedThreadPool.execute(new Runnable() {
public void run() {
try {
// 打印正在执行的缓存线程信息
System.out.println(Thread.currentThread().getName()
+ "正在被执行");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
3、定长线程池,支持定时任务:Executors.newScheduledThreadPool(int n)
创建一个定长线程池,支持定时及周期性任务执行.
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
延迟指定delay
秒数执行:
ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
延迟 initialDelay
秒后每period
秒执行一次
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,
long period,TimeUnit unit);
代码:
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class NewScheduledThreadPoolTest {
public static void main(String[] args) {
//创建一个定长线程池,支持定时及周期性任务执行——延迟执行
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// //1、延迟1秒执行
// scheduledThreadPool.schedule(new Runnable() {
// public void run() {
// System.out.println("延迟1秒执行");
// }
// }, 1, TimeUnit.SECONDS);
//2、延迟1秒后每3秒执行一次
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
public void run() {
System.out.println("延迟1秒后每3秒执行一次");
}
}, 1, 3, TimeUnit.SECONDS);
}
}
4、单线程化的线程池:Executors.newSingleThreadExecutor()
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
创建线程池:
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
使用线程池中线程:
singleThreadExecutor.execute(Runnable command);
代码:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class NewSingleThreadExecutorTest {
public static void main(String[] args) {
// 1、创建一个单线程化的线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
// 2、使用线程池中线程
singleThreadExecutor.execute(new Runnable() {
public void run() {
try {
// 结果依次输出,相当于顺序执行各个任务
System.out.println(Thread.currentThread().getName() + "正在被执行,打印的值是:" + index);
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
5、Spring提供的线程池:ThreadPoolTaskExecutor
1、配置类的方式配置线程池,并注入
@Configuration
public class ExecturConfig {
@Bean("taskExector")
public Executor taskExector() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
int i = Runtime.getRuntime().availableProcessors();//获取到服务器的cpu内核
executor.setCorePoolSize(5);//核心池大小
executor.setMaxPoolSize(100);//最大线程数
executor.setQueueCapacity(1000);//队列最大长度
executor.setKeepAliveSeconds(1000);//线程池维护线程所允许的空闲时间
executor.setThreadNamePrefix("tsak-asyn");//线程前缀名称
//配置拒绝策略【线程池对拒绝任务(无线程可用)的处理策略】
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
return executor;
}
}
2、基于xml配置的方式创建
<!-- spring线程池 -->
<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<!-- 核心线程数 -->
<property name="corePoolSize" value="10"/>
<!-- 最大线程数 -->
<property name="maxPoolSize" value="200"/>
<!-- 队列最大长度 >=mainExecutor.maxSize -->
<property name="queueCapacity" value="10"/>
<!-- 线程池维护线程所允许的空闲时间 -->
<property name="keepAliveSeconds" value="20"/>
<!-- 线程池对拒绝任务(无线程可用)的处理策略 -->
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy"/>
</property>
</bean>
自动注入的方式注入线程池
@Resource(name="taskExecutor")
ThreadPoolTaskExecutor taskExecutor;
// 或者可以直接@Autowried
@AutoWired
ThreadPoolTaskExecutor taskExecutor