第15章 多线程编程 本章将要讲述Java中线程(Thread)相关的内容。它是一个全新的事物。为了理解本 章的内容,需要用到前面学到的以下知识点。 口方法的调用过程: 口程序执行流程; 口继承和覆盖: 口接口和内部类: 口类文件即是Java平台的可执行文件。 线程原本是操作系统中的一个概念。在绝大多数平台上,Java平台中的线程其实就是 利用了操作系统本身的线程。对于学习Jva线程而言,最重要的内容是理解线程。在理解 了线程之后,再去学习Java中常用的线程编程其实不难。除了介绍线程的概念,本章还会 讲解Java线程的使用、多线程编程和线程同步的基本知识。这些都是最常用的线程编程 技术。 本章15.1节用来讲述线程的概念,是本章中最重要的一节。对于线程这种抽象的概念, 一次看不懂也是正常的。15.1节是全章的基础,理解了15.1节的内容,本章剩余的内容就 不难理解了。所以请读者在继续后面的内容之前,务必将15.1节的内容看懂。好,下面首 先理解线程的概念。 15.1线程—执行代码的机器 线程是编程中极其重要的一部分内容,但是对于初学线程的读者来说,它的概念显得 过于抽象而不好理解。和程序的代码不同,线程是隐藏在程序背后的,对于编程者来说它 是看不见摸不着的。为了形象地描绘线程的作用,本节将使用一个“CD机模型”和“演 奏会模型”来与线程进行类比。为了明白线程,首先需要了解Java程序是如何运行的。 15.1.1线程一执行代码的基本单位 什么是线程呢?它不是Java语言语法的一部分。在Java中,线程可以说是一个“机 器”,它的作用就是执行Java代码。换句话说,Java中的代码,都是通过线程为基本单位 来执行的。图l5-1描绘了前面学习的从Java源代码到生成Java类文件的过程。 相信这个过程大家并不陌生,本章后面的内容对上面这个过程将不再叙述。生成了Java 类文件之后,就是运行Java程序了。上段中说过,线程是Java中程序执行的基本单位, 执行一个Java程序(有main0方法的Java类)的过程如图l5-2所示
第 15 章 多线程编程 本章将要讲述 Java 中线程(Thread)相关的内容。它是一个全新的事物。为了理解本 章的内容,需要用到前面学到的以下知识点。 方法的调用过程; 程序执行流程; 继承和覆盖; 接口和内部类; 类文件即是 Java 平台的可执行文件。 线程原本是操作系统中的一个概念。在绝大多数平台上,Java 平台中的线程其实就是 利用了操作系统本身的线程。对于学习 Java 线程而言,最重要的内容是理解线程。在理解 了线程之后,再去学习 Java 中常用的线程编程其实不难。除了介绍线程的概念,本章还会 讲解 Java 线程的使用、多线程编程和线程同步的基本知识。这些都是最常用的线程编程 技术。 本章 15.1 节用来讲述线程的概念,是本章中最重要的一节。对于线程这种抽象的概念, 一次看不懂也是正常的。15.1 节是全章的基础,理解了 15.1 节的内容,本章剩余的内容就 不难理解了。所以请读者在继续后面的内容之前,务必将 15.1 节的内容看懂。好,下面首 先理解线程的概念。 15.1 线程——执行代码的机器 线程是编程中极其重要的一部分内容,但是对于初学线程的读者来说,它的概念显得 过于抽象而不好理解。和程序的代码不同,线程是隐藏在程序背后的,对于编程者来说它 是看不见摸不着的。为了形象地描绘线程的作用,本节将使用一个“CD 机模型”和“演 奏会模型”来与线程进行类比。为了明白线程,首先需要了解 Java 程序是如何运行的。 15.1.1 线程——执行代码的基本单位 什么是线程呢?它不是 Java 语言语法的一部分。在 Java 中,线程可以说是一个“机 器”,它的作用就是执行 Java 代码。换句话说,Java 中的代码,都是通过线程为基本单位 来执行的。图 15-1 描绘了前面学习的从 Java 源代码到生成 Java 类文件的过程。 相信这个过程大家并不陌生,本章后面的内容对上面这个过程将不再叙述。生成了 Java 类文件之后,就是运行 Java 程序了。上段中说过,线程是 Java 中程序执行的基本单位, 执行一个 Java 程序(有 main()方法的 Java 类)的过程如图 15-2 所示
第15章多线程编程 编写Java源文件 编译Java源文件 -没有语法错误· 生成class文件 有语法错误 图15-1生成Java类文件的过程 Java平台 读取并加载class文件 创建一个线程 启ava平台 Java平台退出 让线程从main(O方法 开始执行程序 main(方法结束,线 程也随之结束 图15-2Java程序执行过程 在图l5-2中,启动Java平台就是我们在命令行执行java命令,Java平台退出就是java 命令执行结束。中间的图表示了Java平台执行的过程。因为是在控制台上直接使用java 命令执行一个类文件的,所以很容易觉得java命令是执行Java代码的基本单位。实际上, java命令是通过创建一个Java线程来执行Java代码的。 □说明:Java线程当然也是Java平台的一部分。在本章中为了突出Java线程,从概念上 将它从Java平台中剥离了出来单独讲解。 1.Java线程和CD机 从线程的角度来看,Java平台更像是一个线程管理器。下面我们通过一个例子,来说 明类文件、Java线程和Java平台的关系。大家都用过CD机,CD机中读取CD碟片内容 的部件就是CD机上的激光头。CD和Java之间各个元素可以做如下类比,如图15-3所示。 class文件 CD碟片 Java线程 CD机中的激光头 Java平台 CD机 图15-3Java和CD的对比 ·439·
第 15 章 多线程编程 ·439· 图 15-1 生成 Java 类文件的过程 图 15-2 Java 程序执行过程 在图 15-2 中,启动 Java 平台就是我们在命令行执行 java 命令,Java 平台退出就是 java 命令执行结束。中间的图表示了 Java 平台执行的过程。因为是在控制台上直接使用 java 命令执行一个类文件的,所以很容易觉得 java 命令是执行 Java 代码的基本单位。实际上, java 命令是通过创建一个 Java 线程来执行 Java 代码的。 说明:Java 线程当然也是 Java 平台的一部分。在本章中为了突出 Java 线程,从概念上 将它从 Java 平台中剥离了出来单独讲解。 1.Java线程和CD机 从线程的角度来看,Java 平台更像是一个线程管理器。下面我们通过一个例子,来说 明类文件、Java 线程和 Java 平台的关系。大家都用过 CD 机,CD 机中读取 CD 碟片内容 的部件就是 CD 机上的激光头。CD 和 Java 之间各个元素可以做如下类比,如图 15-3 所示。 图 15-3 Java 和 CD 的对比
第2篇Java语言高级语法 class文件就如同CD碟片:class文件中包含Java程序的可执行代码:CD中包含着音 乐文件。它们都是数据的载体。 Java线程就如同CD中的激光头:Java线程负责执行class文件中的代码:激光头负责 读取并处理CD上的音乐文件。它们都是负责处理数据的。 Java平台就如同CD机:Java平台包含Java线程,然后,Java线程还负责管理Java 线程,包括创建Java线程,为Java线程提供各种资源(在这里不去深究是什么资源,可 以将之理解为Java线程执行代码的各种基础条件):CD机包括激光头,同时它也管理着 激光头,为激光头供电,同时还提供外壳、马达、播放控制、音频解码和音频输出等各种 功能,没有这些功能,激光头本身无法处理数据。Java平台和CD机可以说是独立的系统, 可以完成一个功能:线程和激光头是它们中的核心部件,但是并不是可独立完成整个工作 的部件。 2.从CD机的工作机制看Java线程 通过上面这个例子,相信线程这个概念己经不完全陌生了。对于激光头,它会从CD 的某个位置开始,按照顺序读取CD上的数据。那么线程的工作模式是怎样的呢?其实和 激光头很类似一只要给线程一个“开头”,线程就会一直沿着这个“开头”执行下去。 对于前面的所有例程来说,这个“开头”在图15-2中已经说明了,它就是我们再熟悉不过 的main()方法。也就是说,线程会从main()方法开始执行程序。 CD机的作用就是播放音乐,当CD在播放完CD后,激光头就会关闭,CD机也会自 动关机。同样的道理,Java平台的作用就是执行Java程序代码。线程在执行完main()方法 后,也就结束了。而当Java平台发现自己里面的线程都退出以后,也就会退出。这时Java 程序就运行完毕了。 那么,线程是如何执行代码的呢?这个超出了本书的范围。就好像使用CD机一样, 使用者只要知道CD机中激光头是用来读取和处理CD碟片上的内容就可以了,没必要去 追究它是如何读取CD碟片上的内容的。对于线程也是一样的道理。前面说过,线程是操 作系统中的一个概念,所以“线程是如何执行程序的”这个问题属于操作系统课程中的内 容。对于学习Java而言,在这里先知道如下几点就可以了。 口线程是执行Java程序代码的基本单位。 口Java线程也是Java平台的一部分。 口Java线程是运行在Java平台内部的,Java平台负责管理Java线程。 口Java线程执行程序代码时,Java平台为其提供各种所需的条件。 口当线程执行完给它的方法后,就会退出。Java平台中如果没有正在运行的线程, 就代表程序执行完毕,Java平台也就自动退出了。 本节的内容就先到这里。本节中使用的“CD机模型”比较容易理解,但是它和线程 还不是十分相似。在15.1.2节中将通过“演奏会模型”来加深对线程的理解。 15.1.2演奏会模型 线程是隐藏在Java平台之中的,它的工作方式并没有展露在Java语法中。我们只能 够通过类比的方式来理解Java线程以及程序代码、Java线程和Java平台三者之间的关系。 ·440·
第 2 篇 Java 语言高级语法 ·440· class 文件就如同 CD 碟片:class 文件中包含 Java 程序的可执行代码;CD 中包含着音 乐文件。它们都是数据的载体。 Java 线程就如同 CD 中的激光头:Java 线程负责执行 class 文件中的代码;激光头负责 读取并处理 CD 上的音乐文件。它们都是负责处理数据的。 Java 平台就如同 CD 机:Java 平台包含 Java 线程,然后,Java 线程还负责管理 Java 线程,包括创建 Java 线程,为 Java 线程提供各种资源(在这里不去深究是什么资源,可 以将之理解为 Java 线程执行代码的各种基础条件);CD 机包括激光头,同时它也管理着 激光头,为激光头供电,同时还提供外壳、马达、播放控制、音频解码和音频输出等各种 功能,没有这些功能,激光头本身无法处理数据。Java 平台和 CD 机可以说是独立的系统, 可以完成一个功能;线程和激光头是它们中的核心部件,但是并不是可独立完成整个工作 的部件。 2.从CD机的工作机制看Java线程 通过上面这个例子,相信线程这个概念已经不完全陌生了。对于激光头,它会从 CD 的某个位置开始,按照顺序读取 CD 上的数据。那么线程的工作模式是怎样的呢?其实和 激光头很类似——只要给线程一个“开头”,线程就会一直沿着这个“开头”执行下去。 对于前面的所有例程来说,这个“开头”在图 15-2 中已经说明了,它就是我们再熟悉不过 的 main()方法。也就是说,线程会从 main()方法开始执行程序。 CD 机的作用就是播放音乐,当 CD 在播放完 CD 后,激光头就会关闭,CD 机也会自 动关机。同样的道理,Java 平台的作用就是执行 Java 程序代码。线程在执行完 main()方法 后,也就结束了。而当 Java 平台发现自己里面的线程都退出以后,也就会退出。这时 Java 程序就运行完毕了。 那么,线程是如何执行代码的呢?这个超出了本书的范围。就好像使用 CD 机一样, 使用者只要知道 CD 机中激光头是用来读取和处理 CD 碟片上的内容就可以了,没必要去 追究它是如何读取 CD 碟片上的内容的。对于线程也是一样的道理。前面说过,线程是操 作系统中的一个概念,所以“线程是如何执行程序的”这个问题属于操作系统课程中的内 容。对于学习 Java 而言,在这里先知道如下几点就可以了。 线程是执行 Java 程序代码的基本单位。 Java 线程也是 Java 平台的一部分。 Java 线程是运行在 Java 平台内部的,Java 平台负责管理 Java 线程。 Java 线程执行程序代码时,Java 平台为其提供各种所需的条件。 当线程执行完给它的方法后,就会退出。Java 平台中如果没有正在运行的线程, 就代表程序执行完毕,Java 平台也就自动退出了。 本节的内容就先到这里。本节中使用的“CD 机模型”比较容易理解,但是它和线程 还不是十分相似。在 15.1.2 节中将通过“演奏会模型”来加深对线程的理解。 15.1.2 演奏会模型 线程是隐藏在 Java 平台之中的,它的工作方式并没有展露在 Java 语法中。我们只能 够通过类比的方式来理解 Java 线程以及程序代码、Java 线程和 Java 平台三者之间的关系
第15章多线程编程 本节将要介绍的就是“演奏会模型”。演奏会并不是一个陌生的概念,可以把它看成是由 一个指挥家、一个或多个演奏家、乐谱组成的事物。它的最终结果就是演奏乐谱。我们的 这个演奏会模型与现实中的演奏会差不多,区别有以下几点。 口演奏会中使用的乐谱不是纸质的乐谱,而是用一个显示器显示乐谱。 口所有的乐谱都保存在一个存储设备上。所以在演奏会开始之前要先将乐谱输入到 存储设备中。 口每个演奏家使用一个单独的显示器来看乐谱,但是所有的显示器都从同一个存储 设备上读取乐谱。 口显示器每次只显示一小节乐谱内容,演奏家演奏完这个小节后,显示器会自动显 示出乐谱下一小节的内容,直到乐谱结束。 演奏会的工作模式也不同。 口首先将所有的乐谱输入到存储设备中。 口指挥家按照演奏的进度,每当需要演奏一个乐谱的时候,这个指挥家首先请上一 位演奏家,然后搬上一个显示器来显示需要演奏的乐谱。 口演奏家按照显示器上的内容进行演奏。当演奏家演奏完当前小节后,显示器自动 显示乐谱下一小节的内容。 口当乐谱结束后,演奏家就退场了。当所有的演奏家都退场以后,指挥家就退场, 演奏会就结束了。 1.Java线程和演奏会模型 下面用图15-4将Java程序和这个演奏会模型做一个类比。 Java源文件 乐谱源文件 Java的类文件 乐谱输入存储设备 后的数据 Java平台 指挥家 线程 演奏家 当前执行的程序 显示器当前显示的小节 图15-4Java线程和演奏会模型的类比 2.运行中的Java线程和演奏会模型 通过图15-4可以更清晰地看出线程、Java平台和程序代码之间的关系。一个演奏家专 心演奏摆在面前的乐谱,就好像Java线程一行行的执行代码。Java平台则是指挥家,管理 ·441▣
第 15 章 多线程编程 ·441· 本节将要介绍的就是“演奏会模型”。演奏会并不是一个陌生的概念,可以把它看成是由 一个指挥家、一个或多个演奏家、 乐谱组成的事物。它的最终结果就是演奏乐谱。我们的 这个演奏会模型与现实中的演奏会差不多,区别有以下几点。 演奏会中使用的乐谱不是纸质的乐谱,而是用一个显示器显示乐谱。 所有的乐谱都保存在一个存储设备上。所以在演奏会开始之前要先将乐谱输入到 存储设备中。 每个演奏家使用一个单独的显示器来看乐谱,但是所有的显示器都从同一个存储 设备上读取乐谱。 显示器每次只显示一小节乐谱内容,演奏家演奏完这个小节后,显示器会自动显 示出乐谱下一小节的内容,直到乐谱结束。 演奏会的工作模式也不同。 首先将所有的乐谱输入到存储设备中。 指挥家按照演奏的进度,每当需要演奏一个乐谱的时候,这个指挥家首先请上一 位演奏家,然后搬上一个显示器来显示需要演奏的乐谱。 演奏家按照显示器上的内容进行演奏。当演奏家演奏完当前小节后,显示器自动 显示乐谱下一小节的内容。 当乐谱结束后,演奏家就退场了。当所有的演奏家都退场以后,指挥家就退场, 演奏会就结束了。 1.Java线程和演奏会模型 下面用图 15-4 将 Java 程序和这个演奏会模型做一个类比。 图 15-4 Java 线程和演奏会模型的类比 2.运行中的Java线程和演奏会模型 通过图 15-4 可以更清晰地看出线程、Java 平台和程序代码之间的关系。一个演奏家专 心演奏摆在面前的乐谱,就好像 Java 线程一行行的执行代码。Java 平台则是指挥家,管理
第2篇Java语言高级语法 着整个程序,包括Java线程。Java平台为了让Java线程能够执行代码,做了很多的工作。 下面的图15-5将一个程序的执行过程和一个演奏会的执行过程做了一个类比。 将ava源文件编译灰lass文件 将乐谱输入存储设备 启ava平台 指挥家就绪 Java平台创建一个线程 指挥家请出一位演奏家 Java平台加载类文件,并找到 指挥家搬出一台显示器 其中的main(O方法 并让显示器显示需要演奏的乐谱 Java平台让这个线程从nainO方法执 指挥家让演奏家按照 行程序代码 显示器上的乐谱进行演奏 Java线程执行完上一行代码后,自 演奏家演奏完一小节以后,显示器 动执行下一行代码 显示下一小节,演奏家继续演奏 main(O方法结束,线程退出 乐谱演奏结束,演奏家退场 程序执行完毕,Java平台退出 演奏会演奏结束,指挥家退场 图15-5Java程序执行流程和演奏会流程 下面以一个简单的例程来说明图15-5中所示的流程。 package com.javaeasy.execution; //例程所在的包 public class Execution //例程名 public static void main(String[]args){ //main()方法,这就是程序执行 //的起点,也是线程执行的起点 int i 3+5; //第一行代码是一个运算操作 System.out.println(i); //第二行代码是一个方法调用 上面的例程很简单,这里不再解释。使用如下的javac命令对例程的源代码进行编译。 javac com\javaeasy\execution\Execution.java ·442·
第 2 篇 Java 语言高级语法 ·442· 着整个程序,包括 Java 线程。Java 平台为了让 Java 线程能够执行代码,做了很多的工作。 下面的图 15-5 将一个程序的执行过程和一个演奏会的执行过程做了一个类比。 图 15-5 Java 程序执行流程和演奏会流程 下面以一个简单的例程来说明图 15-5 中所示的流程。 package com.javaeasy.execution; // 例程所在的包 public class Execution { // 例程名 public static void main(String[] args) { // main()方法,这就是程序执行 // 的起点,也是线程执行的起点 int i = 3 + 5; // 第一行代码是一个运算操作 System.out.println(i); // 第二行代码是一个方法调用 } } 上面的例程很简单,这里不再解释。使用如下的 javac 命令对例程的源代码进行编译。 javac com\javaeasy\execution\Execution.java
第15章多线程编程 编译结束后,就会生成相应的类文件Execution.class。.Execution类是有main(方法的, 所以可以使用java命令来执行这个类文件。在使用如下命令执行Execution类的时候, java com.javaeasy.execution.Execution 首先Java平台会启动,启动完毕后,Java平台会创建一个线程。然后Java平台会读 取给它的参数,也就是com.javaeasy.execution.Execution。它是类的全限定名,Java平台会 根据这个名字来寻找需要执行的类,找到Execution.class文件后,就会把它读取并加载到 程序中,然后让开始创建出来的线程去执行Execution类中的mainO方法。 关于mian(0方法:main(O方法从语法上来说只是一个普通的方法,它的特殊之处在于 “Java平台会将mainO方法作为一个程序的开始,让线程从这里开始执行程序”。这也是为 什么所有可以直接被执行的Java程序都必须有这么一个mainO方法的原因。main(O方法是 一个约定俗成,无论Java平台以什么方法作为程序的开始都是没有关系的。 例程中的第1行是一个int变量的加法运算和it变量的赋值操作。线程在执行的时候 就会为变量分配内存并进行运算。例程中的第2行是一个方法调用。对于方法调用,线程 执行的时候会将这个方法“展开”(也就是进去被调用方法内部。在第14章讲解异常传递 时,就在方法调用的时候将方法展开了),这样一个方法调用就变成了执行方法中的代码 了。线程就会这样逐行代码地执行下去,直到main(方法结束。 口说明:线程始终都是在一行行地执行代码,遇到方法调用时,就进去被调用方法内部去 一行行执行。当然,我们在阅读程序的时候,可以简单地认为方法调用就是一个 运算。只有在必要的时候,我们才将一个方法调用“展开”。 通过上面的分析,线程这个概念己经了解的差不多了,后面将讲述线程编程的基础。 本节的内容是后面内容的基础。 口线程的作用就是从某个指定的方法(如main0方法)开始逐行执行代码。就好像 演奏家按照乐谱一小节一小节演奏一样。 口执行完指定的方法线程就结束了。 口通过演奏会模型加深对线程的理解。 15.2Java中的线程编程 前面对“线程是什么”做了大量的讲述和类比。本节中将讲述如何使用线程编程。通 过前面的内容我们知道,线程的作用就是“执行一个方法,执行完这个方法后,线程就结 束了”。在Java中我们可以自由地使用线程:创建线程,指定它需要执行的方法,然后启 动线程,这个线程就会执行下去了。请看本节的内容。 15.2.1线程类Thread 在Java语言中,线程被封装为Thread类(全限定名是java.lang.Thread)。当然,线 程核心的内容并不在这个类中,因为真正的线程是存在于Java平台中的。可以把这个类认 ·443·
第 15 章 多线程编程 ·443· 编译结束后,就会生成相应的类文件 Execution.class。Execution 类是有 main()方法的, 所以可以使用 java 命令来执行这个类文件。在使用如下命令执行 Execution 类的时候, java com.javaeasy.execution.Execution 首先 Java 平台会启动,启动完毕后,Java 平台会创建一个线程。然后 Java 平台会读 取给它的参数,也就是 com.javaeasy.execution.Execution。它是类的全限定名,Java 平台会 根据这个名字来寻找需要执行的类,找到 Execution.class 文件后,就会把它读取并加载到 程序中,然后让开始创建出来的线程去执行 Execution 类中的 main()方法。 关于 mian()方法:main()方法从语法上来说只是一个普通的方法,它的特殊之处在于 “Java 平台会将 main()方法作为一个程序的开始,让线程从这里开始执行程序”。这也是为 什么所有可以直接被执行的 Java 程序都必须有这么一个 main()方法的原因。main()方法是 一个约定俗成,无论 Java 平台以什么方法作为程序的开始都是没有关系的。 例程中的第 1 行是一个 int 变量的加法运算和 int 变量的赋值操作。线程在执行的时候 就会为变量分配内存并进行运算。例程中的第 2 行是一个方法调用。对于方法调用,线程 执行的时候会将这个方法“展开”(也就是进去被调用方法内部。在第 14 章讲解异常传递 时,就在方法调用的时候将方法展开了),这样一个方法调用就变成了执行方法中的代码 了。线程就会这样逐行代码地执行下去,直到 main()方法结束。 说明:线程始终都是在一行行地执行代码,遇到方法调用时,就进去被调用方法内部去 一行行执行。当然,我们在阅读程序的时候,可以简单地认为方法调用就是一个 运算。只有在必要的时候,我们才将一个方法调用“展开”。 通过上面的分析,线程这个概念已经了解的差不多了,后面将讲述线程编程的基础。 本节的内容是后面内容的基础。 线程的作用就是从某个指定的方法(如 main()方法)开始逐行执行代码。就好像 演奏家按照乐谱一小节一小节演奏一样。 执行完指定的方法线程就结束了。 通过演奏会模型加深对线程的理解。 15.2 Java 中的线程编程 前面对“线程是什么”做了大量的讲述和类比。本节中将讲述如何使用线程编程。通 过前面的内容我们知道,线程的作用就是“执行一个方法,执行完这个方法后,线程就结 束了”。在 Java 中我们可以自由地使用线程:创建线程,指定它需要执行的方法,然后启 动线程,这个线程就会执行下去了。请看本节的内容。 15.2.1 线程类 Thread 在 Java 语言中,线程被封装为 Thread 类(全限定名是 java.lang.Thread)。当然,线 程核心的内容并不在这个类中,因为真正的线程是存在于 Java 平台中的。可以把这个类认
第2篇Java语言高级语法 为是真正的线程的“代理人”。我们操作Thread类时,Thread类就会操作真正的线程,这 就好像“使用引用操作引用指向的对象”。 在程序中,可以按照需要创建和使用线程。当创建一个Thread类的实例时,在Java 平台内部,一个真正的线程同时被创建了出来。其实使用线程很简单,根据前面对线程的 介绍,使用线程的时候只需要关心下面两点就可以了。 口如何指定线程需要执行的方法。我们知道,线程的作用就是执行一个方法,直到 方法结束,线程也完成了使命。 口如何启动一个线程。当创建出了线程,也指定了线程需要执行的方法,下面的事 情就是“推动一下”,让线程启动起来。 下面就来讲述使用Java线程的第一种办法。 15.2.2覆盖Thread类的run0方法 为了使用线程,首先需要学习Thread类中的两个重要方法。 口Thread():这是Thread类的一个构造方法,它没有任何参数,所以说创建线程的实 例也是很简单的,可以不提供任何参数。 口void start():start()方法就是启动线程的方法,这个方法是线程类中最核心的方法。 当调用这个方法以后,它就会启动线程,并让线程去执行指定的方法,而这里说 的“指定的方法”,就是下面要说的run(O方法。 口void run():runO方法是Thread类中一个普通的方法,它的特殊之处仅仅在于start0( 方法会将它作为线程的起点。 好,知道了Thread类中这两个方法的作用后,如何让runO方法变成想让线程去执行 的方法呢?这里就要用到继承和覆盖了。我们使用一个类去继承Thread类,然后为这个 Thread类的子类添加一个runO方法,用来覆盖Thread类中原来的runO方法。那么,根据 Java覆盖的原则,start()方法再调runO方法的时候,其实就是调用的Thread类中子类的run) 方法了。也就是说,“只要在Thread类的子类中的run(O方法内部,编写需要让线程执行 的代码”就可以了。 下面以一个例子来演示这种使用线程的方法,首先创建一个Thread类的子类,并覆盖 Thread类中的runO方法。 package com.javaeasy.usethread; //程序所在的包 public class MyThread extends Thread /MyThread类继承自Thread类 public void run(){ /覆盖Thread类中的run()方法 System.out,println("这是在另一个线程中执行的代码。"): /向控制台输出一行字 //run()方法结束 上面的类很简单.首先需要注意的就是MyThread类继承自Thread类,然后是MyThread 类覆盖了Thread类中的runO0方法。这样才能够让线程在启动后(调用start()方法后)执行 到想让线程执行的内容。好,下面是一个使用MyThread类的例程。 ·444·
第 2 篇 Java 语言高级语法 ·444· 为是真正的线程的“代理人”。我们操作 Thread 类时,Thread 类就会操作真正的线程,这 就好像“使用引用操作引用指向的对象”。 在程序中,可以按照需要创建和使用线程。当创建一个 Thread 类的实例时,在 Java 平台内部,一个真正的线程同时被创建了出来。其实使用线程很简单,根据前面对线程的 介绍,使用线程的时候只需要关心下面两点就可以了。 如何指定线程需要执行的方法。我们知道,线程的作用就是执行一个方法,直到 方法结束,线程也完成了使命。 如何启动一个线程。当创建出了线程,也指定了线程需要执行的方法,下面的事 情就是“推动一下”,让线程启动起来。 下面就来讲述使用 Java 线程的第一种办法。 15.2.2 覆盖 Thread 类的 run()方法 为了使用线程,首先需要学习 Thread 类中的两个重要方法。 Thread():这是 Thread 类的一个构造方法,它没有任何参数,所以说创建线程的实 例也是很简单的,可以不提供任何参数。 void start():start()方法就是启动线程的方法,这个方法是线程类中最核心的方法。 当调用这个方法以后,它就会启动线程,并让线程去执行指定的方法,而这里说 的“指定的方法”,就是下面要说的 run()方法。 void run():run()方法是 Thread 类中一个普通的方法,它的特殊之处仅仅在于 start() 方法会将它作为线程的起点。 好,知道了 Thread 类中这两个方法的作用后,如何让 run()方法变成想让线程去执行 的方法呢?这里就要用到继承和覆盖了。我们使用一个类去继承 Thread 类,然后为这个 Thread 类的子类添加一个 run()方法,用来覆盖 Thread 类中原来的 run()方法。那么,根据 Java 覆盖的原则,start()方法再调 run()方法的时候,其实就是调用的 Thread 类中子类的 run() 方法了。也就是说,“只要在 Thread 类的子类中的 run()方法内部,编写需要让线程执行 的代码”就可以了。 下面以一个例子来演示这种使用线程的方法,首先创建一个 Thread 类的子类,并覆盖 Thread 类中的 run()方法。 package com.javaeasy.usethread; // 程序所在的包 public class MyThread extends Thread { // MyThread 类继承自 Thread 类 public void run() { // 覆盖 Thread 类中的 run()方法 System.out.println("这是在另一个线程中执行的代码。"); // 向控制台输出一行字 } // run()方法结束 } 上面的类很简单。首先需要注意的就是MyThread类继承自Thread类,然后是MyThread 类覆盖了 Thread 类中的 run()方法。这样才能够让线程在启动后(调用 start()方法后)执行 到想让线程执行的内容。好,下面是一个使用 MyThread 类的例程
第15章多线程编程 package com.javaeasy.usethread; /1程序所在的包 public class UseMyThread //例程类 public static void main(String[]args){ //main()方法 MyThread thread new MyThread(); //创建一个Thread类的实例 thread.start(); //启动一个新的线程 运行上面的例程,控制台输出如下内容: 这是在另一个线程中执行的代码。 到这里,线程似乎还没有带来什么让人兴奋的特征。不过不着急,下面首先在图15-6 中使用“演奏家模型”说明一下上面例程的执行过程(省略关于编译等无关的步骤)。 Java平台创建一个线程(线程I) 指挥家请出一位演奏家(演奏家1) Java平台让加JseMyThread类 指挥家搬出一台显示器 并找到其中的mainO方法 并让显示器显示需要演奏的乐谱 Java平台让线程1执行 指挥家让演奏家按照 UseMyThread的main()方法 显示器上的乐谱进行演奏 mainO方法第一行,创建一个 演奏家发现乐谱上需要另一位演奏 家,于是让指挥家请出另一位演奏 新的线程的实例(线程2) 家(演奏家2)和新的显示器 main(O方法第二行 Java平台启动线程2 演奏家1告诉指挥家可以 演奏家2看到指挥家做出 启动这个线程 让演奏家2开始演奏了 手势,开始演奏乐谱 执行main()方法的 演奏家1的乐谱结束, 线程结束 线程2执行rmO方法第一 演奏家1退场 演奏家2演奏其 行,向控制台输出相关内容 显示器上的乐谱 run0方法结東,线程2也结束 演奏家2的乐谱结束 演奏家2退场 所有线程都结束了,Java平台退出 演奏会演奏结束指挥家退场 图15-6创建并启动线程的程序执行过程 图15-6可以看出如下几点内容。 ·445·
第 15 章 多线程编程 ·445· package com.javaeasy.usethread; // 程序所在的包 public class UseMyThread { // 例程类 public static void main(String[] args) { // main()方法 MyThread thread = new MyThread(); // 创建一个 Thread 类的实例 thread.start(); // 启动一个新的线程 } } 运行上面的例程,控制台输出如下内容: 这是在另一个线程中执行的代码。 到这里,线程似乎还没有带来什么让人兴奋的特征。不过不着急,下面首先在图 15-6 中使用“演奏家模型”说明一下上面例程的执行过程(省略关于编译等无关的步骤)。 图 15-6 创建并启动线程的程序执行过程 图 15-6 可以看出如下几点内容
第2篇Java语言高级语法 口新线程的创建和启动其实都是通过Java代码触发的。实际上,除了第一个线程(也 就是启动程序的、运行mainO方法的线程)是由Java平台直接创建的之外,其余 的线程都是在Java代码中通过“创建Thread类的实例,启动线程”这种方式创建 并启动的。 口当启动一个新的线程时,其过程是:由Java代码通知Java平台,Java平台再启动 线程。例如在图15-6中,线程1启动线程2的过程实际上就是:线程1执行 thread.start(),这个方法在内部会让Java平台启动第二个线程。所以,启动线程2 对于线程1来说,是一个很短的过程,因为启动线程的具体事情都是Java平台做 的,线程1只是“通知”Java平台要启动线程2而已,通知完了就继续执行代码, 不等待线程2。 口只有所有的线程都退出以后,程序才会退出。 如果这个过程看上去太抽象,请对比着右边演奏会的过程进行理解。下面学习另一种 使用Java线程的方式。 口Thread类的start)(方法是用来启动一个线程的。 口Thread类的runO方法是一个线程启动后执行的方法。 15.2.3使用Runnable接口 我们知道,Java中的类只能够是单继承,也就是说,如果一个类为了使用线程而继承 了Thread类,它就不能再继承别的类了。这很可能给编程带来不便。本节中介绍的就是一 种脱离继承来使用线程的方法。这个方法的核心就是Runnable接口。 Runnable接口的全限定名是java.lang.Runnable。它其中只有一个抽象方法void run(O。 为了了解如何在线程中使用Runnable接口,我们还需要看一下Thread类中的一个叫做 target的属性和Thread类中的run(O方法。Thread类中有一个类型为Runnable的属性,叫 做target。而Thread类的runO方法用到了这个属性,runO方法的代码如下: public void run(){ /Thread类的run()方法 if(target!-null){/检查target属性是否为空,target属性是Runnable /1类型的引用 target.run(); /如果不为空则执行run()方法 //否则什么都不做 /run()方法结束 如何让target的值不为null呢?Thread类的另一个构造方法就是用来给target属性赋 值的,这个构造方法是Thread(Runnable)。当调用这个构造方法时,传递过来的参数就 会赋值给target属性。也就是说,如果直接使用Thread类也是可以的,步骤如下: (I)实现Runnable接口,例如叫做MyRunnable,并在MyRunnable类的runO方法里 编写想要让线程做的事情。 (2)创建一个MyRunnable的实例。 (3)通过构造方法Thread(Runnable)来创建Thread类的实例。 这时再调用start()方法启动这个线程,执行的就是MyRunnable中runO方法的代码了。 下面我们来使用以下这种方法,首先是MyRunnable类。 ·446·
第 2 篇 Java 语言高级语法 ·446· 新线程的创建和启动其实都是通过 Java 代码触发的。实际上,除了第一个线程(也 就是启动程序的、运行 main()方法的线程)是由 Java 平台直接创建的之外,其余 的线程都是在 Java 代码中通过“创建 Thread 类的实例,启动线程”这种方式创建 并启动的。 当启动一个新的线程时,其过程是:由 Java 代码通知 Java 平台,Java 平台再启动 线程。例如在图 15-6 中,线程 1 启动线程 2 的过程实际上就是:线程 1 执行 thread.start(),这个方法在内部会让 Java 平台启动第二个线程。所以,启动线程 2 对于线程 1 来说,是一个很短的过程,因为启动线程的具体事情都是 Java 平台做 的,线程 1 只是“通知”Java 平台要启动线程 2 而已,通知完了就继续执行代码, 不等待线程 2。 只有所有的线程都退出以后,程序才会退出。 如果这个过程看上去太抽象,请对比着右边演奏会的过程进行理解。下面学习另一种 使用 Java 线程的方式。 Thread 类的 start()方法是用来启动一个线程的。 Thread 类的 run()方法是一个线程启动后执行的方法。 15.2.3 使用 Runnable 接口 我们知道,Java 中的类只能够是单继承,也就是说,如果一个类为了使用线程而继承 了 Thread 类,它就不能再继承别的类了。这很可能给编程带来不便。本节中介绍的就是一 种脱离继承来使用线程的方法。这个方法的核心就是 Runnable 接口。 Runnable 接口的全限定名是 java.lang.Runnable。它其中只有一个抽象方法 void run()。 为了了解如何在线程中使用 Runnable 接口,我们还需要看一下 Thread 类中的一个叫做 target 的属性和 Thread 类中的 run()方法。Thread 类中有一个类型为 Runnable 的属性,叫 做 target。而 Thread 类的 run()方法用到了这个属性,run()方法的代码如下: public void run() { // Thread 类的 run()方法 if (target != null) { // 检查 target 属性是否为空,target 属性是 Runnable // 类型的引用 target.run(); // 如果不为空则执行 run()方法 } // 否则什么都不做 } // run()方法结束 如何让 target 的值不为 null 呢?Thread 类的另一个构造方法就是用来给 target 属性赋 值的,这个构造方法是 Thread(Runnable)。当调用这个构造方法时,传递过来的参数就 会赋值给 target 属性。也就是说,如果直接使用 Thread 类也是可以的,步骤如下: (1)实现 Runnable 接口,例如叫做 MyRunnable,并在 MyRunnable 类的 run()方法里 编写想要让线程做的事情。 (2)创建一个 MyRunnable 的实例。 (3)通过构造方法 Thread(Runnable)来创建 Thread 类的实例。 这时再调用 start()方法启动这个线程,执行的就是 MyRunnable 中 run()方法的代码了。 下面我们来使用以下这种方法,首先是 MyRunnable 类
第15章多线程编程 package com.javaeasy.usethread; /1程序在的包 public class MyRunnable implements Runnable //实现Runnable接口 public void run(){ /实现run()方法 System.out,println("这是在另一个线程中执行的代码。"); //向控制台输出一行字 //run()方法结束 MyRunnable实现了Runnable接口,其runO方法就是线程会去执行的方法。然后是 例程。 package com.javaeasy.usethread; public class UseMyRunnable public static void main(String[]args){ //例程的nain()方法 /创建一个MyRunnable类的实例,MyRunnable,MyRunnable实现了Runnable /接口 MyRunnable runnable new MyRunnable(); Thread thread new Thread(runnable); //调用Thread相应的构造 //方法,传入参数 thread.start(); //启动线程 在例程中,按照步骤分别创建MyRunnable类的实例,调用Thread相应的构造方法, 最后启动线程。因为Runnable是个接口,为了简单一些,还可以使用前面学到过的匿名类 来实现相同的功能。使用匿名类的例程如下: package com.javaeasy.usethread; public class UseRunnable public static void main(String[]args)f/测试类的main()方法 //创建一个线程,参数为一个实现了Runnable接口的匿名类的实例 Thread thread new Thread(new Runnable()( public void run()( //实现抽象方法run() System.out.println("这是在另一个线程中执行的代码。"); 1); thread.start(); /1启动线程 例程UseRunnable其实和例程UseMyRunnable是一样的。当然,从本质上讲,无论是 使用继承还是使用Runnable接口,其目的都是一样的。让线程执行我们写的一段代码。使 用继承并覆盖runO方法也好;使用Runnable接口也好,都是为了指定线程执行的方法。 本节不再给出程序执行的实例图,图15-6可以涵盖本节的程序流程。 下面我们回过头去看看图15-6:当一个新的线程启动以后,程序就相当于是有两个同 时在执行的线程。没错,事情就是这样的。就好像演奏会上的两个演奏家一样,两个演奏 家是一起演奏各自的乐谱。两个线程也是各自执行自己的代码,彼此之间互不影响。但是 事情到这里就开始变得有意思了:一个程序内有两个线程。好,下面让我们进入15.2.4节, 看看两个线程的故事。 口使用Runnable接口来让线程执行自己编写的runO方法。 ·447·
第 15 章 多线程编程 ·447· package com.javaeasy.usethread; // 程序在的包 public class MyRunnable implements Runnable { // 实现 Runnable 接口 public void run() { // 实现 run()方法 System.out.println("这是在另一个线程中执行的代码。"); // 向控制台输出一行字 } // run()方法结束 } MyRunnable 实现了 Runnable 接口,其 run()方法就是线程会去执行的方法。然后是 例程。 package com.javaeasy.usethread; public class UseMyRunnable { public static void main(String[] args) { // 例程的 main()方法 // 创建一个 MyRunnable 类的实例,MyRunnable, MyRunnable 实现了 Runnable // 接口 MyRunnable runnable = new MyRunnable(); Thread thread = new Thread(runnable); // 调用 Thread 相应的构造 // 方法,传入参数 thread.start(); // 启动线程 } } 在例程中,按照步骤分别创建 MyRunnable 类的实例,调用 Thread 相应的构造方法, 最后启动线程。因为 Runnable 是个接口,为了简单一些,还可以使用前面学到过的匿名类 来实现相同的功能。使用匿名类的例程如下: package com.javaeasy.usethread; public class UseRunnable { public static void main(String[] args) { // 测试类的 main()方法 // 创建一个线程,参数为一个实现了 Runnable 接口的匿名类的实例 Thread thread = new Thread(new Runnable() { public void run() { // 实现抽象方法 run() System.out.println("这是在另一个线程中执行的代码。"); } }); thread.start(); // 启动线程 } } 例程 UseRunnable 其实和例程 UseMyRunnable 是一样的。当然,从本质上讲,无论是 使用继承还是使用 Runnable 接口,其目的都是一样的。让线程执行我们写的一段代码。使 用继承并覆盖 run()方法也好;使用 Runnable 接口也好,都是为了指定线程执行的方法。 本节不再给出程序执行的实例图,图 15-6 可以涵盖本节的程序流程。 下面我们回过头去看看图 15-6:当一个新的线程启动以后,程序就相当于是有两个同 时在执行的线程。没错,事情就是这样的。就好像演奏会上的两个演奏家一样,两个演奏 家是一起演奏各自的乐谱。两个线程也是各自执行自己的代码,彼此之间互不影响。但是 事情到这里就开始变得有意思了:一个程序内有两个线程。好,下面让我们进入 15.2.4 节, 看看两个线程的故事。 使用 Runnable 接口来让线程执行自己编写的 run()方法