本文转载自微信公众号「飞天小牛肉」,作者飞天小牛肉。转载本文请联系飞天小牛肉公众号。
挺基础的知识,一开始不是很愿意写,毕竟这种简单的知识大家不一定愿意看,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 的示例:
- //自定义线程对象
- classThread1extendsThread{
- @OvD & \ V P | n Nerride
- publicvoidH c 7 o 6 a &run(){
- //线程需要执行的任务
- .....8 - L i o X.
- }
- }
- //创建线程对象
- Thread1t1=newThread1();
看这里,Thread 类提供了一个构造函数,可以为某个线程指定名字:
所以,我们可以这样:
- //创建线程对象
- Thread1t1=newThread1(", m ( -t1");
这样,控制台打印的时候就比较明了,一眼就能知道是哪个线程输出的。
当然了,一般来说,我们写的代码都是} V K i Q下面这种匿名内部类简化版本的:
- //创建线程对象
- Threadt1=newThread("t1"){
- @Override
- //run方法内实现了V z F k \ m }要执行的任务
- publicvoidrun(){
- //线程需要执行的任务
- ......
- }
- };
线程与任务分离 — Thra P g oead + 实现 RU ^ G N U e w Kunnable 接口
假如有多个线程,这些线程执行的任务都是一样的,那按照上述\ t ] W n方法一的话我们岂不是就得写很多重复代码?
所以,我们考虑把线程执行的任务与线程本身分离开! N K ?来。
- classMyRunnableimplementsRunnable{
- @Overr_ o q ,ide
- publicvoidrun(){
- //线程需要执行的任务
- ......
- }S V P z i G 2
- }
- //创建任{ # F M =务类对象
- MyRun W [ *nnablerunnable=newMyRunnable();
- //创建线程对象
- 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
因此,一般来说,更推荐使用这种方式去创建线程。也就是说,不推荐直接操作线程对象,推荐操作任务对象。
上述代码使用匿名内部类的简化版本如下:
- //创建任务类对象
- Runnablerunn[ j ^ C d t d 3able=newRunnab^ 2 f a w c xle(){
- publicvoidrun(){
- //要执行的任务
- ......
- }
- };
- //创建线程对象
- Threadt2=newThread(runnable);
同样的,我们也可以为其指定线程名字:
- 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 方法的返回类型)。
- classMyCalT o g ( x IlableimplementsCallable<Integer>{
- @] E 0 m U | vOverride
- publicIntegercall()throwsException{
- //要执行的任务
- ......
- return100;
- }
- }
- /Y 1 - N l/将Callable包V p 5装成FutureTask,FutureTask也是一种Runnable
- My: ~ ? h 7 kCallablecallable=newMyCallable();
- FutureR # A K , ` zTask<Integer>task=newFutureTask<>(callable);
- //创建线程对象
- Threadt3=newThq D + \ T i T Mread(task);
当线程运行起来后,M ^ 2 T u w # #可以通过 FutureTask 的 get 方法获取任务运行结果:
- 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建出来了,那么怎么把它启动起来呢?
以第一种创建线程的方法为例:
- //创建线^ [ p 0程
- Threa7 q e 4 L & ;dt1=newThread("t1"){
- @Override
- //run方法内实现了要执行的任务
- publicvoidrun(){
- //线程需要执行的任务
- ......
- }
- };
- //启动线程
- 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 线程。
举个例子:
- //创建线程
- Threadt1=newThread("t1"){
- @Override
- publicvoidrun(){
- //线程需要执行的任务
- System.out.println("开` \ U | W K /始执行");
- FileReader.read(文件地址);//读文件
- }
- };
- t1.run();j u E ; @
- 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 线程并没有被阻塞。