本文转载自微信公众号「飞天小牛肉」,作者飞天小牛肉。转载本文请联系飞天小牛肉公众号。

挺基础的知识,一开始不是很愿意写,毕竟这种简单的知识大家不一定愿意看,3 6 * b $ E而且容易写的大众化,不: l z D G : * _ :过还好梳理一R E d : N u v遍下来还算是有点收获,比如我看了 Threadn s . [ S 类重写的 run 方法,才明白为什么可以把任务(Runnable)和线程本身(Thread)分开来。

创建线程的三种方法

线程英译是 Thread,这也是 Java 中~ 0 ~ V r P 5 F |线程对应的类名,在 java.lang 包下。

注意下它实现了 Runnable 接口,下X + T # i ] 2文会详细解释。

线程与任务合并 — 直接继承 Thread 类

线程创建出来自然是需要执行一些特定的任务的,一个线程需要执行的任务、或者说需要做的事情就在 Thread 类的 run 方法里面定义。

这个 run 方法是哪里来的呢?

事实上,它并不是 Th/ b b o Z !read 类自己的。Thread 实3 g k n } O现了 Runnable 接口,run 方法m D w L W – q Z正是在这个接口中被定义为了抽象方法,而 Thread 实现了这个方法。

所以,我们把这个 Runnable 接口称为任务类可能更好理解r , , ! $

如下,S L $ g 6 L .就是通过集成 Thread 类创建一个自定义线程 Thread1 的示例:

  1. //自定义线程对象
  2. classThread1extendsThread{
  3. @OvD & \ V P | n Nerride
  4. publicvoidH c 7 o 6 a &run(){
  5. //线程需要执行的任务
  6. .....8 - L i o X.
  7. }
  8. }
  9. //创建线程对象
  10. Thread1t1=newThread1();

看这里,Thread 类提供了一个构造函数,可以为某个线程指定名字:

所以,我们可以这样:

  1. //创建线程对象
  2. Thread1t1=newThread1(", m ( -t1");

这样,控制台打印的时候就比较明了,一眼就能知道是哪个线程输出的。

当然了,一般来说,我们写的代码都是} V K i Q下面这种匿名内部类简化版本的:

  1. //创建线程对象
  2. Threadt1=newThread("t1"){
  3. @Override
  4. //run方法内实现了V z F k \ m }要执行的任务
  5. publicvoidrun(){
  6. //线程需要执行的任务
  7. ......
  8. }
  9. };

线程与任务分离 — Thra P g oead + 实现 RU ^ G N U e w Kunnable 接口

假如有多个线程,这些线程执行的任务都是一样的,那按照上述\ t ] W n方法一的话我们岂不是就得写很多重复代码?

所以,我们考虑把线程执行的任务与线程本身分离开! N K ?来。

  1. classMyRunnableimplementsRunnable{
  2. @Overr_ o q ,ide
  3. publicvoidrun(){
  4. //线程需要执行的任务
  5. ......
  6. }S V P z i G 2
  7. }
  8. //创建任{ # F M =务类对象
  9. MyRun W [ *nnablerunnable=newMyRunnable();
  10. //创建线程对象
  11. T5 H u m 5 7 F $ lhreadt2q [ % W /=newThread(runnable);

除了避免了重复代码,使用实现 Runnable 接口的方式也比方法一的单继承 Thread 类更具灵活性,毕竟一个类只能继承一个父类,如x ; 4 y : % N果这个类本身已经继承了其$ U 7 h S K (它类,就g g 4 { \ ;不能使用第一种方法了。另外,用这种方式,也更容易与线程; V : 3 {池等高级 API 相结合。m + q Y R E

因此,一般来说,更推荐使用这种方式去创建线程。也就是说,不推荐直接操作线程对象,推荐操作任务对象。

上述代码使用匿名内部类的简化版本如下:

  1. //创建任务类对象
  2. Runnablerunn[ j ^ C d t d 3able=newRunnab^ 2 f a w c xle(){
  3. publicvoidrun(){
  4. //要执行的任务
  5. ......
  6. }
  7. };
  8. //创建线程对象
  9. Threadt2=newThread(runnable);

同样的,我们也可以为其指定线程名字:

  1. Threz t u * m c * F eadt2=newThread(runnable,"t2");

以上两个 Thread 的构_ E m Z造函数如图所示:

可以M * r u 3 A发现,Thread 类的构造函: : ) # N数无一例外全部调用了 init 方法,这个方法到底做了啥?我们点进去看看:

它将构造函数传进来的 Runnable 对象传给了一个成员变量 target。

target 就是 Thread 类中定义的 Runnable 对象,代表着需要执行的任务(What will be run)。

这个变量的存在,就是我们能够把任务(Runnablec $ n @ k)和线程本身(Thread)分开的p Z & I D E原因所在。看下面这段代码:

没错,这就是 Thread 类默g Y e认实现的 run 方法。

在使用第一种方法创建线程的时候,我们定义了一个 Thread 子类并重写了其父类的 run 方法M * , r I – e,所以这个父类实现的 run 方法不会被执行,2 @ Z = e { c E执行的是我们自定义的子类中的 run 方法。

而在使用i [ A第二种方法创建线程的时候,我5 / y L / . | h们并没有在 Thread 子类中重写 run 方法,所以父类默认实现的 run 方法就会被执行。

而这段 run 方法代码的意思就是说,如果 taget != null,也就是说如果 Thread 构造函数中传入了 Runnable 对象,那就执行这个 Runnable 对象的D / \ $ B run 方法。

线程与任务分离 — Thread + 实现 Callable 接口

虽然 RunN 8 X 0 3 / onable 挺不错的,但是仍然有个缺点,那M | m D ; P w E i就是没办法获取任务的Z { W执行结果,因为它的{ 7 – ? . 0 run 方法返回值是 void。

这样,& e t对于需要获取任务执行结果的线程来说,Callable 就成为了一个完美的选择。

Callable 和 Runnable 基^ Y { M z E . j本差不多:

和 Ruq h @ 4 Fnnbaleg – \ / r f 比起来,Callt m D O s Mable 不过就是N V \ Z D d把 run 改成了 can # sll。当然,最重要的是!和 void run 不同,这个 call 方法是拥有返回值的,而且能够抛出异常。

这样,一\ x 8 9 ?个很自然的想法,就是把 Callable. \ A 作为任务对象传给Y , l A _ 0 = ] ! Thread,然后 Thread 重写 call 方法就完事儿。

But,遗憾的是,Thread 类的构造函数里并不接收 Callable 类型的参数。

所以,我们需要把 Callable 包装一下,包装成 Runnablex o | Y 类型,这样就能传给 Thread 构造函数了。

为此,FutureTask 成为了最好的选择。

可以看到 FutureT_ I Qask 间接继承了 Runnable 接口,因此它也可以看作是一s X ( r个 Runnable 对象,可以作为参数传入 Thread 类的构造函数。

另外,Futurn v [ j ! 5 _ 9eTask 还间接继承了 Future 接口,并且,这个 Future 接口定义了可以获取 call() 返回值的方法 gete d 8 e:

看下面这段代码,使用 Callable 定义一x t k | 0 H } # _个任务对象,然后把 Call) Y T i T j e O oable 包装成 FutureTask,然后把 FutureTask 传给 Thread 构造函数,从而创建出一个线程对象。

另外,Ca9 ) k lllable 和 FutureTask 的泛型填的就是 Callable 任务返回的结果类型(就是 call 方法的返回类型)。

  1. classMyCalT o g ( x IlableimplementsCallable<Integer>{
  2. @] E 0 m U | vOverride
  3. publicIntegercall()throwsException{
  4. //要执行的任务
  5. ......
  6. return100;
  7. }
  8. }
  9. /Y 1 - N l/将Callable包V p 5装成FutureTask,FutureTask也是一种Runnable
  10. My: ~ ? h 7 kCallablecallable=newMyCallable();
  11. FutureR # A K , ` zTask<Integer>task=newFutureTask<>(callable);
  12. //创建线程对象
  13. Threadt3=newThq D + \ T i T Mread(task);

当线程运行起来后,M ^ 2 T u w # #可以通过 FutureTask 的 get 方法获取任务运行结果:

  1. Integerresult=task.get();

不过,需要注意的. . y s Y是,get 方法^ $ p / k H会阻塞住当前调用这个方法的线程。比如说w X ^ \ Z q C $我们在主线程中调用了 get 方法去获取 t3 线程的任务运行结果,那么只有这个 call 方法成功返回了,主线程才能够继续往下执行。

换句话说,如果 cb b c @ ; / 8 3all 方法一直得不到结果,那么主线程也就一直无法向下运行。

启动线程

OK,综上,我们已经把线程成功创\ U n ( R M建出来了,那么怎么把它启动起来呢?

以第一种创建线程的方法为例:

  1. //创建线^ [ p 0
  2. Threa7 q e 4 L & ;dt1=newThread("t1"){
  3. @Override
  4. //run方法内实现了要执行的任务
  5. publicvoidrun(){
  6. //线程需要执行的任务
  7. ......
  8. }
  9. };
  10. //启动线程
  11. t1.start();

这里涉及一道经典的面试题,7 ~ v A ]即为什么使用 start 启动线程,而不使用 run 方法启动_ ? V L E线程?

使用 run 方法启动线程看起来好像并没啥问题,对吧,run 方法内定义了要执行的任务,调用 run 方法不就执行了这个任务了@ # ? ] ]?

这确实没错,任务确实能够被正确执行,但是并不是以多线程的方式,当我们使/ y 8 w 2用 t1.run() 的时候,程序仍然是在创建 t1 线程的 main 线程下运行的,并没有创建出a Z ( :一个^ b ~ : ^ o 0 l T新的 t1 线程。

举个例子:

  1. //创建线程
  2. Threadt1=newThread("t1"){
  3. @Override
  4. publicvoidrun(){
  5. //线程需要执行的任务
  6. System.out.println("开` \ U | W K /始执行");
  7. FileReader.read(文件地址);//读文件
  8. }
  9. };
  10. t1.run();j u E ; @
  11. System.out.println("执行完毕");

如果使用 run 方法启动线程,”执行完毕” 这句话需要在文件读取完毕后才能够输出,也就是说读文件这个z @ t q d 4操作仍然是同步的。假设读取操作花费了 5 秒钟,如果没有线程调度机制,这 5 秒 CPUE R u W P O c 什么都做不了,其它代码都得暂停。

而如果使用 start 方法启动线程,”执行完毕” 这句话在文件读取完毕之前就会被很快地输出,L Q J x因为多线程让方法执行变成了异步的,读取文件这个操作是 t1 线程W u L在做,而 main 线程并没有被阻塞。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注