目录
一、多线程的3种实现方式
(1)继承Thread类。
(2)实现Runnable接口。(void run():该方法无返回值、无法抛出异常)
(3)实现Callable接口。(V call() throws Exception:该方法可以返回结果、可以抛出异常)
(4)如何选择?
<1>选择继承Thread类?
<2>实现Runnable接口。
<3>实现Callable接口。
二、代码演示3种实现情形。
(1)继承Thread类创建多线程。
<1>学习多线程先看一个案例。
<2>问题的分析。
<3>改进。继承Thread类。重写run()方法。在run()方法中写while()循环。
<4>总结。
(2)实现Runnable接口创建多线程。
<1>分析。
<2>代码改进。
<3>测试结果。
(3)实现Callable接口创建多线程。
<0>大致实现步骤。(实现Callable接口满足既能创建新线程又能有返回值的需求)
<1>书上的案例代码示例。解释在注释中。
<2>自己的案例。
<3>接着。任务类"CalcTask"(执行累加操作)
<4>接着。测试类"Test"(main方法中创建、启动线程)
(I)Future接口。
(II)RunnableFuture接口的实现子类"FutureTask"。
(III)FutureTask实现类。
(IIII)举例类似的阻塞方法。
(IIIII)代码。
一、多线程的3种实现方式
(1)继承Thread类。
- 继承java.lang包中的Thread类,重写Thread类的run()方法,在run()方法中实现多线程的代码。
- Thread类是Runnable接口的一个实现类。
- 文心一言说:Thread类本身是一个类,它并不直接实现Runnable接口。但它有一个成员变量target,这个成员变量的类型是Runnable。new Thread(Runnable target)。
(2)实现Runnable接口。(void run():该方法无返回值、无法抛出异常)
- 实现java.lang.Runnable接口。在run()方法中实现多线程的代码。
- 也就是如果它中间有异常,无法抛出异常。只能在run()方法中进行异常处理。不能去线程外部进行处理异常。
(3)实现Callable接口。(V call() throws Exception:该方法可以返回结果、可以抛出异常)
- 实现java.util.concurrent.Callable接口,重写call()方法,并使用Future接口获取call()方法的返回的结果。
- 在实际开发中,虽然异常是一种不好的错误信息。但是它可以作为一种信息返回到外界。
- 比如线程在执行过程,我想要把线程所执行的结果返回。像12306的多座位类型分配座位,开一个线程:分配商务座、其它座,线程执行完成之后,将抢到的的座位信息返回给主线程。
- 线程启动比实现Runnable接口还要麻烦一点。就是在启动线程的时候要依赖Thread类,但又不能直接传给Thread类,得借助于另外一个类的对象。后面详细介绍。
(4)如何选择?
<1>选择继承Thread类?
- 启动线程代码简单。但是创建的线程类不能再继承其他类,少用。因为程序的扩展性降低。
- 通过继承Thread类实现了多线程,因为有局限性。因为Java只支持单继承,一个类一旦继承了某个父类,就无法再继承Thread类了。如:Student类继承Person类,那么Student类无法再通过继承Thread类创建线程。
- 但是平时的学习、简单的测试比较方便使用。
<2>实现Runnable接口。
- 为了克服上面方法的弊端——>Thread类提供一个构造方法——new Thread(Runnable target),其中参数Runnable是一个接口,它只有一个run()方法。
这样我们的线程类可以再去继承其他类。但是启动线程的码较复杂,依赖Thread类,调用Thread类的start()。还是比较推荐该方法。保留了程序可扩展性。
<3>实现Callable接口。
- 通过Thread类和Runnable接口实现多线程时,需要重写run()方法,但是由于run()方法没有返回值,无法从新线程中获取返回结果。Java提供了Callable接口来满足这种既能创建新线程又有返回值的需求。
- 因为call()方法有返回值且可抛出异常。可根据实际情况进行选择。比较推荐。
二、代码演示3种实现情形。
(1)继承Thread类创建多线程。
(本模块直接按照上课书本《Java基础入门》来举例)
<1>学习多线程先看一个案例。
- 测试类Example02。main方法中创建Thread01类的对象,调用run()方法,执行while()循环输出"Thread01的run()方法正在执行",接着主线程main也写一个while()循环去打印"main()方法的run()方法正在执行"。
- 测试类代码。
/** * @Title: Example01 * @Author HeYouLong * @Package PACKAGE_NAME * @Date 2024/10/26 下午10:58 * @description: 测试类 */ public class Example01 { public static void main(String[] args) { MyThread01 myThread = new MyThread01(); myThread.run(); while (true){ System.out.println("main()方法在运行!"); } } } //第二个类不能用修饰符 class MyThread01 { public void run(){ while(true){ System.out.println("MyThread类的run()方法正在运行!"); } } }
- 测试代码的运行结果。
<2>问题的分析。
- 这里可以看到程序一直在打印"MyThread类的run()方法正在运行!"这是因为该程序是一个单线程程序!
- 代码中调用MyThread01类的run()方法时,执行里面的死循环,因此MyThread01类的println语句一直在执行!而main()方法当中println语句一直无法得到执行。
- 如果希望上面程序的两个死循环while()中的println语句能够并发的执行,就需要实现多线程。
<3>改进。继承Thread类。重写run()方法。在run()方法中写while()循环。
- 测试类代码。
/** * @Title: Example01 * @Author HeYouLong * @Package PACKAGE_NAME * @Date 2024/10/26 下午10:58 * @description: 测试类 */ public class Example02 { public static void main(String[] args) { MyThread02 myThread = new MyThread02(); //创建MyThread02类的线程对象 myThread.start(); //开启线程 while (true){ //死循环输出信息 System.out.println("main()方法在运行!"); } } } //第二个类不能用修饰符 class MyThread02 extends Thread { @Override public void run() { while (true){ //死循环输出信息 System.out.println("MyThread类的run()方法正在运行!"); } } }
- 测试代码的运行结果。
<4>总结。
- 上面代码利用两个线程模拟多线程环境。
- 在main()方法当中创建了MyThread02类的线程对象myThread。并且通过myThread对象调用start()方法启动新线程。
- 单线程程序在运行时,会按照代码的调用顺序执行。
- 多线程程序中,main()方法和MyThread类的run()方法可以同时运行,互不影响。
(2)实现Runnable接口创建多线程。
<1>分析。
- 提供Thread类提供的构造方法——new Thread(Runnable target)。
- 当Thread(Runnable target)构造方法去创建线程对象时,只需要为该方法传递"一个已经实现了Runnable接口的对象"。
- 然后创建的线程将实现Runnable接口中的run()方法作为运行代码,而不需要调用Thread类中的run()方法。
<2>代码改进。
/** * @Title: Example01 * @Author HeYouLong * @Package PACKAGE_NAME * @Date 2024/10/26 下午10:58 * @description: 测试类 */ public class Example03 { public static void main(String[] args) { MyThread03 myThread = new MyThread03(); //创建MyThread03类的线程对象,并且该类已经实现Runnable接口 Thread thread = new Thread(myThread); //创建线程对象 thread.start(); //开启线程,执行线程中的run()方法。 while (true){ //死循环输出信息 System.out.println("main()方法在运行!"); } } } //第二个类不能用修饰符 class MyThread03 implements Runnable { @Override public void run() { while (true){ //死循环输出信息 System.out.println("MyThread类的run()方法正在运行!"); } } }
<3>测试结果。
(3)实现Callable接口创建多线程。
<0>大致实现步骤。(实现Callable接口满足既能创建新线程又能有返回值的需求)
- 创建Callable接口的实现类,同时重写Callable接口的call()方法。
- 创建Callable接口的实现类对象。
- 通过线程处理类FutureTask的有参构造方法封装Callable接口的实现类对象。
- 调用参数为FutureTask类对象的有参构造方法Thread()创建Thread线程实例。
- 调用线程实例的start()方法启动线程。
<1>书上的案例代码示例。解释在注释中。
- 代码。
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /** * @Title: Example01 * @Author HeYouLong * @Package PACKAGE_NAME * @Date 2024/10/26 下午10:58 * @description: 测试类 */ public class Example04 { public static void main(String[] args) throws ExecutionException, InterruptedException { MyThread04 myThread = new MyThread04(); //创建MyThread04类的线程对象,并且该类已经实现Callable接口 //使用Future接口的实现子类封装MyThread04类 FutureTask<Object> futureTask = new FutureTask<>(myThread); //通过Thread(Runnable target,String name)构造方法创建线程对象 Thread thread = new Thread(futureTask,"线程1"); //创建线程对象 thread.start(); //开启线程,执行线程中的run()方法。 //通过get()方法获取任务类FutureTask的执行结果(call()方法) System.out.println(Thread.currentThread().getName()+"返回的结果:i ="+futureTask.get()); int a=0; while(a++ < 5){ System.out.println("main()方法正在运行!"); } } } //第二个类不能用修饰符 //定义一个实现Callable接口的实现类 class MyThread04 implements Callable<Object> { @Override public Object call() throws Exception { int i =0; while (i++ <5){ System.out.println(Thread.currentThread().getName()+"的call()方法正在运行!"); } return i; } }
- 运行结果。
<2>自己的案例。
- 使用一个线程执行累加操作。执行完后需要把累加的结果返回。
- 通过主线程调用,让某一个线程做累加操作。主线程不会去等待它,等它执行完操作,将返回的值返回回来并拿到它。
- 让任务类实现Callable接口,并重写call()方法。
- 因为累加操作。所以指定泛型为:Integer。
- 如何封装Callable接口类型的数据,然后调用Thread类的构造方法去创建、启动线程?
<3>接着。任务类"CalcTask"(执行累加操作)
- 当线程启动,会自己调用重写的call()方法。进行累加操作。
- 通过线程睡眠,模拟执行操作后返回结果需要的时间。
package com.fs; import java.util.concurrent.Callable; /** * @description: 累加任务 */ /* 使用一个线程执行累加操作。*/ public class CalcTask implements Callable<Integer> { //重写call()方法 @Override public Integer call() throws Exception { int sum = 0; for (int i = 0; i < 100; i++) { sum += i; //线程睡眠,模拟处理消耗的时间 Thread.sleep(10); } return sum; } }
<4>接着。测试类"Test"(main方法中创建、启动线程)
- 这里实现Callable接口——>创建、启动线程的方法要依赖于Thread类。
- 如果是实现Runnable接口,就是可以这样进行如下操作(new Thread(Runnable target))。但是它不行,newThread(参数),参数没有提供。
- 在Thread构造方法当中,它没办法接收Callable接口类型的数据,但可以接收Runnable接口类型的数据。所以还得往下面看。
(I)Future接口。
- 表示一个未来的。它的作用就是包裹一个"未来的结果"。比如Callable接口通过线程启动完成,执行call()方法就会返回一个结果。Future是一个接口,肯定不能new去使用它,但是它有实现类。
- Future接口的子接口。
- ScheduledFuture接口(定时任务、任务调度)、RunnableFuture接口(继承Runnable、Future接口)。
(II)RunnableFuture接口的实现子类"FutureTask"。
- RunnableFuture接口。它的实现类就是我们这次要用的类"FutureTask"。它的父类是Runnable是子接口,那么它就是相当于一个"孙子类"。
(III)FutureTask实现类。
- 它有两个构造方法。一个可以传一个Callable。另外一个可以传一个Runnale。
- 其中它的get()方法是一个"阻塞"方法。也就是某个调用call()方法的线程没有执行完,它会一直等着,直到拿到返回的对应接口类型的值。
- 其次这个get()方法还有一个有参数的,它就是如果等待的时间过长(如果不处理,可能导致"死锁"产生),就会抛出异常。
- 这里就可以相当于join()方法。之前《龟兔赛跑》时,主线程调用join()方法,等待"兔子线程"、"乌龟线程"执行完了后,再统计结果。而这里使用,就可以用get()方法等待两个线程执行完并获取到返回值,再统计结果。
- 取消方法。其实就是执行call()方法时,不想执行了。就可以让线程调用cancel()方法,让它停止执行call()方法了。
(IIII)举例类似的阻塞方法。
- Scanner类的next()方法。同样等着输入回车后,程序才向下执行。Scanner的底层也是是输入流。
- IO流的read()方法。
(IIIII)代码。
- 启动线程后,会等待运行一段时间才有结果。
- 因为即使main线程抢到时间片,而get()方法将这里"阻塞"。直到Thread01执行完call()方法并拿回结果"sum",才继续往下执行。
package com.fs; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /** * @Title: Test * @Author HeYouLong * @Package com.fs * @Date 2024/10/13 下午4:53 * @description: 测试类 */ public class Test { public static void main(String[] args) throws ExecutionException, InterruptedException { //创建任务。创建、启动线程 CalcTask calcTask = new CalcTask(); /* * 将已经实现Callable接口的任务类封装起来 * 再使用newThread() * */ //创建一个FutureTask 对象包含callable的对象, FutureTask<Integer> futureTask = new FutureTask<>(calcTask); Thread thread01 = new Thread(futureTask); //调用get()方法获取Callable线程的call()方法返回结果。 // get()阻塞的,如果callable线程的call()方法没有执行完成,一直在等待,call()方法执行完毕 //线程启动,自己会调用call()方法 thread01.start(); //通过调用get()方法拿到结果 Integer rs = futureTask.get(); System.out.println("main线程执行,rs:"+rs); } }
- 测试。我们让main线程只等100毫秒。而我们的call()方法需要执行10*100=1000毫秒。现在在启动再看看结果。
- 稍微修改一下get()方法。
//通过调用get()方法拿到结果 //MILLISECONDS:毫秒 Integer rs = futureTask.get(100, TimeUnit.MILLISECONDS);//记得处理异常或者抛出
- 如果到达超时时间,还没有得到结果,抛TimeoutException异常。