1、单元 19 多线程单元目标 掌握线程的概念以及线程与进程的区别; 理解线程的状态和生命周期; 掌握多线程的实现方法; 通过继承 Thread 类或实现 Runnable 接口实现多线程; 掌握多线程互斥关系的产生原因; 掌握使用同步技术解决互斥的实现方法;学习任务 011.任务描述运用 Java 多线程技术编写一个简单的动画,要求运行程序时窗口会显示一个飘动的字幕,每隔 1 秒字幕会自动改变显示的位置,先自左向右移动,到达窗口右边界时,再改变为自右向左移动。2.运行结果知识准备人们在日常生活中做多项任务时通常有两种处理方式。 人们可以在同一时刻只进行一项任务,等完成后再开始另一个任务,不同任务
2、在时间上有严格的先后顺序,称之为串行(顺序)处理方式。人们也可以在同一时刻处理多个任务,不同任务在时间上没有严格的先后顺序, 称之为并行处理方式。 例如, 一边听音乐,飘动字幕动画程序设计飘动字幕动画程序设计一边打扫房间; 使用计算机可以一边浏览网页, 一边打印文档, 一边压缩文件等。计算机用来模拟和解决人们现实生活中问题, 因此使用编程语言描述现实世界同样需要串、 并行共存。 计算机中的并行处理即同时处理多个任务, 一般叫 “多任务” 。 多任务处理方式的优点是充分利用 CPU 资源, 提高效率。 含有多个 CPU的计算机可将不同任务分配到不同 CPU 实现并行处理;单 CPU 则靠快速切换
3、任务来模拟并行处理,使系统的空转时间最少。图 5-10 单 CPU 与多 CPU 多任务实现方式示意图19.1 线程与进程的概念和关系19.1.1 线程与进程的概念在编写线程程序之前需要先了解几个相关概念。程序程序(program)是为实现特定目标或解决特定问题而用计算机语言编写的命令序列的集合。进程进程(Process)是程序关于某个数据集合上的一次运行活动,对应了从代码加载、执行至执行完毕的一个完整过程,是系统进行资源分配和调度的一个独立单位。线程线程(Thread)是进程的一个实体,CPU 调度和分派的基本单位,是比进程更小的能独立运行的基本单位。19.1.2 线程与进程的关系一个线程只
4、能属于一个进程,而一个进程可以有多个线程,但至少有一个一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程线程。 操作系统把资源分配给进程,而同一进程的所有线程共享该进程的所有资源。由于多个线程共享同一资源集,所以线程在执行过程中需要协作同步。线程是指进程内的一个执行单元,也是进程内的可调度实体。进程和线程的关系可以比喻成:当打开一个 Word(Office 中的 Word)程序,编写一个工作计划.doc文件就执行了一个程序的一个进程,而当执行这个文件的打印工作时就调用了Word 中的一个线程。线程与进程虽然有密切的关系,但也要清楚分清他们的关系。线程线程是是作作为为CPU 调
5、度和分配的基本单位,调度和分配的基本单位,而而进程进程是是拥有资源的基本单位。拥有资源的基本单位。进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。在创建或撤销进程的时候,由于系统都要为之分配和回收资源导致系统开销增加,而线程的切换则不需要很多的系统开销。线程只有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮。图 5-11 进程与线程关系示意图19.2 线程的生命周期在每一个 Java 的 Application(应用)程序中只存在一个默认的主线程,即每个程序只有一条执行线路, 这个默认的主线
6、程就是 main()方法的执行顺序。 若想自己定义线程就必须要在主线程中使用 Thread 相关类进行定义。每一个线程都要经历一个从出现到死亡的过程,我们把这个过程称之为生命周期。线程的生命周期包括 4 种状态:New(新生新生) 、Runnable(可运行可运行) 、Blocked(被阻塞被阻塞)和和 Dead(死亡(死亡) 。图 5-12 生命周期运行图19.2.1 新生状态当使用 new 操作符创建一个新的线程时,线程并不是马上进行运行,此时线程处在新生(new)状态。当一个线程处于新生状态时,程序还没有开始运行线程中的代码。19.2.2 可运行状态当处于新生状态的线程调用了 start
7、 方法后,该线程就成为了可运行(Runnable)了。一个可运行线程可能是一个正在运行的状态,也可能没有,这取决于操作系统为该线程提供的运行时间。不过 Java 规范中并没有把运行单独作为一个状态,也就是说一个正在运行的线程仍然是处于可运行状态的。一旦线程开始运行,它不一定始终保持运行,线程可能在运行过程中被中断。线程的调度取决于操作系统所提供的服务。 例如使用抢占式调度的系统给每个可以运行的线程一个时间片段来处理任务。当时间片用完后,操作系统会剥夺该线程对资源的占用,在考虑线程优先级的情况下选择下一个线程进行处理。19.2.3 被阻塞状态当线程在可运行状态下执行了睡眠(sleep) 、阻塞
8、I/O 操作、等待(wait)等操作后,线程就进入了被阻塞状态,另一个线程就可以被调度运行了。当一个被阻塞的线程重新被激活是,调度器会检查它的优先级是否高于当前的运行线程,如果是,他就会抢占当前线程的资源并开始运行。一个被阻塞线程只能通过和先前阻塞它的相同过程重新进入可运行状态。19.2.4 死亡状态线程在可运行状态下经过一个正常的 run 方法结束完成自然死亡。 再有就是可以使用 stop 方法来杀死一个线程,同时抛出一个 ThreadDeath 错误对象。19.3 线程的创建19.3.1 Thread 类的简介Thread 类是 Java 提供用来的创建线程的核心类,存在于 java.la
9、ng 包中,它综合了一个线程所需的属性和方法,可以使用该类进行线程的创建、线程的常用操作以及设置线程优先级等。下面介绍常用的方法。 public Thread():创建一个新的线程对象。 public Thread(String name):创建一个名字为 name 的新线程对象。 public Thread(Runnabletarget,String name):在现有的 target 对象基础上创建一个名字为 name 的线程对象,新的对象实际上是把 target 作为了运行对象。 public static void sleep(long mills):使正在运行的线程休眠 mills
10、好秒后再运行。 public final getPriority():获得线程的优先级。 public final setPriority(int priority):设置线程的优先级。 public void start():启动线程。如果能获得 CUP 的使用权就会自动执行调用 run()方法。 public void run():这是:这是 Thread 线程类中最重要的方法,是线程执行线程类中最重要的方法,是线程执行的起点,线程具体的操作都要编写在此方法中。的起点,线程具体的操作都要编写在此方法中。 public final boolean isAlive():判断线程是否在活动,如果
11、是返回 true,否则返回 false。19.3.2 Runnable 接口Java 不支持多继承, 一旦一个类继承了 Thread 类, 就不能再继承其他的类。若想让其他的类支持多线程,那么还可以让这个类实现 Runnable 接口。Runnable 接口位于 java.lang 包中,只提供了一个 run()方法,该方法与 Thread类中的 run()方法作用一样,线程执行的具体操作都要写在此方法中。19.3.3 使用 Thread 类创建线程若要创建一个具有线程功能的类则需要通过继承 Thread 类来实现多线程。首先设计 Thread 的子类,然后根据工作需要重新设计线程中的 run
12、 方法(方法的重写) ,再使用 start 方法启动线程,将执行权转交给 run()方法。【实例 5-9】下面示例演示使用 Thread 类来创建线程并启动线程,然后要求在线程中进行每隔 1 秒钟打印一行数据。public class ThreadEx extends Thread /继承 Thread 类public ThreadEx(String name)/带名字的构造方法super(name);/重写 run()方法,编写代码public void run()System.out.println(this.getName()+”打印信息”);tryThread.sleep(1000);
13、/让线程休眠 1 秒钟,并抛出异常catch(Exception ex)ex.printStackTrace();public class Main public static void main(String args)/创建线程对象ThreadEx thread1=new ThreadEx(“线程 1”);ThreadEx thread2=new ThreadEx(“线程 2”);/启动线程thread1.start();thread2.start();程序运行结果如下:线程 1打印信息线程 1打印信息线程 2打印信息线程 1打印信息线程 2打印信息由于线程没有设置优先级,获得 CPU 的
14、使用权的机会是平等的,所以上面的运行结果两个线程时有先有后。飘动字幕动画程序设计【注意】这个程序的运行结果在不同机器上结果是不一样的,到底什么时候使用线程 1还是线程 2 是由操作系统的线程处理机制控制的。并且这个运行不会结束,只有强行关掉应用程序线程才会关闭。因此注意在写 run()方法时一定要在里面加入结束判断语句,可以是有限次的循环,也可以是对某个界限的判断。线程可以通过 setPriority(int priority)方法来设置线程的优先级,优先级的改变是可以影响 CPU 调用线程的顺序。 优先级是一个 1-10 的数, 默认不设置为 5,Thread 类中包含了 3 个静态常量:
15、public static final int NORM_PRIORITY=5 public static final int MIN_PRIORITY=1 public static final int MAX_PRIORITY=10【实例 5-10】根据上例加入线程优先级设置,要求线程 1 的优先级最高,线程 2 的优先级最低。public class ThreadEx extends Thread /继承 Thread 类public ThreadEx(String name,int priority)/带名字的构造方法super(name);this.setPriority(prior
16、ity);public void run()/与上例一样public class Main public static void main(String args)/创建线程对象,并设置优先级ThreadEx thread1=new ThreadEx(“线程 1”,Thread.MAX_PRIORITY);ThreadEx thread2=new ThreadEx(“线程 2”, Thread.MIN_PRIORITY);/启动线程thread1.start();thread2.start();程序运行结果如下:线程 1打印信息飘动字幕动画程序设计线程 2打印信息线程 1打印信息线程 2打印信
17、息线程 1打印信息线程 2打印信息由于线程 1 的优先级大于线程 2 的优先级,所以每次线程 1 运行完毕后,线程 2 才可以获得 CPU 的使用权去运行。5.10.4 使用 Runnable 接口创建线程Java 语言中创建线程除了使用 Thread 直接创建外,还可以使用 Runnable接口来完成线程的创建。 首先需要实现 Runnable 接口; 然后实现接口中的 run()方法;紧接着创建一个线程对象,并将对象作为参数传递给 Thread 类的构造方法,从而生成一个 Thread 类;最后调用 start()方法启动线程。【实例 5-11】下面示例演示使用 Runnable 接口来创
18、建线程并启动线程,然后要求在线程中进行没隔 1 秒钟打印一行数据。public class RunnableEx implements Runnable /实现 Runnable 接口public String name;public RunnableEx (String name)/带名字的构造方法this.name=name;/重写 run()方法,编写代码public void run()System.out.println(name+”打印信息”);tryThread.sleep(1000);/让线程休眠 1 秒钟,并抛出异常catch(Exception ex)ex.printSta
19、ckTrace();public class Main public static void main(String args)/创建线程目标对象RunnableEx re1=new RunnableEx (“线程 1”);RunnableEx re2=new RunnableEx (“线程 2”);飘动字幕动画程序设计/创建线程对象Thread t1=new Thread(re1);Thread t2=new Thread(re2);/启动线程t1.start();t2.start();任务实施1. 实现思路在窗口中,通过 JLabel 显示一行文字,通过启动一个线程,在线程中每隔一秒改变一
20、次 JLabel 对象的位置可实现字幕飘动的动画效果。(1)创建一个主类 MovingText 继承 JFrame 实现 Runnable 接口;(2)在主类 MovingText 构造方法中创建 JLabel 对象,创建线程对象并启动线程;(3)实现 Runnable 的 run 方法,用 sleep 方法休眠一秒,修改 JLabel对象位置;(4)在 main 方法中实例化 MovingText。2. 程序代码import java.awt.*;import javax.swing.JFrame;public class MovingText extends JFrame implemen
21、ts Runnable Label m_label;int i = 0;boolean bRight = true;public MovingText() Container con = getContentPane();con.setLayout(null);m_label = new Label(多线程可实现动画效果);m_label.setBounds(10, 100, 150, 50);con.add(m_label, Center);/ 设置窗体的标题、大小、可见性及关闭动作setTitle(飘动的字幕);setSize(340, 260);setVisible(true);飘动字幕
22、动画程序设计setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);/ 创建和启动线程Thread td = new Thread(this);td.start();public static void main(String args) MovingText fr = new MovingText();public void run() try Thread t = Thread.currentThread();System.out.println(当前线程是: + t);while (true) Thread.sleep(1000);m_label.s
23、etBounds(10 + i * 10, 100, 150, 50);if (i 20)bRight = false;if (i 0)bRight = true;if (bRight)i+;elsei-; catch (Exception e) 任务拓展创建线程有两种办法,那么是选择继承 Thread 还是实现 Runnable 接口?其实 Thread 也是实现 Runnable 接口的:class Thread implements Runnable /public void run() if (target != null) target.run();其实 Thread 中的 run
24、方法调用的是 Runnable 接口的 run 方法。如果一个类继承 Thread,则不适合资源共享。但是如果实现了 Runable 接口的话,则很容易的实现资源共享。实现 Runnable 接口比继承 Thread 类所具有的优势:1)适合多个相同的程序代码的线程去处理同一个资源;2)可以避免 java 中的单继承的限制;3)增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。所以,建议大家尽量实现接口。main 方法其实也是一个线程。在 java 中所有的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到 CPU 的资源。在 java 中,每次程序运行至少启动 2 个线程。
25、一个是 main 线程,一个是垃圾收集线程。因为每当使用 java 命令执行一个类的时候,实际上都会启动一个JVM,每一个 JVM 实际上就是在操作系统中启动了一个进程。主线程也有可能在子线程结束之前结束,并且子线程不受影响,不会因为主线程的结束而结束。在 java 程序中,只要前台有一个线程在运行,整个 java 程序进程就不会消失,所以此时可以设置一个后台线程,这样即使 java 进程消失了,此后台线程依然能够继续运行。【实例 5-12】下面示例演示使用 setDaemon()方法来设置守护线程,从而满足主线程结束后此守护线程也不会结束。public class DaemonTest im
26、plements Runnable / 后台线程int i = 0;public void run() while (true) System.out.println(Thread.currentThread().getName() + 在运行: + i+);public static void main(String args) DaemonTest he = new DaemonTest();Thread demo = new Thread(he, 线程);demo.setDaemon(true);demo.start();System.out.println(主线程结束);运行上面程序,
27、你会发现主线程结束后,后台线程还会运行一段时间。飘动字幕动画程序设计setDaemon(boolean on)方法可将线程标记为守护线程或后台线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。任务实训1.实训目的 掌握创建线程的方法; 掌握启动线程的方法。2.实训内容运用 Java 多线程技术,通过实现 Runnable 接口来编写一个电子时钟的应用程 RunnableClock,运行程序时会显示系统的当前日期和时间,并且每隔 1 秒后会自动刷新显示当前日期和时间。学习任务 021.任务描述学生成绩读写模拟。程序中有两个线程,一个负责写学生成绩数据,一个负
28、责读取和显示学生成绩数据。为了清楚,一个学生有 20 门课的成绩,写线程写入的每门课的成绩都和其学号相同。如果读线程发现成绩和学号不一致的情况,则说明出现了共享数据读写不一致的问题, 利用线程同步机制解决共享数据读写不一致的问题。2运行结果知识准备上一个任务所提到的线程都是独立执行的, 也就是说每一个线程都包含自己运行时所需要的资源,而不需要操作外部的资源,这样也就不会去关心其他线程对资源状态的改变。Java 规范中是允许多个线程之间共享数据的,当线程以异步方式访问共享数据时,不安全或是不可预知的问题将会出现。飘动字幕动画程序设计学生成绩读写程序设计19.4 多线程的共享互斥由于线程是共享进程
29、资源,因此会出现多线程在同时操作同一资源,其中一个线程对资源的操作可能会改变资源状态, 而该状态的改变又会影响另一个线程对该对象的操作结果。例如,在不同的窗口购买火车票,现在只剩一张火车票的情况下,两个窗口同时进行了卖火车票操作,都会激发一个线程完成卖火车票操作, 结果有可能是一个座位卖出了 2 张相同的票。 需要被同一进程的不同线程访问的数据称为线程共享数据。 像这种在某一时刻只有一个线程可以操作某个资源的机制就叫做共享互斥。【实例 5-13】模拟父母在一个盘子中放入苹果,孩子在盘子中拿出苹果,演示多线程的互斥关系。/省略包的导入和创建/定义一个盘子类,里面放有苹果变量public clas
30、s Plateprivate int apple;public int getApple()return apple;public void putApple(int apple)this.apple=apple;/定义孩子线程,从盘子中拿苹果public class Child extends Threadprivate Plate plate;public Child(Plate plate)this.plate=plate;public void run()int value=0;for(int i=1;i6;i+)value=plate.getApple();System.out.pr
31、intln(孩子从盘子里拿:第+value+个苹果);try学生成绩读写程序设计sleep(int)(Math.random()*100);catch(Exception ex)ex.printStackTrace();/定义父母线程,向盘子里放苹果public class Parents entends Threadprivate Plate plate;public Parents(Plate plate)this.plate=plate;public void run()for(int i=1;i6;i+)plate.putApple(i);System.out.println(父母向盘
32、子里放:第+i+个苹果);trysleep(int)(Math.random()*100);catch(Exception ex)ex.printStackTrace();/定义测试类,完成线程创建和运行public class Mainpublic static void main(String args)Plate p=new Plate();Parents parents=new Parents();Child child=new Child();parents.start();child.start();程序运行结果如下:父母向盘子里放:第 1 个苹果孩子从盘子里拿:第 1 个苹果孩子
33、从盘子里拿:第 1 个苹果父母向盘子里放:第 2 个苹果孩子从盘子里拿:第 2 个苹果父母向盘子里放:第 3 个苹果孩子从盘子里拿:第 4 个苹果在不同的计算机上运行该程序, 结果有可能不同。 但通过运行结果可以看出,父母刚刚放入第三个苹果,孩子则已经去拿第四了,这显然是不合理的。因此上面两个线程就存在了互斥关系,任何一个线程对数据的操作都影响程序的结果。19.5 使用线程同步解决共享互斥对于互斥现象的出现,Java 中提供了同步的控制机制来达到当多个线程需要共享资源时,能够确定资源在某一时刻只能被一个线程占用。同步的方法使得第一个线程处理数据时,第二个线程不能访问数据;或当第一个线程处理完数
34、据后, 第二个线程才能访问数据。因此线程同步是多线程技术中的一个相当重要的部分。在讲解同步技术前,应当先来理解一下 Java 中锁的概念。Java 使用synchronized 关键字来标记对象的“互斥锁” ,从而保证在任何时刻只能有一个线程访问该对象。同时,Java 还提供了 wait()、notify()和 notifyAll()控制方法: public final void wait():使用当前线程主动释放互斥锁,并进入该互斥锁的等待队列。 public final void notify():唤醒 wait 队列中的第一个线程,并将该线程移入互斥锁申请队列中。 public fina
35、lvoid notifyAll():唤醒 wait 队列中的所有线程,并将线程移入互斥锁申请队列。【实例 5-14】使用同步技术改进上面例子,解决拿苹果和放苹果过程中存在的互斥关系。/ 省略包的导入和创建/定义一个盘子类,里面放有苹果变量,对取苹果和放苹果方法进行同步设置public class Plateprivate int apple;private boolean available=false;public synchronized int getApple()while(this.available=false)trywait();catch(Exception ex)ex.pri
36、ntStackTrace();this.available=false;notifyAll();return apple;public synchronized void putApple(int apple)while(this.available=true)trywait();catch(Exception ex)ex.printStackTrace();this.apple=apple;this.available=true;notifyAll();/其他的三个类没有变化,请参照前面例子程序运行结果如下:父母向盘子里放:第 1 个苹果孩子从盘子里拿:第 1 个苹果父母向盘子里放:第 2
37、个苹果孩子从盘子里拿:第 2 个苹果学生成绩读写程序设计父母向盘子里放:第 3 个苹果孩子从盘子里拿:第 3 个苹果通过采用 synchronized 关键字和 wait()、notifyAll()方法可以使多线程之间实现资源同步,从而保证了数据的一致性和正确性。同时 synchronized 关键字不仅可以修饰方法还可以修饰代码块。【注意】程序如果有多个线程,线程执行的顺序是不可预测的,不要对此作出假设;多线程共享数据需要同步保护;程序退出时,要通知并妥善退出所有的线程;任务实施1. 实施思路同步块和同步方法都可以解决共享数据保护的问题。如果代码都是自己写的,尽可能使用同步方法。如果调用别人
38、写好的、自己无法修改的非同步方法,就只能将调用语句放在同步块中了。(1)定义 StudentScore 类,通过增加两个同步方法 readScore 和writeScore 实现数据封装;(2)定义 WriteScore 和 ReadScore 线程类代码,通过调用同步方法readScore 和 writeScore 实现数据读写;(3)运行程序测试解决互斥的有效性。2. 程序代码package unit5.thread.proect2;/ 学生成绩class StudentScorepublic static int MAX_NUM = 3;/ 最大学号,退出条件private int sc
39、ore = new int20;/ 成绩private int no = 0;/ 学号public synchronized int getNo()return no;public synchronized void writeScore(int counter)/ 获得 score 的监视器no = counter;学生成绩读写程序设计for(int i=0;iscore.length;i+)tryThread.sleep(20);catch(Exception e)System.out.println(Write score + e);scorei = counter;public syn
40、chronized void readScore()/ 获得 score 的监视器int i = 0;for(i=0;i score.MAX_NUM)break;System.out.println(Write Thread finished!);public class ReadScore extends ThreadStudentScore score;ReadScore(StudentScore score)this.score = score;public void run()while(true)score.readScore();trysleep(60);catch(Excepti
41、on e)System.out.println(Read score + e);if(score.getNo() = score.MAX_NUM)break;System.out.println(Read Thread finished!);public class ConfusionDemo public static void main(String args) StudentScore s = new StudentScore();WriteScore w = new WriteScore(s);w.setPriority(Thread.NORM_PRIORITY - 1);ReadSc
42、ore r = new ReadScore(s);r.start();w.start();System.out.println(Main Thread finished!);任务拓展当多个线程竞争共享资源时,可以通过同步来保证资源的独占,但没有考虑一个线程已经占有某些资源后又要申请其他资源的情况。 当一个线程需要一个资源二另一个线程持有该资源不释放时,就会发生死锁现象死锁现象。例如,主线程一直在执行拿起筷子的操作,也就一直占有筷子资源,而 Eat 线程若想拿起筷子则永远完成不了,会无限等待下去,造成死锁。如图 5-14 所示:图 5-14 死锁原理示意图死锁现象是很难通过调试和测试发现,通常情况下它很难发生,只有当多个线程恰好偶然产生死锁。因此 Java 技术既不能发现死锁也不能避免死锁。预防和打破死锁现象要从死锁产生条件入手。需要特别注意:线程因某个条件没有满足而一直占有资源;多个线程需要互斥访问,线程要获得加锁顺序,并保证程序以相反的顺序释放锁。Java 从 JDK5.0 版本开始就引入了 ReentrantLock 类来实现锁的功能(详细请见 JDK5.0 文档) 。任务实训1.实训目的 掌握线程的 5 种状态; 掌握如何控制线程的状态; 掌握线程同步的方法。2.实训内容编写一个仓库的进货与销售同步控制的线程实例。学生成绩读写程序设计