1、Java多线程与并发,进程与线程的区别。
答:进程是资源分配的最小单位,线程是CPU调度的最小单位。
1)、进程是资源分配的基本单位,所有与进行相关的资源,都被记录在进程控制块PCB中,以表示该进程拥有这些资源或者正在使用它们。
2)、进程是抢占处理机的调度单位,线程属于某个进程,共享其资源。进程拥有一个完整的虚拟内存地址空间,当进程发生调度的时候,不同的进程拥有不同的虚拟地址空间,而同一进程内不同线程共享同一地址空间,与进程相对应。线程与资源分配无关,它属于某一个进程,并与进程内的其它线程一起共享进程里面的资源。
3)、线程只由堆栈、寄存器、程序计数器和线程计数表TCB组成。
2、进程与线程的区别总结。
1)、线程不能看做独立应用,而进程可看做独立应用。操作系统并没有将多个线程看作多个独立的应用来实现进程的调度和管理以及资源分配。
2)、进程有独立的地址空间,一个进程奔溃后,在保护模式下,不会对其他进程产生影响,相互不影响,线程只是进程的不同指向路径,如果某个线程挂掉,那么它所在的进程也会挂掉。
3)、线程有自己的堆栈和局部变量,但线程没有独立的地址空间,多进程的程序比多线程程序健壮。
4)、进程的切换比线程的切换开销大,效率差很多,如果要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程,每个独立的线程有个程序运行的入口,顺序执行序列和程序的出口,但是线程不能独立执行,必须依存于某一个应用程序当中,由应用程序提供对多个线程的执行控制。
3、Java进程与线程的关系。
答:1)、Java对操作系统提供的功能进行封装,包括进程和线程。
2)、运行一个程序会产生一个进程,进程包含至少一个线程。
3)、每个进程对应一个JVM实例,多个线程共享JVM里面的堆,每个JVM实例唯一对应一个堆,每一个线程都有自己私有的栈。
4)、Java采用单线程编程模型,程序会自动创建主线程,自己的程序中如果没有主动创建线程的话,程序会自动创建一个线程,这个线程就是主线程,因此在编程的时候,将耗时的操作放入子线程中进行,以避免阻塞主线程,影响用户体验。
5)、Java程序启动的时候,主线程立刻运行,主线程的重要性体现在,主线程可以创建子线程,原则上要后于子线程完成执行,主线程通常是最后完成执行,因为它需要执行各种关闭动作。
3、Java线程的start和run方法的区别?
答:1)、调用start()方法会创建一个新的子线程并启动。
2)、run()方法只是Thread的一个普通方法的调用。
4、Java线程的Thread和Runnable的关系?
答:1)、Thread是一个类,Runnable是一个接口,Thread实现了Runnable接口。
2)、Thread是实现了Runnable接口的类,使得run支持多线程。
3)、因为Java类的单一继承原则,推荐多使用Runnable接口的方式。
5、那么如何给java多线程的run()方法传参呢。实现的方式主要有三种。
答:1)、构造函数传参。
2)、成员变量传参,通过set方法进行传参。
3)、回调函数传参。
6、Java线程中如何实现处理线程的返回值。
答:和线程相关的业务逻辑需要放入到run()方法里面,但是run方法是没有参数的,并且也没有返回值的,那么如何给run()方法传参呢。有的程序的执行是依赖于子任务的返回值进行的,当子任务交给子线程去完成的时候,是需要获取到它们的返回值的,此时如何获取到子线程的返回值呢。实现的方式主要有三种。
1)、主线程等待法,即让主线程循环等待,直到目标子线程返回值为止,主线程等待法实现简单,缺点是需要自己实现循环等待的逻辑,但是如果等待的变量一多,代码就会显得异常的臃肿,而且需要循环多久是不确定的,无法左到精准的控制。
2)、使用Thread类的join()阻塞当前线程以等待子线程处理完毕,join方法可以阻塞调用此方法的线程即这里可以阻塞主线程,直到join方法所在的线程执行完毕为止,此方法比主线程等待法做到更精准的控制,实现起来简单,缺点是粒度不够细。
3)、通过Callable接口实现,通过FutureTask Or线程池获取。JDK5之后新增了Callable接口,执行了Callable任务之后,可以获取一个Future的对象,在该对象上调用get方法就可以获取到Callable任务返回的对象。关于通过Callable接口实现的方式有两种方式来实现,第一种是通过FutureTask,第二种是线程池获取。
代码案例,主线程等待法、使用线程Thead类的join()方法的实现,如下所示:
1 package com.thread; 2 3 public class CycleWait implements Runnable { 4 5 // 私有成员变量 6 private String value; 7 8 @Override 9 public void run() { 10 try { 11 // 子线程休眠5秒钟 12 Thread.currentThread().sleep(5000); 13 } catch (InterruptedException e) { 14 e.printStackTrace(); 15 } 16 // 通过run方法给value赋值 17 value = "we have data now"; 18 } 19 20 public static void main(String[] args) { 21 CycleWait cycleWait = new CycleWait(); 22 Thread thread = new Thread(cycleWait); 23 thread.start(); 24 // 方案一,主线程等待法 25 // while (cycleWait.value == null) { 26 // try { 27 // // 子线程休眠0.1秒钟 28 // Thread.currentThread().sleep(100); 29 // } catch (InterruptedException e) { 30 // e.printStackTrace(); 31 // } 32 // } 33 34 35 // 方案二,使用Thread类的join()阻塞当前线程以等待子线程处理完毕 36 try { 37 thread.join(); 38 } catch (InterruptedException e) { 39 e.printStackTrace(); 40 } 41 42 43 // 如果不使用主线程等待法,是无法打印子线程的返回值的 44 System.out.println("value : " + cycleWait.value); 45 } 46 }
关于通过Callable接口实现的方式有两种方式来实现,第一种是通过FutureTask,第二种是线程池获取。
1 package com.thread; 2 3 import java.util.concurrent.Callable; 4 import java.util.concurrent.ExecutionException; 5 import java.util.concurrent.FutureTask; 6 7 public class MyCallable implements Callable<String> { 8 9 @Override 10 public String call() throws Exception { 11 String value = "实现Callable接口的多线程"; 12 System.out.println("Ready to work." + value); 13 // 休眠5秒 14 Thread.currentThread().sleep(5000); 15 System.out.println("task done."); 16 int sum = 0; 17 for (int i = 0; i < 100; i++) { 18 sum = sum + i; 19 } 20 return sum + ""; 21 } 22 23 public static void main(String[] args) { 24 // 第一种方式,FutureTask的构造函数可以接收Callable实现类的实例的 25 FutureTask<String> futureTask = new FutureTask<String>(new MyCallable()); 26 // FutureTask<V> implements RunnableFuture<V> 27 // RunnableFuture<V> extends Runnable, Future<V> 28 Thread thread = new Thread(futureTask); 29 // 启动线程 30 thread.start(); 31 32 // 判断子线程是否执行完毕,isDone()方法用来判断传递给FutureTask的Callable实现类是否已经执行完毕。 33 if (!futureTask.isDone()) { 34 System.out.println("task has not finished,please wait."); 35 } 36 // 获取到子线程返回的值,无参的get()方法主要用来阻塞当前调用它的线程,直到我们的Callable实现类的call方法执行完毕为止 37 // 然后取到返回值,可以精准的获取到子线程处理完毕之后的返回值的。 38 // 带参的get()方法,可以传入等待时间,是一个超时机制,超时时间内还没有获取到call方法的返回值的话就抛出异常。 39 try { 40 System.out.println("task return : " + futureTask.get()); 41 } catch (InterruptedException e) { 42 e.printStackTrace(); 43 } catch (ExecutionException e) { 44 e.printStackTrace(); 45 } 46 47 } 48 49 }
1 package com.thread; 2 3 import java.util.concurrent.ExecutionException; 4 import java.util.concurrent.ExecutorService; 5 import java.util.concurrent.Executors; 6 import java.util.concurrent.Future; 7 8 public class ThreadPollDemo { 9 10 public static void main(String[] args) { 11 // 创建线程池,使用线程池的好处可以提交多个实现Callable接口的类,让线程池并发的处理结果, 12 // 方便对实现了Callable接口的类进行管理 13 ExecutorService newCachedThreadPoll = Executors.newCachedThreadPool(); 14 // 向线程池提交任务。submit接收Callable接口 15 Future<String> future = newCachedThreadPoll.submit(new MyCallable()); 16 // FutureTask<V> implements RunnableFuture<V> 17 // RunnableFuture<V> extends Runnable, Future<V> 18 if (!future.isDone()) { 19 System.out.println("task has not finished,please wait."); 20 } 21 try { 22 System.out.println("task return : " + future.get()); 23 } catch (InterruptedException e) { 24 e.printStackTrace(); 25 } catch (ExecutionException e) { 26 e.printStackTrace(); 27 } finally { 28 // 关闭线程池 29 newCachedThreadPoll.shutdown(); 30 } 31 } 32 }
7、Java线程中线程的状态。
答:线程的状态有六个,新建New状态、运行Runnable状态、无线期等待Waiting、限期等待Timed Waiting、阻塞 Blocked、结束Terminated状态。
1)、新建New状态,创建后尚未启动的线程的状态,即新创建了一个线程,但是还没有调用start方法。
2)、运行Runnable状态,包含Running和Ready。包括操作系统的Running和ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间,比如线程对象创建后,调用了该对象的start方法之后,这个时候线程处于Runnable状态,由于该状态分为两个子状态Running和Ready,处于Running的线程位于可运行线程之中,等待被线程调度选中,获取CPU的使用权,处于Ready状态的线程位于线程池中,等待被线程调度选中,获取CPU的使用权,而处于Ready状态的线程在获得CPU时间后,就变为Running状态的线程。
3)、无线期等待Waiting,不会被分配CPU执行时间,需要显式被唤醒,需要其它线程显式的唤醒。以下方法会让线程陷入无线期等待中。
a)、没有设置Timeout参数的Object.wait()方法。
b)、没有设置Timeout参数的Thread.join()方法。
c)、LockSupport.park()方法。
4)、限期等待Timed Waiting,处于这种状态的线程,不会被分配CPU执行时间,不过无需等待其它线程显式唤醒,在一定时间后会由系统自动唤醒。
a)、Thread.sleep()方法。
b)、设置Timeout参数的Object.wait()方法。
c)、设置Timeout参数的Thread.join()方法。
d)、LockSupport.parkNanos()方法。
e)、LockSupport.parkUntil()方法。
5)、阻塞 Blocked,等待获取排它锁。阻塞状态和等待状态的区别是,阻塞状态在等待着获取到一个排它锁,这个事件将在另外一个线程中放弃这个锁的时候发生,而等待状态则是在等待一段时间或者有唤醒动作的时候发生,在程序等待进入同步区域的时候,线程将进入Blocked状态。比如,当某个线程进入synchronized关键字修饰的方法或者代码块的时候,即获取锁执行的时候,其它想进入此方法或者代码块的线程就只能等着,它们的状态便是Blocked
6)、结束Terminated状态,已终止线程的状态,线程已经结束执行。让线程的run方法完成的时候,或者主线程的main方法完成的时候,我们就认为它终止了,这个线程对象也许是活的,但是它已经不是一个单独执行的线程,线程一旦终止了,就不能再复生,在一个终止的线程调用start方法会抛出异常。
8、Java线程中sleep和wait的区别?
答:sleep方法和wait方法的基本的差别。
1)、sleep()方法是Thread类的方法,wait()方法是Object类中定义的方法。
2)、sleep()方法可以在任何地方使用。
3)、wait()方法只能在synchronized方法或者synchronized块中使用。
sleep方法和wait方法的最本质的差别。
1)、Thread.sleep()方法只会让出CPU,不会导致锁行为的变化。如果当前线程是拥有锁的,那么Thread.sleep()方法不会让线程释放锁,而只会主动让出CPU,让出CPU之后呢,CPU就可以去执行其它任务了。
2)、Object.wait()方法不仅让出CPU,还会释放已经占有的同步资源锁,以便其它正在等待该资源的线程得到该资源进而去运行。
1 package com.thread; 2 3 public class WaitSleepDemo { 4 5 public static void main(String[] args) { 6 // 创建一个不可变的Object对象 7 final Object lock = new Object(); 8 new Thread(new Runnable() { 9 10 @Override 11 public void run() { 12 // 线程A等待获取锁lock 13 System.out.println("thread A is waiting to get lock."); 14 // 获取同步锁才可以执行代码块里面的逻辑 15 synchronized (lock) { 16 try { 17 // 获取到了锁lock 18 System.out.println("thread A get lock."); 19 // 模拟程序执行 20 Thread.sleep(20); 21 // 开始调用wait的方法 22 System.out.println("Thead A do wait method."); 23 // 调用wait方法如果不传入参数就进入无限期等待 24 // 如果传入1000就等待一秒自动被唤醒,进入限期等待状态。 25 // lock.wait(1000); 26 27 // 此时,将A线程和B线程的wait方法和sleep方法反过来 28 Thread.sleep(1000);// sleep方法不会释放同步锁的。 29 System.out.println("Thread A is done."); 30 } catch (InterruptedException e) { 31 e.printStackTrace(); 32 } 33 } 34 } 35 }).start(); 36 37 // 为了显式出效果,此处休眠10秒钟。 38 try { 39 Thread.sleep(10); 40 } catch (InterruptedException e) { 41 e.printStackTrace(); 42 } 43 44 new Thread(new Runnable() { 45 46 @Override 47 public void run() { 48 // 线程B等待获取锁lock 49 System.out.println("thread B is waiting to get lock."); 50 // 获取同步锁才可以执行代码块里面的逻辑 51 synchronized (lock) { 52 try { 53 // 获取到了锁lock 54 System.out.println("thread B get lock."); 55 System.out.println("Thead B is sleeping 10 ms."); 56 // 模拟程序执行 57 // Thread.sleep(10); 58 59 // 此时,将A线程和B线程的wait方法和sleep方法反过来 60 lock.wait();//让出CPU,释放同步锁 61 System.out.println("Thread B is done."); 62 } catch (InterruptedException e) { 63 e.printStackTrace(); 64 } 65 } 66 } 67 }).start(); 68 69 } 70 71 72 }
9、Java线程中锁池EntryList和等待池WaitSet的区别?
答:先了解两个概念,对于java虚拟机中,运行程序的每一个对象来说,都有两个池,锁池EntryList、等待池WaitSet,而这两个吃又与Object基类的wait,notify,notifyAll三个方法,以及synchronized相关。
1)、锁池EntryList,假设线程A已经拥有了某个对象(不是类)的锁,而其它线程B、C想要调用这个对象的某个synchronized方法(或者块),由于B、C线程在进入对象的synchronized方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正被线程A所占用,此时B、C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池,就是将B、C加入到锁池里面。
2)、等待池WaitSet,假设线程A调用了某个对象的wait方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,进入该等待池中的线程不会去竞争该对象的锁,如果线程B执行完之后调用了notify和notifyall方法的话,则处于该对象等待池中的被唤醒的线程A就会进入到该对象lock锁池中,锁池中的对象就会竞争该对象的锁,如果线程B执行完之后,就会将锁自动的释放掉,因此线程A就获得到了锁,但是真实的开发中,多个线程去竞争这个锁,优先级高的线程竞争到这个锁的机率更大,假如某个线程没有竞争到该对象锁,它只会留在锁池中,并不会重新进入到等待池中,而竞争到该对象锁的线程继续向下执行业务逻辑,直到执行完了synchronized方法(或者块)或者遇到了异常才会释放掉该对象锁,这时,锁池中的线程会继续竞争该对象锁。
10、Java线程中 notify 和 notifyall 的区别?
答:1)、notifyall会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会。没有获取到锁的而已经呆在锁池中的线程只能等待其它机会,去获取锁,而不能再次回到等待池中。
2)、notify只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会。
1 package com.thread; 2 3 public class NotificationDemo { 4 5 // 成员变量,volatile修饰的成员变量,表示的是多个线程对其进行修改的时候,一旦线程A对其进行修改 6 // 其它线程都可以立即看到线程A对它的改动。 7 private volatile boolean go = false; 8 9 public static void main(String args[]) throws InterruptedException { 10 11 // 创建一个不可变的对象实例 12 final NotificationDemo notificationDemo = new NotificationDemo(); 13 14 // 等待线程,使线程进入等待状态 15 Runnable waitTask = new Runnable() { 16 17 @Override 18 public void run() { 19 try { 20 notificationDemo.shouldGo(); 21 } catch (InterruptedException e) { 22 e.printStackTrace(); 23 } 24 System.out.println(Thread.currentThread().getName() + " finished Execution"); 25 } 26 }; 27 28 29 // 唤醒线程的线程 30 Runnable notifyTask = new Runnable() { 31 32 @Override 33 public void run() { 34 notificationDemo.go(); 35 System.out.println(Thread.currentThread().getName() + " finished Execution"); 36 } 37 }; 38 39 40 // 创建四个线程 41 Thread t1 = new Thread(waitTask, "WT1"); //will wait等待线程 42 Thread t2 = new Thread(waitTask, "WT2"); //will wait等待线程 43 Thread t3 = new Thread(waitTask, "WT3"); //will wait等待线程 44 Thread t4 = new Thread(notifyTask, "NT1"); //will notify唤醒线程 45 46 //starting all waiting thread 47 t1.start(); 48 t2.start(); 49 t3.start(); 50 51 //pause to ensure all waiting thread started successfully 52 // 休眠200毫秒 53 Thread.sleep(200); 54 55 //starting notifying thread 56 t4.start(); 57 58 } 59 60 /* 61 * wait and notify can only be called from synchronized method or bock 62 * 63 * synchronized方法,需要获取到同步锁才可以执行里面的逻辑的 64 */ 65 private synchronized void shouldGo() throws InterruptedException { 66 // go默认是false 67 while (go != true) { 68 System.out.println(Thread.currentThread() 69 + " is going to wait on this object"); 70 // 无限期等待 71 wait(); //release lock and reacquires on wakeup 72 System.out.println(Thread.currentThread() + " is woken up"); 73 } 74 go = false; //resetting condition 75 } 76 77 /* 78 * both shouldGo() and go() are locked on current object referenced by "this" keyword 79 */ 80 private synchronized void go() { 81 while (go == false) { 82 System.out.println(Thread.currentThread() 83 + " is going to notify all or one thread waiting on this object"); 84 85 go = true; //making condition true for waiting thread 86 //notify(); // only one out of three waiting thread WT1, WT2,WT3 will woke up 87 notifyAll(); // all waiting thread WT1, WT2,WT3 will woke up 88 } 89 } 90 91 92 }
11、Java线程中 yield。
答:1)、当调用Thread.yield()方法的时候,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示。
2)、yield方法对锁的行为不会有影响的,不会让当前线程让出锁。
1 package com.thread; 2 3 public class YieldDemo { 4 5 public static void main(String[] args) { 6 // 创建一个线程,使用的是匿名内部类 7 Runnable yieldTask = new Runnable() { 8 9 @Override 10 public void run() { 11 for (int i = 1; i <= 10; i++) { 12 System.out.println(Thread.currentThread().getName() + i); 13 if (i == 5) { 14 Thread.yield(); 15 } 16 } 17 } 18 }; 19 20 // 当线程A执行到5的时候,会不会让给线程B执行呢。但是最终的决定权还是在线程调度器手上的。 21 Thread t1 = new Thread(yieldTask, "A"); 22 Thread t2 = new Thread(yieldTask, "B"); 23 t1.start(); 24 t2.start(); 25 } 26 27 }
12、Java线程中如何中断线程。
答:已经被抛弃的方法。
1)、通过调用stop()方法停止线程,可以通过一个线程停止另外一个线程,这种方法太过暴力,也不安全,比如线程A调用线程B的stop方法,去停止线程B,调用这个方法的时候,线程A其实并不知道线程B执行的具体情况,这种突然间的停止会导致线程B的一些清理工作无法完成,还有一个情况就是执行stop方法后,线程B会马上释放锁,有可能会引发数据不同步的问题。
2)、通过调用suspend()方法和resume()方法。
目前使用的方法,如何中断线程。
1)、调用Interrupt()方法,通知线程应该中断了。含义是通知线程你应该中断了,该线程到底是中断还是继续执行呢,应该由这个线程自己去处理。
a)、如果线程处于被阻塞状态,例如sleep、wait、join状态,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。
b)、如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。
2)、Interrupt()方法并不能真正的中断线程,需要被调用的线程配合中断。
a)、在正常运行任务的时候,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。在调用阻塞方法的时候,正确去处理InterruptedException异常,例如,在catch异常后就结束线程。
b)、如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。
13、Java线程中线程状态以及状态之间的转换。
1)、新建:通过实现Runnable接口或者继承Thread类可以得到一个线程类,通过new一个线程实例就进入了new即新建状态了。
2)、可运行:此时调用t.start()方法,就进入到了可运行runnable状态。若此时处于runnable状态的线程被OS选中,并获得了时间片之后j就会进入Running状态,Running状态仅仅是逻辑上的划分。
3)、运行中:如果running状态的线程调用了yield()方法可能会让出CPU回到runnable状态,当然这取决于操作系统的调度,yield只是起到了一个建议的作用。如果时间片用完了,线程还没有结束的话也会进入到runnable状态。
4)、阻塞:如果处于Running状态的线程又等待用户输入或者调用thrad.sleep()方法则会进入阻塞状态。此时只会让出CPU,如果当前线程已经获得锁的话,是不会对占有锁有任何影响的,即不会释放已经获得的锁。
5)、锁池:此外,处于Running状态、runnable状态的线程执行synchronized方法或者方法块的时候,发现并未获取到相应的锁,也会进入到阻塞的状态,同时会被放入到锁对象的锁池当中。
6)、等待队列:如果处于Running状态运行中的线程,调用了wait方法之后呢,就会进入到限期或者非限期的等待状态,同时会被放入到锁对象的等待队列当中。
7)、等待队列 -> 锁池 -> 可运行: 处于等待队列中的线程如果wait时间到了或者被其它线程调用notify或者notifyall去唤醒的话,则会被放入到锁池当中,之后,位于锁池中的线程一旦获得了锁,则会再次进入可运行runnable状态当中,被OS选中之后,就会进入到Running状态运行状态。
8)、死亡:最后处于Running状态的线程,在方法执行完毕或者异常退出,该线程就会结束,进入死亡terminated状态。
方便面试回答,还是使用一下经典的图,下图展示了Java线程的生命周期,Java线程具有五种基本状态:
1)、新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new Thread ();
2)、就绪状态(Runnable):当调用线程对象的start()方法后,线程就进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()方法后,此线程立即就会执行;
3)、运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
4)、阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
a)、等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
b)、同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
c)、其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5)、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。