1、1,第7章 多线程,浙江工业大学计算机科学与技术,浙江工业大学图形图像研究所,高,飞,2,练习思考题,1、 什么是GUI?功能是什么? 2、 Java中GUI工具包有_和_。 3、 通过调用方法_可以改变当前容器的布局方式。 4、 通过_方法向容器中添加组件。 5、 AWT布局管理器有_、_、_、_和_。 6、 Java GUI编程中常用的Button是_。 A. 对象 B. 组件 C. 变量 D. 接口,3,第7章 多线程,线程简介 Thread类的子类创建线程 实现Runnable接口 基本的线程控制,本章的目的:,回顾关键词: Java的GUI 、用AWT生成图形化用户界面 、常用容器
2、、布局管理器 、 AWT事件处理模型 、 AWT常用组件,4,7.1线程简介,到目前为止所介绍过的各种范例都是单线程程序,也就是启动的Java程序在“同一时间”内只会做一件事。文本模式下最常进行的就是单线程程序。有时需要程序“同时”可以作很多事,即所谓多线程(Multi-thread)程序,在窗口程序、网络程序中常使用多线程功能,了解多线程概念与注意事项是非常重要的。,5,7.1.1进程与线程,程序是一段静态的代码,它是应用软件执行的蓝本。 进程是程序的一次动态执行过程,它对应了从代码加载、执行至执行完毕的一个完整过程,这个过程也是进程本身从产生、发展至消亡的过程。线程是比进程更小的执行单位,
3、一个进程在其执行过程中,可以产生多个线程,形成多条执行线索,每条线索,即每个线程也有它自身的产生、存在和消亡的过程,也是一个动态的概念。 Java的多线程就是在操作系统每次分时给Java程序一个时间片的CPU时间内,在若干个独立的可控制的线程之间切换。,6,7.1.2线程的状态,Java使用Thread类及其子类的对象来表示线程,线程在它的一个完整的生命周期中通常要经历如下的4种状态: 1. 创建状态(new Thread) 2. 可运行状态( Runnable ) 3. 不可运行状态(Not Runnable) 4. 死亡状态(Dead),7,7.2第一种方法: Thread类的子类创建线程
4、,用Thread类或子类创建线程对象 . 编写Thread类的子类时,需要重写父类的run方法,其目的是规定线程的具体操作,否则线程就什么也不做,因为父类的run方法中没有任何操作语句。 当JVM将CPU使用权切换给线程时,如果线程是Thread的子类创建的,该类中的run方法就立刻执行。,8,【例7-2】通过继承定义线程类,class ComputerSum int sum;public void setSum(int n) sum = n;public int getSum() return sum; ,9,class People extends Thread int timeLengt
5、h; /线程休眠的时间长度ComputerSum sum;People(String s, int timeLength, ComputerSum sum) setName(s); /调用Thread类的方法setName为线程起个名字this.timeLength = timeLength;this.sum = sum;public void run() for (int i = 1; i = 5; i+) int m = sum.getSum();sum.setSum(m + 1);System.out.println(“我是“ + getName() + “,现在的和:“ + sum.g
6、etSum();try sleep(timeLength); catch (InterruptedException e) ,10,public class ep7_2 public static void main(String args) People teacher, student;ComputerSum sum = new ComputerSum();teacher = new People(“老师“, 200, sum);student = new People(“学生“, 200, sum);teacher.start();/start是Thread类的方法student.sta
7、rt(); ,11,teacher = new People(“老师“, 170, sum); student = new People(“学生“, 200, sum);,12,7.3第二种方法:实现Runnable接口,创建线程的另一个途径就是用Thread类直接创建线程对象。使用Thread类创建线程对象时,常用的构造方法是: Thread(Runnable target); 该构造方法中的参数是一个Runnable类型的接口,因此,在创建线程对象时,必须向构造方法的参数传递一个实现Runnable接口类的实例,该实例对象称为所创线程的目标对象。,13,【例7-3】通过接口构造线程体,im
8、port java.awt.Graphics; import java.util.Date; import java.text.SimpleDateFormat; import java.awt.Color; import java.awt.Font; public class ep7_3 extends java.applet.Applet implements RunnableThread clockThread;public void start() /start为Applet类的方法if (clockThread = null) setBackground(Color.lightGra
9、y);repaint();clockThread = new Thread(this, “Clock“);clockThread.start(); / 启动线程 ,14,public void run() /run是Runable接口的方法while (clockThread != null) repaint(); / 刷新显示画面try clockThread.sleep(1000 catch (InterruptedException e) ,15,public void paint(Graphics g) /paint是Applet类的方法Date now = new Date(); /
10、 获得当前的时间对象SimpleDateFormat fr= new SimpleDateFormat(“yyyy年MM月dd日 HH:mm:ss“);Font ft=new Font(“宋体“,Font.PLAIN,20);/g.drawString(now.getHours() + “:“ + now.getMinutes() + “:“/+ now.getSeconds(), 5, 10);这些方法过时了g.setFont(ft);g.drawString(fr.format(now),80,150);public void stop() /stop是Applet类的方法/clockTh
11、read.stop();本方法过时了clockThread.interrupt();clockThread = null; ,16,本程序是Applet,要运行Applet程序,必须定义一个html文件,ep7_3.html文件内容如下:,17,18,上面这个例子是通过每隔1秒种就执行线程的刷新画面功能,显示当前的时间;看起来的效果就是一个时钟,每隔1秒就变化一次。由于采用的是实现接口Runnable的方式,所以该类Clock还继承了Applet,Clock就可以Applet的方式运行。,19,7.4基本的线程控制,可以通过线程的方法进行基本的线程控制,下面我们熟悉一下常用的方法。 1star
12、t()方法 线程调用该方法将启动线程,从新建状态进入就绪队列排队。一旦CPU资源轮转到它时,就脱离主线程开始自己的生命周期。,20,2run()方法 系统的Thread类中,run()方法没有具体内容,用户需要在程序中重写run()方法来覆盖原来的run()方法,run()方法中定义线程对象被调度之后所执行的操作,是系统自动调用而用户不能引用的方法。当run()方法执行完毕,线程就变成死亡状态,在线程没有结束run()方法之前,不要让线程再调用start()方法,否则将发生IllegalThreadStateException异常,这一点在写程序的时候需要注意。,21,3sleep(int m
13、illsecond)方法 sleep方法可以暂停一个线程的执行,在适当的时候再恢复其执行。就是让当前线程睡眠(停止执行)若干毫秒,线程由运行中状态进入不可运行状态,停止执行时间到后线程进入可运行状态。,22,4isAlive()方法 测试线程状态。可以通过Thread 中的isAlive()方法来获取线程是否处于活动状态;线程由start()方法启动后,直到其被终止之间的任何时刻,都处于Alive状态。线程处于新建状态时,调用isAlive()方法返回false,线程进入死亡状态后,调用isAlive()方法返回false。,23,5currentThread()方法 currentThrea
14、d()方法是Thread类的类方法,可以直接通过类名调用,该方法返回当前正在使用CPU资源的线程。,24,6interrupt()方法 interrupt()方法可以用来中断线程,还可以用来“吵醒”休眠的线程。但线程调用sleep方法处于休眠状态时,一个占有CPU资源的线程可以让休眠的线程调用interrupt方法唤醒自己。,25,【例7-4】interrupt的例子。,class A implements Runnable Thread student, teacher;A() teacher = new Thread(this);student = new Thread(this);tea
15、cher.setName(“王教授“);student.setName(“小强“);,26,public void run() if (Thread.currentThread() = student) try System.out.println(student.getName() + “正在睡觉,不听课“);Thread.sleep(1000 * 60 * 60); catch (InterruptedException e) System.out.println(student.getName() + “被老师叫醒了“);System.out.println(student.getNam
16、e() + “开始听课“); else if (Thread.currentThread() = teacher) for (int i = 1; i = 3; i+) System.out.println(“上课!“);try Thread.sleep(500); catch (InterruptedException e) student.interrupt(); / 吵醒student ,27,public class ep7_4 public static void main(String args) A a = new A();a.student.start();a.teacher.
17、start(); ,28,7stop()方法 (已过时) 通过调用线程的实例方法stop()来终止线程。线程终止后,其生命周期结束了,即进入死亡态,终止后的线程不能再被调度执行。,29,8join()方法 一个线程在占有CPU资源期间,可以让其他线程调用join()方法和本线程联合。当前线程等待调用该方法的线程结束后,再重新排队等待CPU资源,以便恢复执行。如果当前线程准备联合的线程已经结束,也就是start方法体已经执行完,那么不会产生任何效果。 TimerThread tt=new TimerThread(100); tt.start(); public void timeout() tt
18、.join(); /当前线程等待线程tt 执行完后再继续往下执行 ,30,【例7-5】线程联合的例子。,public class ep7_5 public static void main(String args) ThreadJoin a = new ThreadJoin();a.customer.start();a.tvMaker.start(); ,31,class ThreadJoin implements Runnable TV tv;Thread customer, tvMaker;ThreadJoin() customer = new Thread(this);tvMaker =
19、 new Thread(this);customer.setName(“顾客“);tvMaker.setName(“电视制造厂“);,32,public void run() if (Thread.currentThread() = customer) System.out.println(customer.getName() + “等“ + tvMaker.getName() + “生产电视“);try tvMaker.join(); / 线程customer开始等待tvMaker结束 catch (InterruptedException e) System.out.println(cus
20、tomer.getName() + “买了一台电视:“ + tv.name+ “ 价钱:“ + tv.price); else if (Thread.currentThread() = tvMaker) System.out.println(tvMaker.getName() + “开始生产电视,请等.“);try tvMaker.sleep(2000); catch (InterruptedException e) tv = new TV(“红星牌“, 3288);System.out.println(tvMaker.getName() + “生产完毕“); ,33,class TV flo
21、at price;String name;TV(String name, float price) this.name = name;this.price = price; ,34,7.5线程的调度,Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程。线程调度器按照线程的优先级决定应调度哪些线程来执行。 线程的优先级用数字来表示,范围从1到10,即Thread.MIN_PRIORITY到Thread.MAX_PRIORITY。一个线程的缺省优先级是5,即Thread.NORM_PRIORITY。,35,下述方法可以对优先级进行操作: int getPriority(); 得到线
22、程的优先级。 void setPriority(int newPriority); 当线程被创建后,可通过此方法改变线程的优先级。,36,线程调度器按线程的优先级高低选择高优先级线程(进入运行中状态)执行,同时线程调度是抢先式调度,即如果在当前线程执行过程中,一个更高优先级的线程进入可运行状态,则这个线程立即被调度执行。 抢先式调度又分为:时间片方式和独占方式。在时间片方式下,当前活动线程执行完当前时间片后,如果有其他处于就绪状态的相同优先级的线程,系统会将执行权交给其他就绪态的同优先级线程;当前活动线程转入等待执行队列,等待下一个时间片的调度。 在独占方式下,当前活动线程一旦获得执行权,将一
23、直执行下去,直到执行完毕或由于某种原因主动放弃CPU,或者是有一高优先级的线程处于就绪状态。,37,下面几种情况下,当前线程会放弃CPU: 1)线程调用了yield()或sleep()方法主动放弃; 2)由于当前线程进行I/O 访问,外存读写,等待用户输入等操作,导致线程阻塞;或者是为等候一个条件变量,以及线程调用wait()方法; 3)抢先式系统下,由高优先级的线程参与调度;时间片方式下,当前时间片用完,由同优先级的线程参与调度。,38,7.6多线程的互斥与同步,经常有一些同时运行的线程需要共享数据,此时就需考虑其他线程的状态和行为,否则就不能保证程序的运行结果的正确性。,ATM机1: 取1
24、00元 数据库减去100元,ATM机2: 取100元 数据库减去100元,39,7.6.1临界资源问题,下面是一个堆栈的类定义: class stack int idx = 0; / 堆栈指针的初始值为0char data = new char6; / 堆栈有6个字符的空间public void push(char c) / 压栈操作dataidx = c; / 数据入栈idx+; / 指针向上移动一位public char pop() / 出栈操作idx-; / 指针向下移动一位return dataidx; / 数据出栈 ,40,两个线程A和B在同时使用Stack的同一个实例对象,A正在往
25、堆栈里push一个数据,B则要从堆栈中pop一个数据。如果由于线程A和B在对Stack对象的操作上的不完整性,会导致操作的失败,具体过程如下所示: 1) 操作之前 data = | p | q | | | | | idx=2 2) A执行push中的第一个语句,将r推入堆栈; data = | p | q | r | | | | idx=2,41,3) A还未执行idx+语句,A的执行被B中断,B执行pop方法,返回q;data = | p | q | r | | | | idx=1 4A继续执行push的第二个语句: data = | p | q | r | | , | | idx=2 最后
26、的结果相当于r没有入栈。产生这种问题的原因在于对共享数据访问的操作的不完整性。,42,7.6.2互斥锁,为解决操作的不完整性问题,在Java 语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。关键字synchronized 来与对象的互斥锁联系。当某个对象用synchronized 修饰时,表明该对象在任一时刻只能由一个线程访问。,43,public void push(char c) synchronized (this) / this表示Stack的当前对象dataidx = c;i
27、dx+; public char pop() synchronized (this) / this表示Stack的当前对象idx-;return dataidx; ,44,synchronized 除了象上面讲的放在对象前面限制一段代码的执行外,还可以放在方法声明中,表示整个方法为同步方法。 public synchronized void push(char c) 如果synchronized用在类声明中,则表明该类中的所有方法都是synchronized的。,45,7.6.3多线程的同步,本小节将讨论如何控制互相交互的线程之间的运行进度,即多线程之间的同步问题,下面我们将通过多线程同步的模
28、型:生产者-消费者问题来说明怎样实现多线程的同步。 我们把系统中使用某类资源的线程称为消费者,产生或释放同类资源的线程称为生产者。,46,在下面的Java的应用程序中,生产者线程向文件中写数据,消费者从文件中读数据,这样,在这个程序中同时运行的两个线程共享同一个文件资源。通过这个例子我们来了解怎样使它们同步。,47,class SyncStack / 同步堆栈类private int index = 0; / 堆栈指针初始值为0private char buffer = new char6; / 堆栈有6个字符的空间public synchronized void push(char c) /
29、 加上互斥锁while (index = buffer.length) / 堆栈已满,不能压栈try this.wait(); / 等待,直到有数据出栈 catch (InterruptedException e) this.notify(); / 通知其它线程把数据出栈bufferindex = c; / 数据入栈index+; / 指针向上移动,48,public synchronized char pop() / 加上互斥锁while (index = 0) / 堆栈无数据,不能出栈try this.wait(); / 等待其它线程把数据入栈 catch (InterruptedExce
30、ption e) this.notify(); / 通知其它线程入栈index-; / 指针向下移动return bufferindex; / 数据出栈 ,49,class Producer implements Runnable / 生产者类SyncStack theStack;public Producer(SyncStack s) theStack = s;public void run() char c;for (int i = 0; i 10; i+) c = (char) (Math.random() * 26 + A);theStack.push(c); / 把字符入栈Syste
31、m.out.println(“Produced: “ + c); / 打印字符try Thread.sleep(int) (Math.random() * 1000); catch (InterruptedException e) ,50,class Consumer implements Runnable / 消费者类SyncStack theStack;public Consumer(SyncStack s) theStack = s;public void run() char c;for (int i = 0; i 10; i+) c = theStack.pop(); / 从堆栈中读
32、取字符System.out.println(“Consumed: “ + c);try Thread.sleep(int) (Math.random() * 1000); catch (InterruptedException e) ,51,public class ep7_7 public static void main(String args) SyncStack stack = new SyncStack();/ 下面的消费者类对象和生产者类对象所操作的是同一个同步堆栈对象Runnable source = new Producer(stack);Runnable sink = new
33、 Consumer(stack);Thread t1 = new Thread(source); / 线程实例化Thread t2 = new Thread(sink); / 线程实例化t1.start(); / 线程启动t2.start(); / 线程启动 ,52,类Producer是生产者模型,其中的 run()方法中定义了生产者线程所做的操作,循环调用push()方法,将生产的10个字母送入堆栈中,每次执行完push操作后,调用sleep()方法睡眠一段随机时间,以给其他线程执行的机会。类Consumer是消费者模型,循环调用pop()方法,从堆栈中取出一个数据,一共取10次,每次执行完
34、pop操作后,调用sleep()方法睡眠一段随机时间,以给其他线程执行的机会。,53,在上述的例子中,通过运用wait()和notify()方法来实现线程的同步,在同步中还会用到notifyAll()方法,一般来说,每个共享对象的互斥锁存在两个队列,一个是锁等待队列,另一个是锁申请队列,锁申请队列中的第一个线程可以对该共享对象进行操作,而锁等待队列中的线程在某些情况下将移入到锁申请队列。,54,比较一下wait()、notify()和notifyAll()方法:,1)wait,nofity,notifyAll:必须在已经持有锁的情况下执行,所以它们只能出现在synchronized作用的范围内
35、,也就是出现在用synchronized修饰的方法或类中。 2)wait的作用:释放已持有的锁,进入等待队列。 3)notify的作用:唤醒wait队列中的第一个线程并把它移入锁申请队列。 4)notifyAll的作用:唤醒wait队列中的所有的线程并把它们移入锁申请队列。,55,7.7 Daemon线程,一个Daemon线程是在后台执行的服务线程,例如网络服务器侦听连接端口的服务,隐藏系统线程,垃圾收集线程或其他JVM 建立的线程。当程序中所有的非Daemon的线程都结束了,即使Daemon线程的run()方法中还有需要执行的语句,也立刻结束执行。 线程默认是非Daemon线程。,56,一个
36、线程调用 void setDaemon(boolean on); 方法可以将自己设置成一个Daemon线程。这里,参数on取值为true意味着是Daemon线程;取值为false意味着是非Daemon线程,也称用户(user)线程。一个线程必须在运行之前设置自己是否是Daemon线程。,57,【例7-8】Daemon简单的示范,public class ep7_8 public static void main(String args) Thread thread = new Thread(/ 这是匿名类的写法new Runnable() public void run() while (true) System.out.print(“T“););/ 设置为Daemon线程thread.setDaemon(true);thread.start(); ,58,这个程序在主线程结束之后,Daemon线程也就会跟着结束,可以使用setDaemon()方法来设置一个线程是否为Daemon线程,如果没有使用setDaemon()方法设置为true,则程序会不断地打印出T字符而不终止(只能按Ctrl+C强迫中止程序);使用isDaemon()方法则可以判断该线程是否为Daemon线程。,59,第7章 小结,线程简介 Thread类的子类创建线程 实现Runnable接口 基本的线程控制,