2.1 Java中的线程
如今,计算机用户(以及移动终端和平板电脑用户)使用电脑工作时要同时使用不同的应用程序。阅读新闻、在社交网络上发表文章或听音乐的同时,可以使用文字处理程序编写文档。之所以可以同时做以上所有事情,是因为现代操作系统支持多进程处理。
用户可以同时执行不同的任务。此外,在应用程序内部,你也可以同时做不同的事情。例如,如果你正在使用文字处理程序,在为文本添加粗体样式的同时便可保存文件。这是因为用于编写这些应用程序的现代编程语言允许程序员在应用程序中创建多个执行线程。每个执行线程执行不同的任务,这样你就可以同时做不同的事情。
Java使用Thread类实现执行线程。你可以使用以下机制在应用程序中创建执行线程。
- 扩展
Thread类并重载run()方法。 - 实现
Runnable接口,并将该类的对象传递给Thread对象的构造函数。
这两种情况下你都会得到一个Thread对象,但是相对于第一种方式来说,更推荐使用第二种。其主要优势如下。
Runnable是一个接口:你可以实现其他接口并扩展其他类。对于采用Thread类的方式,你只能扩展这一个类。- 可以通过线程来执行
Runnable对象,但也可以通过其他类似执行器的Java并发对象来执行。这样可以更灵活地更改并发应用程序。 - 可以通过不同线程使用同一
Runnable对象。
一旦有了Thread对象,就必须使用start()方法创建新的执行线程并且执行Thread类的run()方法。如果直接调用run()方法,那么你将调用常规Java方法而不会创建新的执行线程。下面来看看Java编程语言中线程最重要的特征。
2.1.1 Java中的线程:特征和状态
关于Java的线程,首先要说明的是,所有的Java程序,不论并发与否,都有一个名为主线程的Thread对象。你可能知道,Java SE程序通过main()方法启动执行过程。执行该程序时,Java虚拟机(JVM)将创建一个新Thread并在该线程中执行main()方法。这是非并发应用程序中唯一的线程,也是并发应用程序中的第一个线程。
与其他编程语言相同,Java中的线程共享应用程序中的所有资源,包括内存和打开的文件。这是一个强大的工具,因为它们可以快速而简单地共享信息。但是,正如第1章所述,必须使用足够的同步元素避免数据竞争条件。
Java中的所有线程都有一个优先级,这个整数值介于Thread.MIN_PRIORITY和Thread.MAX_PRIORITY之间(实际上它们的值分别是1和10)。所有线程在创建时其默认优先级都是Thread.NORM_PRIORITY(实际上它的值是5)。可以使用setPriority()方法更改Thread对象的优先级(如果该操作不允许执行,它会抛出SecurityException异常)和getPriority()方法获得Thread对象的优先级。对于Java虚拟机和线程首选底层操作系统来说,这种优先级是一种提示,而非一种契约。线程的执行顺序并没有保证。通常,较高优先级的线程将在较低优先级的线程之前执行,但是,正如之前所述,这一点并不能保证。
在Java中,可以创建两种线程。
- 守护线程。
- 非守护线程。
二者之间的区别在于它们如何影响程序的结束。当有下列情形之一时,Java程序将结束其执行过程。
- 程序执行
Runtime类的exit()方法,而且用户有权执行该方法。 - 应用程序的所有非守护线程均已结束执行,无论是否有正在运行的守护线程。
具有这些特征的守护线程通常用在作为垃圾收集器或缓存管理器的应用程序中,执行辅助任务。你可以使用isDaemon()方法检查线程是否为守护线程,也可以使用setDaemon()方法将某个线程确立为守护线程。要注意,必须在线程使用start()方法开始执行之前调用此方法。
最后,不同情况下线程的状态不同。所有可能的状态都在Thread.States类中定义。你可以使用getState()方法获取Thread对象的状态。显然,你还可以直接更改线程的状态。线程的可能状态如下。
NEW:Thread对象已经创建,但是还没有开始执行。RUNNABLE:Thread对象正在Java虚拟机中运行。BLOCKED:Thread对象正在等待锁定。WAITING:Thread对象正在等待另一个线程的动作。TIME_WAITING:Thread对象正在等待另一个线程的操作,但是有时间限制。THREAD:Thread对象已经完成了执行。
在给定时间内,线程只能处于一个状态。这些状态不能映射到操作系统的线程状态,它们是JVM使用的状态。了解了Java编程语言中最重要的线程特性之后,让我们来看看Runnable接口和Thread类最重要的方法。
2.1.2 Thread类和Runnable接口
如前文所述,你可以使用以下任一机制创建新的执行线程。
- 扩展
Thread类并且重载其run()方法。 - 实现
Runnable接口,并将该对象的实例传递给Thread对象的构造函数。
在好的Java实践做法中,相对于第一种方法而言,更推荐使用第二种方法,这将是我们在本章以及整本书中都将采用的方法。
Runnable接口只定义了一种方法:run()方法。这是每个线程的主方法。当你执行start()方法来启动一个新线程时,它将调用run()方法(Thread类的run()方法或者在Thread类的构造函数中以参数形式传递的Runnable对象)。
相反,Thread类有很多不同的方法。它有一种run()方法,实现线程时必须重载该方法,扩展Thread类和你必须调用的start()方法创建新的执行线程。下面给出Thread类的其他常用方法。
- 获取和设置
Thread对象信息的方法。getId():该方法返回Thread对象的标识符。该标识符是在线程创建时分配的一个正整数。在线程的整个生命周期中是唯一且无法改变的。getName()/setName():这两种方法允许你获取或设置Thread对象的名称。这个名称是一个String对象,也可以在Thread类的构造函数中建立。getPriority()/setPriority():你可以使用这两种方法来获取或设置Thread对象的优先级。在本章中,上文已经解释了Java如何管理线程的优先级。isDaemon()/setDaemon():这两种方法允许你获取或建立Thread对象的守护条件。此前已经解释过该条件的原理。getState():该方法返回Thread对象的状态。之前已经介绍过Thread对象的所有可能状态。
interrupt()/interrupted()/isInterrupted():第一种方法表明你正在请求结束执行某个Thread对象。另外两种方法可用于检查中断状态。这些方法的主要区别在于,调用interrupted()方法时将清除中断标志的值,而isInterrupted()方法不会。调用interrupt()方法不会结束Thread对象的执行。Thread对象负责检查标志的状态并做出相应的响应。sleep():该方法允许你将线程的执行暂停一段时间。它将接收一个long型值作为参数,该值代表你想要Thread对象暂停执行的毫秒数。join():这个方法将暂停调用线程的执行,直到调用该方法的线程执行结束为止。可以使用该方法等待另一个Thread对象结束。setUncaughtExceptionHandler():当线程执行出现未校验异常时,该方法用于建立未校验异常的控制器。currentThread():这是Thread类的静态方法,它返回实际执行该代码的Thread对象。
接下来,你将学习如何使用这些方法来实现如下两个示例。
- 一个矩阵乘法应用程序。
- 一个在操作系统中查找文件的应用程序。
