《面向对象的程序设计-Java张白一第三版》由会员分享,可在线阅读,更多相关《面向对象的程序设计-Java张白一第三版(53页珍藏版)》请在金锄头文库上搜索。
1、第13章 多线程 第13章 多线程 13.1 Java中的多线程实现技术中的多线程实现技术 13.2 多线程的管理多线程的管理 第13章 多线程 13.1 Java中的多线程实现技术中的多线程实现技术多线程机制是Java语言的又一重要特征,使用多线程技术可以使系统同时运行多个执行体,这样就可以加快程序的响应时间,提高计算机资源的使用效率。正确使用多线程技术可提高整个应用系统的性能。第13章 多线程 13.1.1 线程的生命周期线程的生命周期每个Java程序都有一个缺省的主线程。对于Application,主线程是main()方法执行的线索。对于Applet,主线程指挥浏览器加载并执行Java小
2、程序。要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程。新建的线程在它的一个完整的生命周期中通常要经历新生、就绪、运行、阻塞和死亡等五种状态,这五种状态之间的转换关系和转换条件如图13.1所示。第13章 多线程 图13.1 线程的生命周期 第13章 多线程 1新生状态新生状态当用new关键字和某线程类的构造方法创建一个线程对象后,这个线程对象处于新生状态,此时它已经有了相应的内存空间,并已被初始化。处于该状态的线程可通过调用start()方法进入就绪状态。2就绪状态就绪状态处于就绪状态的线程已经具备了运行的条件,但尚未分配到CPU资源,因而
3、它将进入线程队列排队,等待系统为它分配CPU。一旦获得了CPU资源,该线程就进入运行状态,并自动地调用自己的run方法。此时,它脱离创建它的主线程,独立开始了自己的生命周期。第13章 多线程 3运行状态运行状态进入运行状态的线程执行自己的run方法中的代码。若遇到下列情况之一,将终止run方法的执行:(1) 终止操作。调用当前线程的stop方法或destroy方法进入死亡状态。(2) 等待操作。调用当前线程的join(millis)方法或wait(millis)方法进入阻塞状态。当线程进入阻塞状态时,在millis(毫秒)内可由其他线程调用notify或notifyAll方法将其唤醒,进入就绪
4、状态。在millis内若不唤醒,则需等待到当前线程结束。第13章 多线程 (3) 睡眠操作。调用sleep(millis)方法来实现。当前线程停止执行后,会处于阻塞状态,睡眠millis(毫秒)之后重新进入就绪状态。(4) 挂起操作。通过调用suspend方法来实现。将当前线程挂起,进入阻塞状态,之后当其他线程调用当前线程的resume方法后,才能使其进入就绪状态。(5) 退让操作。通过调用yield方法来实现。当前线程放弃执行,进入就绪状态。(6) 当前线程要求I/O时,则进入阻塞状态。(7) 若分配给当前线程的时间片用完,则当前线程进入就绪状态。若当前线程的run方法执行完,则线程进入死亡
5、状态。第13章 多线程 4阻塞状态阻塞状态一个正在执行的线程在某些特殊情况下,如执行了suspend、join或sleep方法,或等待I/O设备的使用权,那么它将让出CPU并暂时中止自己的执行,进入阻塞状态。阻塞时它不能进入就绪队列,只有当引起阻塞的原因被消除时,线程才可以转入就绪状态,重新进到线程队列中排队等待CPU资源,以便从原终止处开始继续运行。5死亡状态死亡状态处于死亡状态的线程将永远不再执行。线程死亡有两个原因:一是正常运行的线程完成了它的全部工作;二是线程被提前强制性地终止了。例如,通过执行stop或destroy方法来终止线程。第13章 多线程 13.1.2 Thread类的方法
6、类的方法Thread类(线程类)是java.lang包中的一个专门用来创建线程和对线程进行操作的类。Java在Thread类中定义了许多方法,这些方法可以帮助我们运用和处理线程。这些方法可分为四组:(1) 构造方法。该方法用于创建用户的线程对象。表13.1列出了Thread类的构造方法。第13章 多线程 表表13.1 java.lang.Thread类的构造方法类的构造方法 第13章 多线程 (2) run()方法。该方法用于定义用户线程所要执行的操作。(3) 改变线程状态的方法,如start()、sleep()、stop()、suspend()、resume()、yield()和wait()
7、方法等。这是最常用的一组方法。(4) 其他方法,如setPriority()、setName()等。表13.2列出了Thread类的后三组方法。第13章 多线程 表表13.2 java.lang.Thread类的常用方法类的常用方法 第13章 多线程 第13章 多线程 在Java语言中创建线程对象有两种途径:一是以创建Thread类的子类为途径,二是以实现Runnable接口为途径。用实现Runnable接口的方式创建线程与用继承Thread类的方式创建线程无本质差别,但是,由于Java不支持多继承,因此任何类如果已经继承了某一类时,就无法再继承Thread类,这时只能通过实现接口Runnab
8、le的方式创建线程对象。例如,因为小应用程序已经继承了Applet类,所以不能再继承Thread类,而只能通过Runnable接口实现多线程。第13章 多线程 13.1.3 通过继承通过继承Thread类方式创建线程类方式创建线程前已述及,在Java语言中创建线程对象的途径之一是创建Thread类的子类。创建Thread类的子类时,首先应声明子类的构造方法,其次应用自己定义的run()方法去覆盖Thread类的run()方法,即将自己要执行的程序区块写入run()方法中。【示例程序C13_1.java】 用Thread类的子类创建两个线程对象。第13章 多线程 imporlendar;clas
9、s C13_1 extends Thread int pauseTime; String name; public C13_1(int hTime, String hStr) pauseTime = hTime; name = hStr; public void run() Calendar now; / Calendar是Java系统提供的日期时间类的类型标识符第13章 多线程 int year,month,date,hour,minute,second; for(int i=1;i10;i+) try now=Calendar.getInstance(); /取系统时间 year=now.
10、get(Calendar.YEAR); /取年值 month=now.get(Calendar.MONTH)+1; /取月值 date=now.get(Calendar.DATE); /取日期值 hour=now.get(Calendar.HOUR_OF_DAY); /取小时值 minute=now.get(Calendar.MINUTE); /取分值 second=now.get(Calendar.SECOND); /取秒值 System.out.println( +name+时间: +year+ 年 +month+ 月 + date+ 日 + hour+ 小时 +minute+ 分 +se
11、cond+ 秒); /显示时间 Thread.sleep(pauseTime); 第13章 多线程 catch(Exception e) System.out.println(线程错误:+e); static public void main(String args) C13_1 myThread1 = new C13_1(2000, 线程A); /A线程执行一次后睡眠2000毫秒 myThread1.start(); C13_1 myThread2 = new C13_1(1000, 线程B); /B线程执行一次后睡眠1000毫秒 myThread2.start(); 第13章 多线程 执行
12、这个程序后,可得到如下运行结果(读者上机运行时与这里列出的具体时间不同): 线程A时间: 2012 年 8 月 19 日 9 小时 7 分 2 秒 线程B时间: 2012 年 8 月 19 日 9 小时 7 分 2 秒 线程B时间: 2012 年 8 月 19 日 9 小时 7 分 3 秒 线程A时间: 2012 年 8 月 19 日 9 小时 7 分 4 秒 线程B时间: 2012 年 8 月 19 日 9 小时 7 分 4 秒 线程B时间: 2012 年 8 月 19 日 9 小时 7 分 5 秒 线程A时间: 2012 年 8 月 19 日 9 小时 7 分 6 秒 线程B时间: 201
13、2 年 8 月 19 日 9 小时 7 分 6 秒 线程B时间: 2012 年 8 月 19 日 9 小时 7 分 7 秒 线程A时间: 2012 年 8 月 19 日 9 小时 7 分 8 秒第13章 多线程 线程B时间: 2012 年 8 月 19 日 9 小时 7 分 8 秒 线程B时间: 2012 年 8 月 19 日 9 小时 7 分 9 秒 线程A时间: 2012 年 8 月 19 日 9 小时 7 分 10 秒 线程B时间: 2012 年 8 月 19 日 9 小时 7 分 10 秒 线程A时间: 2012 年 8 月 19 日 9 小时 7 分 12 秒 线程A时间: 2012
14、 年 8 月 19 日 9 小时 7 分 14 秒 线程A时间: 2012 年 8 月 19 日 9 小时 7 分 16 秒 线程A时间: 2012 年 8 月 19 日 9 小时 7 分 18 秒第13章 多线程 13.1.4 通过实现通过实现Runnable接口方式创建线程接口方式创建线程创建线程对象的另一个途径是实现Runnable接口,而Runnable接口只有一个方法run(),用户新建线程的操作就由这个方法来决定。run()方法必须由实现此接口的类来实现。定义好run()方法之后,当用户程序需要建立新线程时,只要以这个实现了run()方法的类为参数创建系统类Thread的对象,就可
15、以把用户实现的run()方法继承过来。第13章 多线程 【示例程序C13_2.java】 通过创建两个线程实现“Java Now!”与矩形框在屏幕上呈相反方向的不停走动。该程序由图13.2所示的三个程序组成: 实现屏幕上的字符“Java Now!”走动的线程程序CString.java; 实现屏幕上矩形框走动的线程程序CSquare.java, 主程序C13_2.java。 第13章 多线程 图13.2 示例程序位置图 第13章 多线程 (1) 主程序C13_2.java:package c2;import java.awt.BorderLayout;import java.awt.Color
16、;import java.awt.Container;import java.awt.Dimension;import javax.swing.JApplet;public class C13_2 extends JApplet Override public void init() Container cp=getContentPane(); /得到窗口容器对象 CString pa=new CString(); /创建JPanel类的对象第13章 多线程 CSquare pa1=new CSquare(); /创建JPanel类的对象 pa.setPreferredSize(new Dim
17、ension(300,150); pa.setBackground(Color.cyan); /设置pa的对象背景颜色 pa1.setPreferredSize(new Dimension(300,150); pa1.setBackground(Color.cyan); /设置pa1的对象背景颜色 /cp容器的布局为BorderLayout,添加pa及pa1的对象到cp容器中 cp.add(pa,BorderLayout.NORTH); cp.add(pa1,BorderLayout.SOUTH); 第13章 多线程 (2) CString.java程序:package c2;import j
18、ava.awt.Font;import java.awt.Graphics;import java.awt.Graphics2D;import javax.swing.JPanel;public class CString extends JPanel implements Runnable int x=10,y=50; String Message=“Java Now!”; /创建字符串对象 Font f=new Font(“TimesRoman”,Font.BOLD,24); /创建字体对象 Thread th1=new Thread(this);第13章 多线程 public CStri
19、ng() start(); private void start() th1.start(); Override public void run() while(true) x=x-5; if(x=0)x=300; repaint(); /repaint()方法调用paint()方法重画字符串 try第13章 多线程 Thread.sleep(500); /使th1线程睡眠500ms catch(InterruptedException e) ; / while /run Override public void paint(Graphics g) super.paint(g); Graphi
20、cs2D g2=(Graphics2D)g; g2.setFont(f); /设置字体 g2.drawString(Message,x,y); 第13章 多线程 (3) CSquare.java程序:package c2;import java.awt.Graphics;import java.awt.Graphics2D;import java.awt.geom.Rectangle2D;import javax.swing.JPanel;public class CSquare extends JPanel implements Runnable int x1,y1,w1,h1; Threa
21、d th2=new Thread(this); public CSquare()第13章 多线程 x1=5; y1=100; w1=40; h1=40; start(); private void start() th2.start(); Override public void run() while(true) x1=x1+5; if(x1=250)x1=0; repaint(); /repaint()方法调用paint()方法重画矩形框 try第13章 多线程 Thread.sleep(500); /使th2线程睡眠500ms catch(InterruptedException e)
22、; / while /run Override public void paint(Graphics g) super.paint(g); Graphics2D g2=(Graphics2D)g; Rectangle2D.Double rec1=new Rectangle2D.Double(x1,y1,w1,h1); g2.draw(rec1); 该程序的运行结果如图13.3所示。第13章 多线程 图13.3 程序C13_2运行中的一个瞬间 第13章 多线程 13.2 多线程的管理多线程的管理13.2.1 线程调度线程调度在单CPU的计算机上运行多线程程序,或者当线程数多于处理机的数目时,势必
23、存在多个线程争用CPU的情况,这时需要提供一种机制来合理地分配CPU,使多个线程有条不紊、互不干扰地工作,这种机制称为调度。在Java运行系统中,由线程调度器对线程按优先级进行调度。线程调度器中写好了相应的调度算法,当有多个线程在同一时刻处于就绪状态时,线程调度器会选择优先级最高的线程运行。但是,如果发生下列情况之一,调度器就会终止此线程的运行:第13章 多线程 (1) 本线程的线程体中调用了yield()方法,从而让出了对CPU的占有权;(2) 本线程的线程体中调用了sleep()方法,使线程进入睡眠状态;(3) 本线程由于I/O操作而进入阻塞状态;(4) 另一个具有更高优先级的线程从睡眠状
24、态被唤醒,或其I/O操作完成而返回就绪状态。Java的线程调度算法可分为两种:一种是优先抢占式调度;另一种是轮转调度。第13章 多线程 当线程的优先级不同时,为保证优先级最高的线程先运行而采用优先抢占式调度算法,即优先级高的线程优先抢占CPU。例如,在程序的运行过程中若设置线程A具有最高优先级,则线程A将立即取代正在运行的其他线程,直到线程A处于阻塞状态或运行结束。当若干个线程具有相同的优先级时,可采用队列轮转调度算法,即当一个线程运行结束时,该优先队列中排在最前面的线程运行。如果某个线程由于睡眠或I/O阻塞成为一个等待再次运行的线程,那么当它恢复到可运行状态后,即被插入到该队列的队尾,必须等
25、到其他具有相同优先级的线程都被调度过一次后,才有机会再次运行。第13章 多线程 13.2.2 线程优先级线程优先级在Java系统中,运行的每个线程都有优先级。设置优先级是为了在多线程环境中便于系统对线程进行调度,优先级高的线程将优先得以运行。Java线程的优先级是一个在110之间的正整数,数值越大,优先级越高,未设定优先级的线程其优先级取缺省值5。Java线程的优先级设置遵从下述原则:(1) 线程创建时,子线程继承父线程的优先级。(2) 线程创建后,可在程序中通过调用setPriority()方法改变线程的优先级。第13章 多线程 (3) 线程的优先级是110之间的正整数,并用标识符常量MIN
26、_PRIORITY表示优先级为1,用NORM_PRIORITY表示优先级为5,用MAX_PRIORITY表示优先级为10。其他级别的优先级既可以直接用110之间的正整数来设置,也可以在标识符常量的基础上加一个常数。例如,下面的语句将线程优先级设置为8。setPriority(Thread.NORM_PRIORITY+3);第13章 多线程 【示例程序C13_3.java】 创建三个线程A、B、C,根据优先级确定线程的执行顺序。class C13_3 public static void main(String args) Thread First=new MyThread(“A”); /创建A
27、线程 First.setPriority(Thread.MIN_PRIORITY); /A线程优先级为1 Thread Second=new MyThread(“B”); /创建B线程 Second.setPriority(Thread.NORM_PRIORITY+1); /B线程优先级为6 Thread Third=new MyThread(“C”); /创建C线程 Third.setPriority(Thread.MAX_PRIORITY); /C线程优先级为10第13章 多线程 First.start(); Second.start(); Third.start(); class MyT
28、hread extends Thread String message; MyThread(String message) this.message= message; public void run( ) for (int i=0;i2;i+) System.out.println(message+ +getPriority(); 第13章 多线程 该程序的运行结果如下:A 1 C 10 C 10 B 6 B 6 A 1从程序的运行结果可以看出,虽然线程C在程序中最后调用start()方法进入就绪状态,但由于它的优先级是三个线程中最高的,因此可先执行。第13章 多线程 13.2.3 线程同步
29、线程同步由于Java支持多线程,具有并发功能,从而大大提高了计算机的处理能力。在各线程之间不存在共享资源的情况下,几个线程的执行顺序可以是随机的。但是,当两个或两个以上的线程需要共享同一资源时,线程之间的执行次序就需要协调,并且在某个线程占用这一资源时,其他线程只能等待。例如生产者和消费者的问题,只有当生产者生产出产品并将其放入货架后,消费者才能从货架上取走产品进行消费。当生产者没有生产出产品时,消费者是没法消费的。同理,当生产者生产的产品堆满货架时,应该暂停生产,等待消费者消费。在程序设计中,可用两个线程分别代表这里的第13章 多线程 生产者和消费者,可将货架视为任意时刻只允许一个线程访问的
30、临界资源。在这个问题中,两个线程要共享货架这一临界资源,需要在某些时刻(货空/货满)协调它们的工作,即货空时消费者应等待,而货满时生产者应等待。为了不发生混乱,还可进一步规定:当生产者往货架上放货物时不允许消费者取货物,当消费者从货架上取货物时不允许生产者放货物。这种机制在操作系统中称为线程间的同步。在同步机制中,将那些访问临界资源的程序段称为临界区。第13章 多线程 在Java系统中,临界区程序段是用关键字“synchronized”来标注,并通过一个称为监控器的系统软件来管理的。当执行被冠以synchronized的程序段即临界区程序时,监控器将这段程序(访问的临界资源)加锁,此时,称该线
31、程占有临界资源,直到这段程序执行完,才释放锁。只有锁被释放后,其他线程才可以访问这些临界资源。用关键字synchronized定义临界区的语句形式是:synchronized (expression) statement其中,expression代表类的名字,是可选项;statement可以是一个方法,也可以是一个语句或一个语句块,最常见的是一个方法。下面通过一个例子来说明线程的同步问题。第13章 多线程 【示例程序C13_4.java】 生产者与消费者的同步问题。public class C13_4 public static void main(String args) HoldInt h
32、=new HoldInt(); /h为监控器 ProduceInt p=new ProduceInt(h); ConsumeInt c=new ConsumeInt(h); p.start(); c.start(); 第13章 多线程 class HoldInt private int sharedInt; private boolean writeAble=true; /writeAble=true表示生产者线程能生产新数据 public synchronized void set(int val) /临界区程序段,也称为同步方法 while(!writeAble) /生产者线程不能生产新数
33、据时进入等待 try wait(); catch(InterruptedException e) /生产者被唤醒后继续执行下面的语句 writeAble=false; sharedInt=val; notify(); 第13章 多线程 public synchronized int get() /同步方法 while(writeAble) /消费者线程不能消费数据时进入等待状态 try wait(); catch(InterruptedException e) /消费者被唤醒后继续执行下面的语句 writeAble=true; notify(); return sharedInt; 第13章
34、多线程 /ProduceInt 是生产者线程class ProduceInt extends Thread private HoldInt hi; public ProduceInt(HoldInt hiForm) hi=hiForm; public void run( ) for(int i=1;i=4;i+) hi.set(i); System.out.println(产生的新数据是: + i); 第13章 多线程 /ConsumeInt 是消费者线程class ConsumeInt extends Thread private HoldInt hi; public ConsumeInt(
35、HoldInt hiForm) hi=hiForm; public void run( ) for(int i=1;i=4;i+) int val=hi.get(); System.out.println(读到的数据是: + val); 第13章 多线程 在这个程序中,共享数据sharedInt的方法set()和get()头部的修饰符synchronized使HoldInt的每个对象都有一把锁。当ProduceInt对象调用set()方法时,HoldInt对象就被锁定。当set()方法中的数据成员writeAble值为true时,set()方法就可以向数据成员sharedInt中写入一个值,而
36、get()方法不能从sharedInt上读出值。如果set()方法中的writeAble的值为false,则调用set()方法中的wait()方法,把调用set()方法的ProduceInt对象放到HoldInt对象的等待队列中,并将HoldInt对象的锁打开,使该对象的其他synchronized方法可被调用。这个ProduceInt对象将一直在等待队列中等待,直到被唤醒使它进入就绪状态,等待分配CPU。当ProduceInt第13章 多线程 对象再次进入运行状态时,HoldInt对象就被隐含地锁定,而set()方法将继续执行while循环中wait()方法后面的语句。在本例中,wait()
37、方法后面无其他语句,因此将进入下一次循环,判断while条件。ConsumeInt对象调用get()方法的情况与ProduceInt对象调用set()方法的情况类似,这里不再赘述。第13章 多线程 该程序的运行结果如下:产生的新数据是: 1读到的数据是: 1产生的新数据是: 2读到的数据是: 2产生的新数据是: 3产生的新数据是: 4读到的数据是: 3读到的数据是: 4第13章 多线程 13.2.4 线程组线程组Java系统的每个线程都属于某一个线程组。采用线程组结构以后,可以对多个线程进行集中管理。比如,可以同时启动、挂起或者终止一个线程组中的全部线程。Java系统专门在java.lang包中提供了ThreadGroup类来实现对线程组的管理功能。第13章 多线程 大多数情况下,一个线程属于哪个线程组是由编程人员在程序中指定的,若编程人员没有指定,则Java系统会自动将这些线程归于“main”线程组。main线程组是Java系统启动时创建的。一个线程组不仅可以包含多个线程,而且线程组中还可以包含其他的线程组,构成树形结构。一个线程可以访问本线程组的有关信息,但无法访问本线程组的父线程组。有关线程组的更详细的内容请查阅Java手册,本书不再赘述。