一、多线程

1 、并发与并行

  • 并行:指两个或多个事件在同一时刻发生(同时执行)。
  • 并发:指两个或多个事件在同一个时间段内发生(交替执行)。

image.png

注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,cpu会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

2、 线程与进程

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
  • 线程:是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

    进程与线程的区别

  • 进程:有独立的内存空间,进程是程序的一次执行过程。

  • 线程:是进程中的一个执行单元,一个进程中至少有一个线程,一 进程中也可以有多个线程。

    线程调度

  • 分时调度 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

    3、Thread类(java.lang.Thread类)

    构造方法

    | 方法 | 说明 | | —- | —- | | public Thread() | 创建线程对象 | | public Thread(String name) | 创建线程对象并指定线程名字 | | public Thread(Runnable target) | 使用Runnable创建线程 | | public Thread(Runnable target,String name) | 使用Runable创建线程并指定线程名字 |

常用方法

方法 说明
String getName() 获取线程的名字
void start() 开启线程,每个对象只调用一次start
void run() run方法写线程执行的代码,此线程要执行的任务在此处定义代码
static void sleep(long millis) 让当前线程睡指定的时间
static Thread currentThread() 获取当前线程对象

4、实现多线程方式

继承Thread

将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法 步骤: A:自定义一个类,继承Thread,这个类称为线程类;
B:重写Thread类中的run方法,run方法中就是线程要执行的任务代码;
C:创建线程类的对象;
D:启动线程,执行任务;

实现Runnable接口

创建线程的另一种方法是声明一个实现Runnable接口的类。 那个类然后实现了run方法。 然后可以分配类的实例,在创建Thread时作为参数传递,并启动。
构造方法:

  • public Thread(Runnable target):分配一个带有指定目标新的线程对象。
  • public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象并指定名字。

    关于调用run方法的疑问

    1、自定义类继承Thread类,重写run方法,调用start启动线程,会自动调用我们线程类中的run方法。
    2、自定义类,实现Runnable接口,把任务对象传给Thread对象。调用Thread对象的start方法,执行Thread的run。那么为什么最后执行的是任务类中的run呢?
    image.png

    实现Runnable接口的好处

    1、避免了Java单继承的局限性;
    2、把线程代码和任务的代码分离,解耦合(解除线程代码和任务的代码模块之间的依赖关系)。代码的扩展性非常好

    匿名内部类方式实现线程的创建

    使用匿名内部类的方式实现Runnable接口,重新复写Runnable接口中的run方法。
    1. public static void main(String[] args) {
    2. //使用匿名内部类实现多线程 r表示任务类的对象
    3. Runnable r=new Runnable(){
    4. public void run() {
    5. for (int i = 0; i < 10; i++) {
    6. System.out.println(Thread.currentThread().getName()+"---"+i);
    7. }
    8. }
    9. };
    10. //创建线程类对象
    11. Thread t1=new Thread(r,"t1");
    12. Thread t2=new Thread(r,"t2");
    13. //启动线程
    14. t1.start();
    15. t2.start();
    16. }

    五、线程控制

    线程休眠

    使用Thread类中的sleep()函数可以让线程休眠, static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)

二、线程安全

1、多线程安全的问题

多个线程在对共享数据进行读改写的时候,可能导致的数据错乱就是线程的安全问题了

2、多线程安全问题分析

image.png
从上图可以看出多线程存在的问题:四个窗口同时出售同一张票、重复票、跳票等问题。

原因分析:

A:多线程程序,如果是单线程就不会出现上述卖票的错误信息;B:多个线程操作共享资源,如果多线程情况下,每个线程操作自己的也不会出现上述问题;
C:操作资源的代码有多行,如果代码是一行或者很少的情况下,那么一行代码很快执行完毕,也不会出现上述情况;
D:CPU的随机切换。本质原因是CPU在处理多个线程的时候,在操作共享数据的多条代码之间进行切换导致的;

3、多线程安全问题解决

可以人为的控制CPU在执行某个线程操作共享数据的时候,不让其他线程进入到操作共享数据的代码中去,这样就可以保证安全。 上述的这个解决方案:称为线程的同步。使用 synchronized关键字。

synchronized关键字概述:

  • synchronized关键字:表示“同步”的。它可以对“多行代码”进行“同步”——将多行代码当成是一个完整的整体,一个线程如果进入到这个代码块中,会全部执行完毕,执行结束后,其它线程才会执行。这样可以保证这多行的代码作为完整的整体,被一个线程完整的执行完毕。
  • synchronized被称为“重量级的锁”方式,也是“悲观锁”——效率比较低。
  • synchronized有几种使用方式:a).同步代码块b).同步方法【常用】

4、线程同步机制

同步代码块:

  • synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
    1. synchronized(同步锁){
    2. 需要同步操作的代码
    3. }

    同步锁:

    对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.

    1. 锁对象 可以是任意类型。
    2. 多个线程对象要使用同一把锁才能起到同步作用。
    3. 操作共享数据的代码需要加同步

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED锁阻塞)。

同步方法:

image.png
上图方法中加了个synchronized关键字代表同步方法.
注意:
1.非静态同步方法的锁是this;
2.如果一个方法内部,所有代码都需要被同步,那么就用同步方法;

静态同步方法

既然有非静态同步方法,那么肯定也会有静态同步方法。 将上述非静态同步方法改为静态同步方法,代码如下所示:

image.png问题:非静态同步方法有隐式变量this作为锁,那么静态方法中没有this,那么静态同步方法中的锁又是什么呢?
静态同步方法的锁是:当前类的字节码文件对象(Class对象)。 总结: 同步代码块:锁是任意对象,但是必须唯一;
非静态同步方法:锁是this;
静态同步方法:锁是当前类的字节码文件对象;类名.class

三、Lock锁

Lock锁也称同步锁

  • public void lock():加同步锁。
  • public void unlock():释放同步锁。

由于Lock属于接口,不能创建对象,所以我们可以使用它的子类ReentrantLock来创建对象并使用Lock接口中的函数
案例:

  1. /*
  2. * 需求:使用Lock实现线程安全的卖票。
  3. * Lock是接口,只能通过他的子类ReentrantLock创建对象
  4. * 构造函数 ReentrantLock() 创建一个 ReentrantLock 的实例。
  5. * void lock() 获取锁。
  6. * void unlock() 试图释放此锁。
  7. */
  8. //定义一个任务类用来卖票
  9. class SellTicketTask implements Runnable
  10. {
  11. //定义100张票
  12. private static int tickets=100;
  13. //创建对象作为任意一把锁
  14. // private Object obj=new Object();
  15. //定义一把锁
  16. Lock l=new ReentrantLock();
  17. //模拟卖票
  18. public void run() {
  19. /*while(true)
  20. {
  21. synchronized (obj) {
  22. if(tickets>0)
  23. {
  24. System.out.println(Thread.currentThread().getName()+"出票:"+tickets--);
  25. }
  26. }
  27. }*/
  28. //使用Lock锁替换synchronized
  29. while(true)
  30. {
  31. //获取锁
  32. l.lock();
  33. if(tickets>0)
  34. {
  35. try {Thread.sleep(1);} catch (InterruptedException e) {}
  36. System.out.println(Thread.currentThread().getName()+"出票:"+tickets--);
  37. }
  38. //释放锁
  39. l.unlock();
  40. }
  41. }
  42. }
  43. public class SellTicketDemo {
  44. public static void main(String[] args) {
  45. // 创建任务类对象
  46. SellTicketTask stt = new SellTicketTask();
  47. //创建线程对象
  48. Thread t1 = new Thread(stt,"窗口1");
  49. Thread t2 = new Thread(stt,"窗口2");
  50. Thread t3 = new Thread(stt,"窗口3");
  51. Thread t4 = new Thread(stt,"窗口4");
  52. //启动线程
  53. t1.start();
  54. t2.start();
  55. t3.start();
  56. t4.start();
  57. }
  58. }

四、死锁

1、什么是死锁?

:::danger 是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待的现象。 :::

2、产生死锁的条件

:::danger

  1. 有多把锁
  2. 有多个线程
  3. 有同步代码块嵌套 ::: 1)创建一个任务类DeadLockTask 实现Runnable接口,复写run函数;2)创建两个Object类的对象lock_a,lock_b作为锁对象;
    3)定义一个变量flag,让不同的线程切换到不同的地方去执行,按照不同的方式来获取锁;
    4)在run函数中使用if-else结构来控制两个线程去执行不同的内容,并使用while循环一直让其执行;
    5)在if中嵌套书写两个同步代码块lock_a和lock_b分别作为两个代码块的锁,将if中相同的内容复制一份写到else中;
    6)创建测试类DeadThreadLockDemo,在这个类的主函数中创建任务类的对象;
    7)创建两个线程对象t1和t2;
    8)让主线程休息1毫秒;
    9)使用t1对象调用start函数开启线程,让下一个线程进入到else中;
    10)开启t2线程;
  1. /*
  2. * 演示线程死锁的问题
  3. */
  4. //定义一个线程任务类
  5. class DeadLockTask implements Runnable
  6. {
  7. //定义两个锁对象
  8. private Object lock_a=new Object();
  9. private Object lock_b=new Object();
  10. //定义一个变量作为标记,控制取锁的方式
  11. boolean flag=true;
  12. public void run() {
  13. //当线程进来之后,一个线程进入到if中,另一个进入到else中
  14. if(flag)
  15. {
  16. while(true)
  17. {
  18. synchronized(lock_a)
  19. {
  20. System.out.println(Thread.currentThread().getName()+"if.....lock_a");
  21. synchronized(lock_b)
  22. {
  23. System.out.println(Thread.currentThread().getName()+"if.....lock_b");
  24. }
  25. }
  26. }
  27. }else
  28. {
  29. while(true)
  30. {
  31. synchronized(lock_b)
  32. {
  33. System.out.println(Thread.currentThread().getName()+"else.....lock_b");
  34. synchronized(lock_a)
  35. {
  36. System.out.println(Thread.currentThread().getName()+"else.....lock_a");
  37. }
  38. }
  39. }
  40. }
  41. }
  42. }
  43. public class DeadThreadLockDemo {
  44. public static void main(String[] args) {
  45. // 创建任务类对象
  46. DeadLockTask dlt = new DeadLockTask();
  47. //创建线程对象
  48. Thread t1 = new Thread(dlt);
  49. Thread t2 = new Thread(dlt);
  50. //开启第一个线程
  51. t1.start();
  52. //修改标记让下一个线程进入到else中
  53. dlt.flag=false;
  54. t2.start();
  55. }
  56. }

注意:在开发中一旦发生了死锁现象,不能通过程序自身解决。必须修改程序的源代码。

五、线程状态

1、概述

:::danger 线程由生到死的完整过程。
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State这个枚举中给出了六种线程状态 ::: | 线程状态 | 导致状态发生条件 | | —- | —- | | NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程对象,没有线程特征。 | | Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用了t.start()方法 :就绪(经典教法) | | Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 | | Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 | | Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 | | Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |

image.png

六、等待唤醒机制(包子铺卖包子)

1、Object类的方法

:::danger wait() :让当前线程进入等待状态,并且释放锁对象
notify(): 唤醒一个正在等待的线程,唤醒是随机的
void notifyAll() 唤醒在此对象监视器上等待的所有线程。
注意事项: 必须要使用锁对象来调用的。 :::

两个方法为什么要定义在Object类中?

因为需要用锁对象调用这两个方法,任意对象都可以作为锁对象。 也就是说任意类型的对象都可以调用的两个方法,就需要定义在Object类中

两个方法必须写在同步里面吗?

两个方法必须要在同步里面调用,因为在同步里面才有锁对象。
1.定义一个包子类,类中成员变量: :::danger pi //皮儿
xian //馅儿
flag://用来表示有没有包子,用true来代表有 用false来代表没有 ::: 2.定义一个生产包子的任务类即生产者线程类: :::danger 生产者线程思想:如果有包子就不需要制作,让生产者线程进入等待状态;如果没有包子,开始制作包子,并且唤醒消费者线程来吃包子 ::: 3.定义一个消费包子的任务类即消费者线程类: :::danger 消费者线程思想:如果没有包子就不消费,让消费者线程进入等待状态;如果有包子,开始吃包子,并且唤醒生产者线程来生产包子 :::

  1. /*
  2. 包子类需要定义3个成员变量:
  3. pi
  4. xian
  5. flag:表示是否有包子
  6. */
  7. //包子类
  8. public class BaoZi {
  9. //皮儿
  10. String pi;
  11. //馅儿
  12. String xian;
  13. //布尔值
  14. boolean flag=false; //用来表示有没有包子,用true来代表有 用false来代表没有
  15. }
  16. //生产包子:生产者线程执行的任务
  17. /*
  18. 生产者线程思想:如果有包子就不需要制作,让生产者线程进入等待状态;如果没有包子,开始制作包子,并且唤醒消费者线程来吃包子
  19. */
  20. public class ZhiZuo implements Runnable {
  21. //成员变量
  22. BaoZi baoZi;
  23. //构造方法
  24. public ZhiZuo(BaoZi baoZi) {
  25. this.baoZi = baoZi;
  26. }
  27. @Override
  28. public void run() {
  29. //制作包子
  30. while (true){
  31. synchronized ("锁"){//t1
  32. if(baoZi.flag == true){
  33. //如果有包子就不需要制作
  34. //就让制作的线程进入等待状态
  35. try {
  36. "锁".wait();
  37. } catch (InterruptedException e) {
  38. }
  39. }else{
  40. //else表示没有包子
  41. //制作包子
  42. baoZi.pi = "白面";
  43. baoZi.xian = "韭菜大葱";
  44. //修改包子状态
  45. baoZi.flag = true;
  46. System.out.println("生产出了一个包子!");
  47. //生产好了包子叫醒吃货(消费者)来吃
  48. "锁".notify();
  49. }
  50. }
  51. }
  52. }
  53. }
  54. //吃包子:消费者线程执行的任务
  55. /*
  56. 消费者线程思想:如果没有包子就不消费,让消费者线程进入等待状态;如果有包子,开始吃包子,并且唤醒生产者线程来生产包子
  57. */
  58. public class ChiHuo implements Runnable {
  59. //成员变量
  60. BaoZi baoZi;
  61. //构造方法
  62. public ChiHuo(BaoZi baoZi) {
  63. this.baoZi = baoZi;
  64. }
  65. @Override
  66. public void run() {
  67. //吃包子
  68. while(true){
  69. synchronized ("锁"){
  70. if(baoZi.flag == false){
  71. //没包子
  72. //让吃包子的线程进入等待
  73. try {
  74. "锁".wait();
  75. } catch (InterruptedException e) {
  76. }
  77. }else{
  78. //else表示有包子
  79. //开吃
  80. System.out.println("吃货吃了一个" + baoZi.pi+"皮儿," + baoZi.xian + "馅儿的大包子");
  81. baoZi.pi = null;
  82. baoZi.xian = null;
  83. //修改包子状态
  84. baoZi.flag = false;
  85. //吃完包子叫醒对方(生产者)来做
  86. "锁".notify();
  87. }
  88. }
  89. }
  90. }
  91. }
  92. //测试类
  93. public class Test01 {
  94. public static void main(String[] args) {
  95. //创建包子
  96. BaoZi baoZi = new BaoZi();
  97. //创建对象
  98. ZhiZuo zz = new ZhiZuo(baoZi);
  99. Thread t1 = new Thread(zz);//生产者线程
  100. t1.start();
  101. //创建对象:消费者线程
  102. ChiHuo ch = new ChiHuo(baoZi);
  103. Thread t2 = new Thread(ch);
  104. t2.start();
  105. }
  106. }

七、wait和sleep区别

:::danger 1.sleep(time) 属于Thread类中的,静态方法直接使用类名调用,让当前某个线程休眠,休眠的线程cpu不会执行,该方法可以使用在同步中也可以不使用在同步中,和锁对象无关,如果使用在同步中,不会释放锁对象,直到线程休眠时间到自然醒,然后cpu继续执行
2.等待方法:
1)wait(time) :可以让某个线程计时等待,时间到自然醒,或者时间未到,中途被其他线程唤醒
2)wait()无参数的方法,让某个线程无限等待,只能被其他线程唤醒
等待方法位于Object类中,必须在synchronized中使用,必须使用锁对象调用, 和锁对象有关。当某个线程遇到等待方法那么会立刻释放锁对象,cpu不会执行等待的线程,如果某个线程被唤醒,那么必须具有锁对象才可以执行,没有锁对象进入到锁阻塞状态。只有获取到锁对象才进入运行状态 :::

八、jdk5后的Lock实现线程之间的通讯

:::danger 1.创建Lock锁对象:Lock l = new ReentrantLock();
2.
上锁 l.lock()
释放锁:l.unlock()
3.使用锁对象调用Lock中的方法获取Condition对象:Condition newCondition() 返回绑定到此 Lock 实例的新 Condition 实例。
Condition c = l.newCondition() ;
说明:
Condition是一个接口,Condition 替代了 Object 监视器方法的使用。 和Lock一起使用完成线程的通讯
4.
线程等待:c.await() ;
唤醒线程:c.signal() 或者c.signalAll() :::

九、线程池方式

1、概念:

:::danger 程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。因为启动线程的时候会在内存中开辟一块空间,消耗系统资源,同时销毁线程的时候首先要把和线程相关东西进行销毁,还要把系统的资源还给系统。这些操作都会降低操作性能。尤其针对一个线程用完就销毁的更加降低效率。 :::

2、使用线程池的好处

:::danger

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机或者宕机)。 :::

    3、线程池的使用

    :::danger Java里面线程池的顶级接口是java.util.concurrent.Executor,以及他的子接口java.util.concurrent.ExecutorService ::: Executors类中有个创建线程池的方法如下:
  • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

  • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行

    4、Callable开启多线程

    Future submit(Callable task) : 获取线程池中的某一个线程对象

    1、Callable是什么?

Callable是一个接口类似于Runnable,实现Callable可以调用带有返回值类型的任务方法。
2、Future是什么?
image.png :::danger 方法:V get() : 获取计算完成的结果。
A:我们自定义类,实现Callable接口
B:实现Call方法,Call方法有返回值
C:然后把任务类对象交给线程池执行
D:执行完成的结果保存Future中
E:最后我们调用Future的get方法拿到真正的结果。 :::

  1. /*
  2. * 演示:带返回值的线程任务
  3. * 需求:通过Callable计算从1到任意数字的和
  4. */
  5. class SumTask implements Callable<Integer>{
  6. @Override
  7. public Integer call() throws Exception {
  8. int sum = 0;
  9. for(int i = 1; i <= 5; i++){
  10. sum += i;
  11. }
  12. return sum ;
  13. }
  14. }
  15. public class CallableDemo02 {
  16. public static void main(String[] args) throws InterruptedException, ExecutionException {
  17. // 创建任务对象
  18. SumTask st = new SumTask();
  19. // 获取线程池
  20. ExecutorService es = Executors.newFixedThreadPool(1);
  21. // 执行任务
  22. Future<Integer> future = es.submit(st);
  23. // 等待运算结束,获取结果
  24. Integer i = future.get();
  25. System.out.println(i);
  26. }
  27. }
  1. @Configuration
  2. @EnableConfigurationProperties(TaskThreadPoolInfo.class)
  3. @Slf4j
  4. public class TaskExecutePool {
  5. private TaskThreadPoolInfo info;
  6. public TaskExecutePool(TaskThreadPoolInfo info) {
  7. this.info = info;
  8. }
  9. /**
  10. * 定义任务执行器
  11. * @return
  12. */
  13. @Bean(name = "threadPoolTaskExecutor",destroyMethod = "shutdown")
  14. public ThreadPoolTaskExecutor threadPoolTaskExecutor(){
  15. //构建线程池对象
  16. ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
  17. //核心线程数:核心线程数(获取硬件):线程池创建时候初始化的线程数
  18. taskExecutor.setCorePoolSize(info.getCorePoolSize());
  19. //最大线程数:只有在缓冲队列满了之后才会申请超过核心线程数的线程
  20. taskExecutor.setMaxPoolSize(info.getMaxPoolSize());
  21. //缓冲队列:用来缓冲执行任务的队列
  22. taskExecutor.setQueueCapacity(info.getQueueCapacity());
  23. //允许线程的空闲时间:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
  24. taskExecutor.setKeepAliveSeconds(info.getKeepAliveSeconds());
  25. //线程名称前缀
  26. taskExecutor.setThreadNamePrefix("StockThread-");
  27. //设置拒绝策略
  28. // taskExecutor.setRejectedExecutionHandler(rejectedExecutionHandler());
  29. //参数初始化
  30. taskExecutor.initialize();
  31. return taskExecutor;
  32. }
  33. /**
  34. * 自定义线程拒绝策略
  35. * @return
  36. */
  37. /**
  38. @Bean
  39. public RejectedExecutionHandler rejectedExecutionHandler(){
  40. RejectedExecutionHandler errorHandler = new RejectedExecutionHandler() {
  41. @Override
  42. public void rejectedExecution(Runnable runnable, ThreadPoolExecutor executor) {
  43. //TODO 可自定义Runable实现类,传入参数,做到不同任务,不同处理
  44. log.info("股票任务出现异常:发送邮件");
  45. }
  46. };
  47. return errorHandler;
  48. } */
  49. }