为了便于后面自己手写线程池,我们必须对如何创建线程非常熟悉,尤其是实现Callable接口的方式;
创建线程共有3种方式:
-
继承Thread类
-
实现Runnable接口
-
实现Callable接口
一、继承Thread类创建线程
(1)继承Thread类,并重写run方法,该run方法就代表了线程将要完成的任务,因此把run()方法称为执行体;
(2)new刚刚创建的Thread子类的实例,即创建了线程对象;
(3)调用线程对象的shart()方法来启动线程;
代码示例:
package com.jiguiquan.www.newthread; public class ExtendsThread { public static void main(String[] args) { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName()+"\t "+i); if (i == 5) { new FirstThreadTest().start(); new FirstThreadTest().start(); } } } } class FirstThreadTest extends Thread { public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName()+"\t "+i); } } }
二、实现Runnable接口创建线程类
(1)创建一个类,实现Runnable接口,并实现该接口的run方法,该run()方法同样是该线程的线程执行体;
(2)new 刚刚创建的Runnable接口的实现类的实例,并以该实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象(在此环节,可以给此线程对象命名);
(3)调用线程对象的start()方法来启动线程;
代码示例:
package com.jiguiquan.www.newthread; public class ImplementRunnable { public static void main(String[] args) { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName()+"\t "+i); if (i == 6) { new Thread(new RunnableThreadTest(), "新线程1").start(); new Thread(new RunnableThreadTest(), "新线程2").start(); } } } } class RunnableThreadTest implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName()+"\t "+i); } } }
执行结果:
main 0 main 1 main 2 main 3 main 4 main 5 main 6 main 7 main 8 新线程1 0 新线程2 0 main 9 新线程2 1 新线程2 2 新线程2 3 新线程2 4 新线程2 5 新线程1 1 新线程1 2 新线程1 3 新线程1 4 新线程1 5 新线程1 6 新线程1 7 新线程1 8 新线程1 9 新线程2 6 新线程2 7 新线程2 8 新线程2 9
三、通过Callable和Future的方式创建线程
首先得到知道,在所有的现有已知的Thread的构造方法中并没有传入Callable实现对象的构造方法
思考:为什么没有传入Callable的构造方法,是Oracle公司的技术人员的疏忽吗?
肯定不是,而是jdk中早就预留了解决办法,继续往下看:
解决方案的思路是:找一个中间类,即实现了Callable接口,又实现了Runnable接口——适配性——面向接口编程的精髓——FutureTask
FutureTask类实现了Runnable接口
FutureTask的构造方法中可以传入一个实现了Callable接口的实现类
所以FutureTask就是我们要找的,可以从中穿针引线的中间类;
第一步,实现线程的调用:
package com.jiguiquan.www.newthread; import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; /** ** 如何使用实现Callable接口的方式来创建线程? * Callable接口有Runnable没有的优势; * 解决方案是:找一个中间类,即实现了Callable接口,又实现了Runnable接口 * @author jiguiquan * */ public class CallableThreadTest { public static void main(String[] args) { //使用FutureTask这个中间类,就可以传入实现了Callable接口的CallableThread类 FutureTask<Integer> futureTask = new FutureTask<>(new CallableThread()); //如何启动? Thread t1 = new Thread(futureTask); t1.start(); } } class CallableThread implements Callable<Integer>{ @Override public Integer call() throws Exception { System.out.println("*******come in Callable"); return 1024; } }
执行结果:
显然,实现了Callable接口的线程,被顺利调用了;
第二步,如何获取Callable实现类线程的返回值:
核心api——futureTask.get() 获取线程中call()方法的返回值;
public static void main(String[] args) throws Exception { //使用FutureTask这个中间类,就可以传入实现了Callable接口的CallableThread类 FutureTask<Integer> futureTask = new FutureTask<>(new CallableThread()); //如何启动? Thread t1 = new Thread(futureTask); t1.start(); System.out.println("返回的结果是:"+futureTask.get()); }
执行结果:
是不是很简单!
四、使用Callable接口实现多个线程作业的异步计算方案
经典案例:ABCD四个人一起作业,A只需要1s完成任务,B和D也都需要1s中完成任务,但是C需要10秒钟才可以完成任务; |
分析:我们可以让ABC三个人按原计划在main线程中作业,但是让C单独去旁边,重新开辟一个新线程进行计算作业,完成作业后,把计算结果交出来,然后和ABD三人的结果相加,即可等到了最终结果;
原单线程计算结果需要13秒;但是使用Callable的办法,只需要10秒就可以完成全部作业;
实现代码:
package com.jiguiquan.www.newthread; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; /** ** 如何使用实现Callable接口的方式来创建线程? * Callable接口有Runnable没有的优势; * 解决方案是:找一个中间类,即实现了Callable接口,又实现了Runnable接口 * @author jiguiquan * */ public class CallableThreadTest { public static void main(String[] args) throws Exception { Long start = System.currentTimeMillis(); //使用FutureTask这个中间类,就可以传入实现了Callable接口的CallableThread类 FutureTask<Integer> futureTask = new FutureTask<>(new CallableThread()); //如何启动? Thread t1 = new Thread(futureTask,"AA"); Thread t2 = new Thread(futureTask,"BB"); t1.start(); t2.start(); //即使为futureTask创建两个线程,同时启动,futureTask也只会执行一次 System.out.println("********多人计算********"); TimeUnit.SECONDS.sleep(3); //主线程,代表ABD三人计算,耗费3秒 int result1 = 100; while (!futureTask.isDone()) { //如果futureTask作业结果没有完成,那么线程在这里“自旋”等待,直到完成后,跳出循环 } int result2 = futureTask.get(); Long end = System.currentTimeMillis(); System.out.println("计算的最终结果是:"+(result1+result2)); System.out.println("总耗时为:"+(end-start)/1000+" 秒"); } } class CallableThread implements Callable<Integer>{ @Override public Integer call() throws Exception { System.out.println("*******come in Callable"); TimeUnit.SECONDS.sleep(10); //代表C一人计算,耗费的10秒 return 1024; } }
执行结果:
结论,遇到这种多人作业,其中一人短板的时候,可以让他单独开辟一个线程去作业,最后将计算结果交回给主线程,统计进去就可以;
为什么不使用其他方式:原因很简单,其他线程的实现方式,没有返回值,即算完后,主线程没有办法直到它的计算结果,肯定不满足要求;
**注意**
1、如果想要获取Callable线程的计算结果,必须要将get方法放到最后:
因为get()方法是一个堵塞操作,因为futureTask的任务还没有做完,所以它获取不到返回值,只有堵塞等待,如果将它放到前面,那么它会堵塞住整个主线程,肯定是不对的,主线程都没办法作业了;
2、即使为futureTask开辟2个线程,start两次,futureTask也只会执行1次,因为任务只有一个;
3、如果想让Callable实现类中的方法执行两次,那么就只能创建两个futureTask1和futureTask2了,分别启动;(间接说明了,futureTask才是线程Thread对象的target)
五、创建线程的3种方式的对比
1、使用Runnable、Callable的方式可实现多接口,Thread的方式只能单继承(虽然简单,但是太弱);
2、Runnable和Callable接口方式的对比:
-
线程执行体不同——Callable接口实现的方法体是Call(),而Runnable接口实现的方法体是run();
-
是否有返回值——Callable的任务执行后可以有返回值,而Runnable的任务时不能有返回值的;
-
是否允许抛出异常——call()方法可以抛出异常,而run()方法不可以;
-
运行Callable任务可以拿到一个Future对象,返回异步执行的结果。它还提供了检查计算是否完成的方法,以“自旋”的方式等待计算的完成,并get()到计算的结果;通过Future对象可以了解任务执行情况,甚至可以取消任务的执行,还可获取执行结果。(boolean cancel(boolean mayInterruptIfRunning)方法可用来执行取消任务的操作)