多线程是什么

  • 多线程是指从软硬件上实现多条执行流程的技术
  • 多线程:
    • 继承Thread类
    • 实现Runnable接口
  • 不要把主线程任务放在子线程之前,要不然就是单一线程

方式一:

  • 继承Thread类; 重写run方法; 创建线程对象; 调用start方法启动
  • 优缺点:优点编码简单 缺点:存在单继承的局限性,线程类继承Thread后不能继承其他类,不便于扩展。

启动线程调用start();调用run就是普通调用

  1. //线程的开始
  2. public class ThreadTest {
  3. public static void main(String[] args) {
  4. //创建线程对象
  5. Thread ct = new CreateThread();
  6. //调用start 启动线程
  7. ct.start();
  8. for (int i = 0; i < 10; i++) {
  9. System.out.println("主线程"+i);
  10. }
  11. }
  12. }
  13. /*
  14. * 多线程的创建方式之一:继承Thread类实现
  15. * */
  16. public class CreateThread extends Thread{
  17. /*
  18. * 重写run方法,里面是定义线程以后要干啥
  19. * */
  20. @Override
  21. public void run() {
  22. for (int i = 0; i < 10; i++) {
  23. System.out.println("子线程"+i);
  24. }
  25. }
  26. }

多线程的实现方案二:实现Runnable接口

  1. 定义一个线程任务类MyRunnable实现Runnable接口,实现run()方法
  2. 创建线程任务对象MyRunnable
  3. 把MyRunnable任务对象交给Thread处理
  4. 调用线程对象的start()方法启动线程

Runnable接口:

  • 优缺点:
    • 优:线程任务类只是实现接口,可以继承类和实现接口,扩展性强。
    • 缺:编程多一层对象包装,如果线程执有执行结果是不可以直接返回的
    • 第一种和第二种都是没有返回结果的 ```java package com.h.runnable; /*
  • 第二种线程创建方式
  • */ public class Test { public static void main(String[] args) {
    1. //创建一个任务对象
    2. Runnable ctr = new CreatThreadRunnable();
    3. //把任务对象交给线程对象
    4. new Thread(ctr).start();
    5. for (int i = 0; i < 5; i++) {
    6. System.out.println("主线程"+i);
    7. }
    } } 实现的接口 package com.h.runnable;

/实现Runnable接口/ public class CreatThreadRunnable implements Runnable{ /*

  1. * 重写run方法
  2. *
  3. * */
  4. @Override
  5. public void run() {
  6. for (int i = 0; i < 5; i++) {
  7. System.out.println("子线程"+i);
  8. }
  9. }

}

  1. 实现Runnable(匿名内部类)<br />写法:
  2. ```java
  3. public class InnerRunnable {
  4. public static void main(String[] args) {
  5. Runnable target = new Runnable(){
  6. public void run(){
  7. for (int i = 0; i < 5; i++) {
  8. System.out.println("匿名内部类线程"+i);
  9. }
  10. }
  11. };
  12. new Thread(target).start();
  13. for (int i = 0; i < 5; i++) {
  14. System.out.println("主线程"+i);
  15. }
  16. }
  17. }
  18. 简化
  19. public class InnerRunnable {
  20. public static void main(String[] args) {
  21. new Thread(new Runnable(){
  22. public void run(){
  23. for (int i = 0; i < 5; i++) {
  24. System.out.println("匿名内部类线程"+i);
  25. }
  26. }
  27. }).start();
  28. for (int i = 0; i < 5; i++) {
  29. System.out.println("主线程"+i);
  30. }
  31. }
  32. }
  33. //使用Lambda表达式简化
  34. new Thread(()->{
  35. for (int i = 0; i < 5; i++) {
  36. System.out.println("匿名内部类线程"+i);
  37. }
  38. }).start();

方式三:
JDK5.0提供:Callable和FutureTask来实现

  • 前两种方式重写run方法均不能直接返回结果
  • 不适合需要返回线程执行结果的业务场景

实现方案三:

  1. 得到任务对象
    1. 定义类实现Callable接口,重写call方法,封装要做的事情。
    2. 用FutureTask把Callable对象封装成线程任务对象
  2. 把线程任务对象交给Thread处理
  3. 调用Thread的start方法启动线程,执行任务
  4. 线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果

    线程返回值需要怎么做:

  5. 创建任务对象MyCallable并实现Callable接口 泛型自己需要什么类型返回申明什么类型

  6. 重写call方法 并返回你所需要的值
  7. 到主线程创建Callable任务对象 Callable call = new MyCallable(100);
  8. 把Callable任务对象交给FutureTask对象
    1. FutureTask对象实现了RunnableFuture接口
    2. RunnableFuture继承了Runnable接口
    3. 所以Thread类构造器中可以放FutureTask对象
    4. FutureTask可以使用get方法来获取Callable所返回的值
  9. 把FutureTask交给Thread
  10. Thread类调用start方法启动线程
  11. FutureTask对象调用get()方法获取call()方法返回的值
  12. get方法返回两个结果一个是正常结果,另一个是异常结果所以得使用try{}catch(){}
  13. get()方法有一个监测就是看call方法是否跑完没有如果没有跑完,代码执行到调用get()方法的时候就会等待,等待跑完后才会提取结果

FutureTask的API:
public FutureTask<>(Callable call);把Callable对象封装成FutureTask对象;
public V get() throws Exception; 获取线程执行call方法返回的结果。如果线程没有执行完,就会等待
优点:

  • 线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强
  • 可以在线程执行完毕后去获取线程执行结果
    • 缺点
  • 编码复杂

代码:

  1. package com.h.callable;
  2. import java.util.concurrent.Callable;
  3. import java.util.concurrent.ExecutionException;
  4. import java.util.concurrent.FutureTask;
  5. /*
  6. * 学会线程创建方式三、实现Callable接口,结合FutureTask完成
  7. * */
  8. public class CallableTest {
  9. public static void main(String[] args) {
  10. //创建Callable任务对象
  11. Callable<String> mc = new MyCallable(100);
  12. //把Callable任务对象,封装给FutureTask对象
  13. //为什么要交给FutureTask对象因为
  14. // 作用1、FutureTask是Runnable对象(实现了Runnable接口),可以交给Thread了
  15. // 作用2、可以在线程执行完毕之后通过调用其get方法得到线程执行完完成的结果
  16. FutureTask<String> ft = new FutureTask<>(mc);
  17. Thread th = new Thread(ft);
  18. th.start();
  19. try{
  20. String str = ft.get();
  21. System.out.println("第一个结果"+str);
  22. }
  23. }
  24. }
  25. /*
  26. * 1、定义一个任务类,实现Callable类 应该申明线程任务执行完毕后的结果的数据类型
  27. * */
  28. class MyCallable implements Callable<String> {
  29. private int n;
  30. public MyCallable(int n) {
  31. this.n = n;
  32. }
  33. /*重写call方法 线程任务方法*/
  34. @Override
  35. public String call() throws Exception {
  36. int sum = 0;
  37. for (int i = 0; i <= n; i++) {
  38. sum+=i;
  39. }
  40. return "子线程的结果是" +sum;
  41. }
  42. }

Thread常见方法
给线程设置名字

  1. 使用Thread.setName();
  2. 使用构造器来给线程设置名字

获取线程名字

  1. 使用Thread.getName();

获取当前线程的方法:
谁调用的,谁就是当前线程

  • Thread.currentThread();

使用Runnable的时候进行设置名称

  • new Thread(Runnable runnable,String name);

线程休眠:

  • Thread.sleep(long time);当前线程休眠指定的时间后再继续执行,单位为毫秒 ```java

public class MyThreadTest { public static void main(String[] args) { Thread t1 =new MyThread(“构造器1号”); //给线程取名字 t1.setName(“1号”); t1.start();

  1. //拿到当前线程对象
  2. Thread m = Thread.currentThread();
  3. m.setName("主线程");
  4. for (int i = 0; i < 5; i++) {
  5. System.out.println(m.getName()+""+i);
  6. }
  7. }

}

public class MyThread extends Thread{ //使用构造器给线程设置名字 public MyThread(String name) { super(name); }

  1. @Override
  2. public void run() {
  3. for (int i = 0; i < 5; i++) {
  4. //获取线程名字
  5. System.out.println(Thread.currentThread().getName()+"子线程");
  6. }
  7. }

}

  1. <a name="UoKwb"></a>
  2. #### 线程安全问题:
  3. - **多个线程同时操作同一个共享资源**的时候可能会出现业务安全问题,称为线程安全问题
  4. - 同步代码块
  5. 代码:
  6. ```java
  7. package com.h.h_account;
  8. public class Account {
  9. private String cardId;
  10. private double money;
  11. public Account() {
  12. }
  13. public Account(double money) {
  14. this.money = money;
  15. }
  16. public Account(String cardId, double money) {
  17. this.cardId = cardId;
  18. this.money = money;
  19. }
  20. public String getCardId() {
  21. return cardId;
  22. }
  23. public void setCardId(String cardId) {
  24. this.cardId = cardId;
  25. }
  26. public double getMoney() {
  27. return money;
  28. }
  29. public void setMoney(double money) {
  30. this.money = money;
  31. }
  32. public void drawMoney(double money) {
  33. //获取当前线程是谁在调用
  34. String name = Thread.currentThread().getName();
  35. //判断账户余额是否足够
  36. if(this.money>= money){
  37. //取钱
  38. System.out.println(name + "取钱:"+money);
  39. this.money -= money;
  40. System.out.println("当前剩余余额:"+this.money);
  41. }else {
  42. System.out.println("余额不足");
  43. }
  44. }
  45. }
  46. package com.h.h_account;
  47. /*
  48. * 取钱的线程
  49. * */
  50. public class DrawThread extends Thread{
  51. private Account acc;
  52. public DrawThread(Account acc,String name) {
  53. super(name);
  54. this.acc = acc;
  55. }
  56. @Override
  57. public void run() {
  58. //同时取钱
  59. acc.drawMoney(100);
  60. }
  61. }
  62. package com.h.h_account;
  63. public class AccountTest {
  64. public static void main(String[] args) {
  65. /*小明与小红共同使用一个账户*/
  66. Account ac = new Account(100);
  67. Thread t1 = new DrawThread(ac ,"小明");
  68. Thread t2 = new DrawThread(ac,"小红");
  69. t1.start();
  70. t2.start();
  71. }
  72. }
  73. 同步锁解决问题
  74. //在这加锁的 然后当两个线程到这的时候只能有一个线程能抢到,抢到以后另一个线程就等待
  75. synchronized ("hh") {
  76. if (this.money >= money) {
  77. //取钱
  78. System.out.println(name + "取钱:" + money);
  79. this.money -= money;
  80. System.out.println("当前剩余余额:" + this.money);
  81. } else {
  82. System.out.println("余额不足");
  83. }
  84. }

解决上面线程安全问题:线程同步
线程同步核心思想

  • 加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来
  • 作用:把出现线程安全问题的核心代码给上锁
  • 原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行

synchronized(同步锁对象){
操作共享资源的代码(核心代码)
}
锁对象要求

  • 理论上:锁对象只要对于当前同时执行的线程来说是同一个对象即可

锁对象用任意唯一的对象不好

  • 会影响其他无关线程的执行
  1. 同步锁解决问题
  2. //在这加锁的 然后当两个线程到这的时候只能有一个线程能抢到,抢到以后另一个线程就等待
  3. synchronized (this) {
  4. if (this.money >= money) {
  5. //取钱
  6. System.out.println(name + "取钱:" + money);
  7. this.money -= money;
  8. System.out.println("当前剩余余额:" + this.money);
  9. } else {
  10. System.out.println("余额不足");
  11. }
  12. }

锁对象的规范要求

  • 规范上:建议使用共享资源作为锁对象。
  • 对于实例方法建议使用this作为锁对象
  • 对于静态方法建议使用字节码(类名.class)对象作为锁对象。

静态方法

  1. class Acc{
  2. //100个线程来也只有一个人能拿到
  3. public static void run(){
  4. synchronized(Acc.class)
  5. }
  6. }

synchronized (“hh”)这样写是唯一性所有线程都必须从他这拿锁
synchronized (this)这样写是每个对象拿自己的那把锁,其他线程不受干扰
为什么写this呢
因为this是那个类所调用的那个方法传过来的对象
问题:

  1. 同步代码块是如何实现线程安全的?
    1. 对出现问题的核心代码使用synchronzed()进行加锁
    2. 每次只能一个线程占锁进入访问
  2. 同步代码块的同步锁对象有什么要求?
    1. 对于实例化方法建议使用this作为锁对象
    2. 对于静态方法建议使用字节码(类名.class)对象作为锁对象

END

同步方法
同步锁可以作为修饰符加上
同步方法的底层原理

  • 同步方法与同步代码块原理一样 只是不需要你自己添加this或类名.class

    1. class Acc{
    2. //同步方法
    3. public synchronized void run(){
    4. //核心代码块
    5. }
    6. }

    性能方面:同步代码块可以选择你所想锁的代码 同步方法是将整个方法锁起来,性能方面差别微乎其微。
    同步代码块 与 同步方法原理一样
    实际开发中同步方法使用较多因为代码简单

    LOCK锁:

    创建方式:
    把锁写在类里面,每次new类的时候都会创建出一个锁

  • Lock lock = new ReentrantLock();

  • Lock是接口ReentrantLock是Lock的实现类
  • lock.lock();//上锁
  • lock.unlock();//解锁

看代码:

  1. package com.h.h_lock;
  2. import java.util.concurrent.locks.Lock;
  3. import java.util.concurrent.locks.ReentrantLock;
  4. public class Account {
  5. private String cardId;
  6. private double money;
  7. //自创锁 唯一不可替换锁 非常专业 每次创建对象都有一个锁
  8. private final Lock lock = new ReentrantLock();
  9. public Account() {
  10. }
  11. public Account(double money) {
  12. this.money = money;
  13. }
  14. public Account(String cardId, double money) {
  15. this.cardId = cardId;
  16. this.money = money;
  17. }
  18. public String getCardId() {
  19. return cardId;
  20. }
  21. public void setCardId(String cardId) {
  22. this.cardId = cardId;
  23. }
  24. public double getMoney() {
  25. return money;
  26. }
  27. public void setMoney(double money) {
  28. this.money = money;
  29. }
  30. //在核心代码块上调用lock方法就上锁
  31. public void drawMoney(double money) {
  32. //获取当前线程是谁在调用
  33. String name = Thread.currentThread().getName();
  34. //为了代码的强健性加上finally
  35. lock.lock(); //上锁
  36. try {
  37. if (this.money >= money) {
  38. //取钱
  39. System.out.println(name + "取钱:" + money);
  40. this.money -= money;
  41. System.out.println("当前剩余余额:" + this.money);
  42. } else {
  43. System.out.println("余额不足");
  44. }
  45. }finally {
  46. //加锁核心代码出现异常,而不会影响解锁
  47. lock.unlock();//解锁
  48. }
  49. }
  50. }

线程池(重点)

谁代表线程池

  • ExecutorService接口

线程池概述:

  • 线程池就是一个可以复用线程的技术,反复使用;

不使用线程池的问题

  • 如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务又来了又要创建新线程,而这样创建开销是很大的,这样会严重影响系统性能,所以我们需要用线程池。

如何得到线程池

  • 方式一:使用ExecutorService的实现类ThreadPoolExecutor创建一个线程池
  • 方式二:使用Executors(线程池的工具类)调用方法返回不同的特点的线程池对象

构造器:
七个参数

image.png

线程池常见面试题:

临时线程什么时候创建啊?

  • 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
  • 什么是还可以创建临时线程
    • 当线程池给的最大线程数还小于等于当前线程的时候还可以创建

什么时候会开始拒绝任务

  • 核心线程与临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。

Runnable添加进线程池:

  1. package com.h.h_threadpool;
  2. import java.util.concurrent.*;
  3. /*
  4. * 自定义线程池对象,并测试特性
  5. * */
  6. public class ThreadPoolDemo1 {
  7. //1、创建线程池对象
  8. /*int corePoolSize,
  9. int maximumPoolSize,
  10. long keepAliveTime,
  11. TimeUnit unit,
  12. BlockingQueue<Runnable> workQueue,
  13. ThreadFactory threadFactory,
  14. RejectedExecutionHandler handler
  15. */
  16. public static void main(String[] args) {
  17. ExecutorService es = new ThreadPoolExecutor(3,5,6,
  18. TimeUnit.SECONDS,new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
  19. new ThreadPoolExecutor.AbortPolicy());
  20. //2、给任务线程池处理
  21. Runnable runnable = new MyRunnable();
  22. //把线程丢入线程池 核心线程3个
  23. es.execute(runnable);
  24. es.execute(runnable);
  25. es.execute(runnable);
  26. //新任务排队5个
  27. es.execute(runnable);
  28. es.execute(runnable);
  29. es.execute(runnable);
  30. es.execute(runnable);
  31. es.execute(runnable);
  32. //临时线程
  33. es.execute(runnable);
  34. es.execute(runnable);
  35. //不创建 拒绝策略被触发
  36. // es.execute(runnable);
  37. //关闭线程池 开发中一般不会使用
  38. // es.shutdownNow(); //立即关闭 即使任务没有完成
  39. // es.shutdown(); //等待任务完成以后才能关闭
  40. }
  41. }
  42. Runnable线程
  43. package com.h.h_threadpool;
  44. public class MyRunnable implements Runnable {
  45. public void run() {
  46. for (long i = 0; i < 5; i++) {
  47. System.out.println(Thread.currentThread().getName()+"输出了Hello World"+i);
  48. }
  49. try {
  50. Thread.sleep(100000);
  51. } catch (InterruptedException e) {
  52. e.printStackTrace();
  53. }
  54. }
  55. }

Callable添加进线程池

  1. package com.h.h_threadpool;
  2. import java.util.concurrent.*;
  3. /*
  4. * 自定义线程池对象,并测试特性
  5. * */
  6. public class ThreadPoolDemo2 {
  7. //1、创建线程池对象
  8. /*int corePoolSize,
  9. int maximumPoolSize,
  10. long keepAliveTime,
  11. TimeUnit unit,
  12. BlockingQueue<Runnable> workQueue,
  13. ThreadFactory threadFactory,
  14. RejectedExecutionHandler handler
  15. */
  16. public static void main(String[] args) throws Exception {
  17. ExecutorService es = new ThreadPoolExecutor(3,5,6,
  18. TimeUnit.SECONDS,new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
  19. new ThreadPoolExecutor.AbortPolicy());
  20. //直接把MyCallable对象扔进线程池里面 可以通过返回值 拿到对象
  21. Future<String> f1 = es.submit(new MyCallable(100));
  22. Future<String> f2 = es.submit(new MyCallable(200));
  23. Future<String> f3 = es.submit(new MyCallable(300));
  24. Future<String> f4 = es.submit(new MyCallable(400));
  25. System.out.println(f1.get());
  26. System.out.println(f2.get());
  27. System.out.println(f3.get());
  28. System.out.println(f4.get());
  29. }
  30. }
  31. Callable:
  32. class MyCallable implements Callable<String> {
  33. private int n;
  34. public MyCallable(int n) {
  35. this.n = n;
  36. }
  37. /*重写call方法 线程任务方法*/
  38. @Override
  39. public String call() throws Exception {
  40. int sum = 0;
  41. for (int i = 0; i <= n; i++) {
  42. sum+=i;
  43. }
  44. return "子线程的结果是" +sum;
  45. }
  46. }

可以直接把Callable放入线程池中

Executors工具类:
Executors的底层其实也是基于线程池的实现类ThreadPoolExecutors创建线程池对象

  1. Executors工具类底层是基于什么方式实现的线程池对象?
    1. 线程池ExecutorService的实现类:ThreadPoolExecutor
  2. Executors是否合适大型互联网场景的线程池方案?
    1. 不适合
    2. 建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确的运行规则,规避资源耗尽的风险。

定时器:

  • 定时器是一种控制任务延时调用,或者周期调用技术

定时器的实现方式

  • 方式一、Timer
    • 构造器:public Timer();//创建定时器对象
  • 方式二、ScheduuledExecutorService

Timer定时器的特点和存在的问题

  1. Timer是单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入
  2. 可能因为其中的某个任务的异常使Timer线程死掉,从而影响后续任务执行
  3. 是单线程,出现异常程序就会死掉


    单线程定时器: ```java package com.h.h_timer;

import java.util.Timer; import java.util.TimerTask;

/使用定时器/

public class TimerDemo { public static void main(String[] args) { //创建定时器 Timer t = new Timer(); //调用方法处理定时任务 //每隔3秒触发一次,每隔两秒执行一次 //每隔3秒触发一次是这个任务的触发,每隔两秒是再次执行这个任务 t.schedule(new TimerTask() { @Override public void run() { //定时器本身就是一个线程 System.out.println(Thread.currentThread().getName()+”执行一次”);

  1. }
  2. },3000,2000);
  3. }

}

  1. **ScheduledExecutorService**
  2. - 是为了弥补Timer的缺陷
  3. - Executors的方法:public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize);//得到线程池对象
  4. - ScheduleExecutorService的方法:public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimerUnit unit)
  5. **ScheduledExecutorService的优点 **
  6. 1. 基于线程池,某个任务的执行情况不会影响其他定时任务的执行
  7. 其他线程挂掉的时候别的线程不会挂掉<br />互不干扰
  8. 线程池定时器:
  9. ```java
  10. package com.h.h_timer;
  11. import java.util.Timer;
  12. import java.util.TimerTask;
  13. import java.util.concurrent.Executors;
  14. import java.util.concurrent.ScheduledExecutorService;
  15. import java.util.concurrent.ScheduledThreadPoolExecutor;
  16. import java.util.concurrent.TimeUnit;
  17. /*使用定时器*/
  18. public class TimerDemo2 {
  19. public static void main(String[] args) {
  20. //1、创建ScheduledExecutorService线程池,做定时器
  21. //以后使用这种定时器进行开发
  22. ScheduledExecutorService ses = Executors.newScheduledThreadPool(1);
  23. //2、开启定时任务
  24. //参数列表 1:定时器 2:触发时间 3:周期时间 4:单位 秒?毫秒?分?时?
  25. ses.scheduleAtFixedRate(new TimerTask() {
  26. @Override
  27. public void run() {
  28. System.out.println("定时任务1");
  29. }
  30. }, 0, 2, TimeUnit.SECONDS);
  31. }
  32. }

并发与并行

  • 正在运行的程序(软件)就是一个,线程是属于进程的,多个线程其实是并发与并行同时进行

并发理解

  • CPU同时处理线程的数量有限
  • CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程是同时执行,这就是并发。
  • 并发:同一时间段,抢占CPU执行自己
  • 并行:同一时刻同时执行
    image.png

新的线程知识

如何让两个线程依次输出结果
例如:线程1 ABCDE 线程2: 12345
使用最新技术:
park是叫醒 unpark阻塞
LockSupport.park();与LockSupport.unpark();

notify()是按特定算法去叫醒某一个线程没法精确叫醒某一个线程
使用notifyAu()

  1. char aI = {1,2,3,4,5,6};
  2. char aI=C = {"a","b","c","d","e"};
  3. TransferQueue<Character> queue = new LinkedTransferQueue<~>();
  4. TransferQueue 相当于一个套筒 一个人把手伸进去 另一个也把手伸进去 一个人交出东西 另一个接收东西
  5. new Thread(() -> {
  6. try{
  7. for(char c : aI){
  8. System.out.print(queue.take());
  9. //传一个东西进行
  10. queue.transfer(c);
  11. }
  12. }catch(InterruptedException e){
  13. e.printStackTrace();
  14. }
  15. },"t1").start;
  16. new Thread(() ->{
  17. try{
  18. for(char c : aC){
  19. //拿一个东西出来
  20. queue.transfer(c);
  21. System.out.print(queue.take());
  22. }
  23. }catch(InterruptedException e){
  24. e.printStackTrace();
  25. }
  26. },"t2").start;
  27. })