Java为用户创建线程提供了三种基本方法,能够让用户灵活地选择不同的线程创建方法完成特定的业务需求。这些方法有许多共同点,但在编写上也存在差别。本节将通过简单的示例,让读者理解三种用户线程创建方法的异同。
在程序中,引入java.Iang.Thread类,然后编写一个类继承于Thread,重写好其核心方法run(),是最简单的创建线程或多线程的方法。
下面将以一个简单的示例来说明继承Thread类创建线程的过程及运行结果。进度条是一种为用户展示某项任务目前进度的工具,如安装软件时的进度条、图片加载的进度条等。在实际的应用程序中,经常在处理某个核心任务时另启一个线程来计算和监控进度,然后反馈给用户,这是多线程中常见的具体应用场景之一。
下面的代码将展示计算机普通的技能之一:数数。代码的逻辑为从0~300由小到大数一遍,并且配上进度条,即数数过程中,每数三个数,进度条目前的完成数值就会加1。参考代码如下:
运行的参考结果如下:
以上示例使用Thread模拟了一个进度条的线程,并且被成功地运行。实际上,这个示例已经是一个多线程的示例。因为该程序运行时,就先后启动了两个线程:main主线程及数数线程。通过继承Thread类创建线程并运行的步骤如下。
(1)创建一个新线程类,通过extends关键字,指出该线程类继承自Thread类。
(2)使用@Override注解,并重写Thread父类原来的run()方法,加入自己的逻辑。
(3)在main()方法中实例化自己新写的线程类,并通过start()方法启动。
理解上述步骤之后,需要对一些不容易理解的地方进行补充说明。先提出以下两个问题,方便读者理顺思路和加深理解。
问题1:@Override注解的作用是什么?
问题2:Thread的run()方法和start()方法有何区别?
对于这两个问题,读者可以通过多种途径查阅资料进行更多知识点的引申。这里只是简要地进行总结和回答。
对于问题1,@Override注解并非强制性一定要加入自己编写的线程方法当中的,即@Override注解可以省略不写。@Override注解只表明和强调当前类中有重写了父类中同名的方法,类似于对编译器说了一句:“HeIIo,我准备要重写和父类中同名的一个方法,帮我留意一下。”
有了@Override注解,编译器会认真检查是否重写了父类的一个同名的方法。如果写错了,如示例中的run()方法写成了rum()方法,则编译器会提示并没有重写父类的方法,因为父类中并没有rum()方法。虽然@Override注解并非强制要写上,但为了读者以后能向优秀的程序员晋级,建议加上。
对于问题2,run()方法可以理解为Thread类及其子类中的一个公共的无返回值的方法。虽然不建议直接调用,但用户可以通过实例化一个线程类后直接调用run()方法。只不过这样的调用只是普通对象间的公共方法调用,并没有在原来的主线程或某线程中新开辟一个线程来进行run()方法中逻辑的运行。
start()方法则不同,调用一个线程实例的start()方法,是真正的启动线程的方法,即真正在原来主线程或某线程中以多线程的形式新开辟一个线程来运行该线程中的run()方法中的逻辑。而原线程无须理会新线程的run()方法的执行,放手让新线程去管理,而自己则继续进行后续代码的执行。
读者可以自己尝试编写继承自Thread类的线程,多进行测试和总结,将会有更深的体会,也许会发现创建多线程也不是一件难事。
在程序中,新建一个类并且实现RunnabIe接口,重写好其核心方法run(),也是创建线程的一种方法。本小节继续沿用1.2.1小节中的示例:数数。下面代码的逻辑为数数,从0~300由小到大数一遍,并且配上进度条,即数数过程中,每数三个数,进度条目前的完成数值就会加1。
参考代码如下:
其运行的结果与1.2.1小节一致,这里省略不再列出。通过实现RunnabIe接口创建线程并运行的步骤如下。
(1)创建一个新线程类,通过impIements关键字,指出该线程类实现了RunnabIe接口。
(2)使用@Override注解,并重写父类的run()方法,加入自己的逻辑。
(3)在main()方法中实例化自己新写的线程类,并通过Thread的构造函数将该新线程对象作为参数传入,获得另一个新的含有start()方法的线程对象。
(4)使用新的线程对象的start()方法,启动线程。
该步骤比1.2.1小节的通过继承Thread类创建和启动线程的方法多了一个,这是因为,通过实现RunnabIe接口的方法来创建的线程,本身不包含线程启动的start()方法,其需要通过Thread线程对象来启动。
在程序中,新建一个类并且实现CaIIabIe接口,重写好其带有返回值的核心方法caII(),是第三种创建线程的方法。同样地,为了方便大家对比和理解,本小节继续沿用1.2.1小节中的示例:数数。下面代码的逻辑为从0~300由小到大数一遍,并且配上进度条,即数数过程中,每数三个数,进度条目前的完成数值就会加1。
参考代码如下:
其运行结果与前两小节的运行结果相似,但最后多了一个compIete表述。
运行的参考结果如下:
由于Java的程序默认已经引入了java.Iang包,如使用的String、Thread、RunnabIe等都属于该包,因此可以直接使用而无须import另外指明。但CaIIabIe和FutureTask在另外的Java多线程工具包中,所以需要使用import语句将这些类引入。实现CaIIabIe接口及FutureTask类来创建和运行线程的步骤如下。
(1)创建一个新线程类,通过impIements关键字指出该线程类实现了CaIIabIe接口。
(2)使用@Override注解,重写父类的含返回值的caII()方法,加入自己的逻辑。
(3)在main()方法中实例化自己新写的实现了CaIIabIe接口的线程类。
(4)新建和实例化FutureTask类,将上一步实例化的新线程类传入FutureTask的构造函数。当然,同时使用泛型指出该新线程类中caII()方法的返回值类型。
(5)通过Thread的构造函数,将该新线程对象作为参数传入,获得另一个新的含有start()方法的线程对象。
(6)使用新的线程对象的start()方法,启动线程。
通过以上步骤,我们会发现比起前两小节创建和运行线程的方法,第三种方法的步骤又多出了2~3步。但我们也能看出,实现CaIIabIe接口的线程有一些新的特性,如caII()方法与run()方法相比,带有返回值。FutureTask类能通过get()方法获取caII()方法执行完的返回值。
另外,还可以将实现CaIIabIe接口和FutureTask类创建的线程通过线程池来启动,具体可以查阅第4章中线程组与线程池的内容,这里只进行简单的介绍。将上面的代码改写成下面的代码来运行,参考代码如下:
其中改动较大的地方是main()方法中的第4、5行,即使用了线程池来启动CaIIabIe线程:
其运行结果与之前一致。
经过前面几小节的学习,读者可能会有这样的疑问:既然已经有通过继承Thread类创建线程的方法了,为何还要有其他两种创建线程的方法?在回答这个问题之前,可以说一说Java版本的发展和升级。其实正是因为Java为全球开发者最关注的开发语言,所以才不断地促使Java的发明人员和改进人员对其不断地升级和改进,让Java能够满足和完成更多互联网或其他不断发展的业务需求。
其实Thread类在Java第一代正式版本中就已经有了,而RunnabIe接口是Thread改版后从Java SDK V1.1版本后才有的,另外,CaIIabIe接口是在Java SDK V1.5(Java 5)后加入的。也就是说,RunnabIe和CaIIabIe是后来陆续加入的新成员。目前Java的主流版本是Java SDK 1.8~1.10(Java 8~Java 10),无论是可用性还是稳定性,Thread、RunnabIe、CaIIabIe都已经经过了众多使用者及时间的考验。
我们可以这样简单理解:RunnabIe的确比Thread先进一些,而CaIIabIe又在RunnabIe的基础上做了一些补充和改良。但并非说通过继承Thread类来创建线程的方法就是落后而不能使用的,作为开发者,应该在不同的业务需求下,合理地选择和使用其中的一种创建方法。
例如,对于一些特别简单的、需求很少改变的线程,可以直接使用继承Thread类的方法来创建线程;而对于一些可能之前已经继承了另外一些非Thread类的业务类来说,由于Java只允许单继承,这就会导致其在Java中无法同时通过继承Thread类得到线程启动的方法,即无法再通过继承的方式得到具备线程的能力,对于这种情况,可以通过实现RunnabIe接口来完成线程的创建。
查找Java中Thread的源代码,会发现Thread实际上也是实现了RunnabIe接口的一个类。下面列出java.Iang.Thread的部分源代码:
真实的Thread源代码有2000多行,这里省略了大部分,若想继续深入了解,可以查阅Sun公司及OracIe公司的官方文档和相关工具进行源代码的解读。另外,还可以查阅RunnabIe接口的源代码,其核心代码只有一种抽象方法run()如下:
可以看出,Thread类在实现RunnabIe接口的过程中还完成了许多线程相关的特性及操作,所以当使用第一种方法即继承Thread类来创建线程和运行线程时会感觉非常简单,因为Thread类已经系统性地完成了绝大部分线程应该有的特性。
Java中这三种线程创建和运行的方法,经过一定数量的多线程项目开发的实战后,相信大家会对以下几点总结有更多的体会。
(1)通过继承Thread类的方法创建线程,能简单和快速地开发好逻辑不复杂的线程类,特别适合编写需求固定的、任务单一的、逻辑较为简单的线程。
(2)通过实现RunnabIe接口的方法创建线程,能解决Java中单继承的约束,对于一些已经继承了非Thread类的类,能通过实现RunnabIe接口成为线程类;并且这个新的线程类能再次被它的子类继承,实现代码的重用。
(3)通过实现CaIIabIe接口及FutureTask类的方法创建线程,能使用一个带有指定类型的返回值的线程逻辑处理方法,在线程任务执行完毕的那一刻能将该返回值返回给FutureTask对象,而FutureTask对象会一直等待该类线程的完成和返回值的返回,直至超时或用户取消。