第10章 Java的线程处理 10.1 线程的基本概念 10.2 线程的属性 10.3 线程组 10.4 多线程程序的开发
10.1 线程的基本概念 我们已经对多任务非常熟悉,Windows和Linux都是多任务的操作系统。这些操作系统可以同时运行两个或两个以上的程序,并且看起来这些程序似乎在同时运行。当然,除非你的计算机拥有多个处理器,否则这些程序是不可能同时运行的。操作系统负责把系统资源分配给这些运行中的程序,并让人感觉它们是并发活动的。图10.1显示了支持多任务的操作系统和不支持多任务的操作系统运行程序的情况。
图 10.1
实现多任务通常有两种方法,一种称为抢占式多任务(preemptive multitasking);一种叫合作式多任务(cooperative multitasking)。对于抢占式多任务,操作系统自行决定何时中断一个程序,将执行时间分给其他程序。相反,对于合作式多任务操作系统将与程序进行协商,只有程序自愿放弃控制时才被中断。虽然抢占式多任务实现起来困难一些,但却有效得多。对于合作式多任务来说,一个运行不好的程序会占有整个系统。
多线程把操作系统的多任务原理应用到程序中,进一步发展了这一原理。应用了多线程技术的程序如同多任务操作系统一样,可以同时执行多个任务。每个任务被称为一个线程——它是线程控制流的简称。实际上,多线程的应用非常广泛,例如,浏览器在下载数据的同时还可以浏览其他网页,或者当某个网页下载太慢时,还可以控制浏览器中止这个网页浏览。Java语言本身也使用一个线程在后台收集无用的内存单元——这样就减少了用户管理内存的麻烦!
通常,我们把操作系统的多个任务称为进程(Process),而程序中的多任务则称为线程。那么,线程和进程之间有什么区别呢?最基本的区别就是每个进程都拥有一组完整的属于自己的变量,而线程则共享这些数据。看起来这样似乎不如进程安全,确实如此,本章后面将会更详细地讨论。但线程的优势在于创建和注销线程的开销比运行新的进程少得多,所以现在主流的操作系统都支持多线程。而且,和进程间的通信相比,线程间的通信要快得多,也方便得多。
10.1.1 线程 不少程序语言都提供对线程的支持,同这些语言相比,Java的特点是从最底层开始就对线程提供支持。除此以外,标准的Java类是可重载的,它允许在一个给定的应用程序中由多个线程调用同一方法,而线程彼此之间又互不干扰。Java的这些特点为多线程应用程序的设计奠定了基础。 究竟什么是线程呢?正如图10.2中所示,一个线程是给定的指令的序列 (你所编写的代码)、一个栈(在给定的方法中定义的变量),以及一些共享数据(类一级的变量)。线程也可以从全局类中访问静态数据。
图 10.2
每个线程都有其自己的堆栈和程序计数器(PC)。用户可以把程序计数器(PC)设想为用于跟踪线程正在执行的指令,而堆栈用于跟踪线程的上下文(上下文是当线程执行到某处时,当前的局部变量的值)。虽然用户可以编写出在线程之间传送数据的子程序,但在正常情况下,一个线程不能访问另外一个线程的栈变量。
一个线程或执行上下文由三个主要部分组成: ① 一个虚拟处理机 ② CPU执行的代码 ③ 代码操作的数据 代码可以或不可以由多个线程共享,这和数据是独立的。两个线程如果执行同一个类的实例代码,则它们可以共享相同的代码。
类似地,数据可以或不可以由多个线程共享,这和代码是独立的。两个线程如果共享对一个公共对象的存取,则它们可以共享相同的数据。 在Java编程中,虚拟处理机封装在Thread类的一个实例里。构造线程时,定义其上下文的代码和数据是由传递给它的构造函数的对象指定的。
10.1.2 创建线程 在Java平台中,创建一个线程非常简单,最直接的方法就是从线程类java.lang.Thread继承。在缺省情况下,线程类可以被所有的Java应用程序调用。为了使用线程类,我们需要了解The java.lang.Thread 类中定义的五个方法: ● run():该方法用于线程的执行。你需要重载该方法,以便让线程做特定的工作。 ● start():该方法使得线程启动run()方法。
● stop():该方法同start()方法的作用相反,用于停止线程的运行。 ● suspend():该方法同stop()方法不同的是,它并不终止未完成的线程,而只是挂起线程,以后还可恢复。 ● resume():该方法重新启动已经挂起的线程。 下面我们看一个通过派生Thread类来创建线程的实例。
例10.1 TestThreads.java public class TestThreads { public static void main (String args []) MyThread a = new MyThread("Thread A"); MyThread b = new MyThread("Thread B"); MyThread c = new MyThread("Thread C"); a.start(); b.start(); c.start();
} class MyThread extends Thread { String which; MyThread (String which) this.which = which; public void run()
{ int iterations = (int)(Math.random()*100) %15; int sleepinterval = (int)(Math.random()*1000); System.out.println(which + " running for " + iterations +" iterations"); System.out.println(which + " sleeping for " + sleepinterval + "ms between loops"); for (int i = 0; i < iterations; i++) System.out.println(which +" " + i); try
Thread.sleep(sleepinterval); } catch (InterruptedException e) {} 这个例子演示了如何从现有的Thread类中派生出一个新类。
新创建的类重载了run()方法,但实现run()方法不必很严格,因为Thread类可提供一个缺省的run()方法,尽管它不是特别有用。其运行结果如下: Thread A running for 2 iterations Thread A sleeping for 913ms between loops Thread A 0 Thread B running for 12 iterations Thread B sleeping for 575ms between loops Thread B 0
Thread C running for 4 iterations Thread C sleeping for 370ms between loops Thread C 0 Thread C 1 Thread B 1 Thread C 2 Thread A 1 Thread C 3 Thread B 2 Thread B 3
Thread B 4 Thread B 5 Thread B 6 Thread B 7 Thread B 8 Thread B 9 Thread B 10 Thread B 11
10.1.3 使用Runnable接口 在不少场合,你不能重新定义类的父母,或者不能定义派生的线程类,但也许你的类的层次要求父类为特定的类,然而,Java语言是不支持多父类的。在这些情况下,可以通过Runnable接口来实现多线程的功能。 实际上,Thread类本身也实现了Runnable接口。一个Runnable接口提供了一个public void run()方法。下面我们来看一个用Runnable接口创建线程的实例。
例10.2 RunnableTest.java public class RunnableTest { public static void main(String args[]) Test r = new Test(); Thread t = new Thread(r); t.start(); }
class Test implements Runnable { int i; public void run() while (true) System.out.println("Hello " + i++); if (i == 10) break;
} 上面程序的运行结果非常简单,这里不再列出。使用Runnable接口,需要我们实现run()方法。我们也需要创建Thread对象的一个实例,它最终是用来调用run()方法的。首先,main()方法构造了Test类的一个实例r。实例r有它自己的数据,在这里就是整数i。因为实例r是传给Thread的类构造函数的,所以r的整数i就是线程运行时刻所操作的数据。线程总是从它所装载的Runnable实例(在本例中,这个实例就是r)的run()方法开始运行。
一个多线程编程环境允许创建基于同一个Runnable实例的多个线程。这可以通过以下方法来做到: Thread t1= new Thread(r); Thread t2= new Thread(r); 此时,这两个线程共享数据和代码。
10.1.4 方法的选择 以上例子虽然展示了如何使用Runnable接口创建一个线程,但是它并不典型。我们说过,使用Runnable结构的主要原因是必须从其他父类继承。那么,什么时候才是使用Runnable接口的最佳时机呢。给定各种方法的选择,你如何决定使用哪个?下面分别列出了选用这两种方法的几个原则。
使用Runnable的原因: ● 从面向对象的角度来看,Thread类是一个虚拟处理机严格的封装,因此只有当处理机模型修改或扩展时,才应该继承类。正因为这个原因和区别一个正在运行的线程的处理机、代码和数据部分的意义,本教程采用了这种方法。 ● 由于Java技术只允许单一继承,所以如果你已经继承了Thread,你就不能再继承其他任何类,例如Applet。在某些情况下,这会使你只能采用实现Runnable的方法。
● 因为有时你必须实现Runnable,所以你可能喜欢保持一致,并总是使用这种方法。继承Thread的优点: ● 当一个run()方法体现在继承Thread类的类中,用this指向实际控制运行的Thread实例。因此代码简单了一些,许多Java编程语言的程序员使用扩展Thread的机制。 注:如果你采用这种方法,在你的代码生命周期的后期,单继承模型可能会给你带来困难。下面的例子中分别使用了两种方式创建线程,大家可以分析一下原因,以进一步理解如何使用这两个线程模型。
例10.3 TimerTest.java import java.awt.*; import java.awt.event.*; import javax.swing.*; import java.util.*; public class TimerTest { public static void main(String[] args)
JFrame f = new TimerTestFrame(); f.show(); } class TimerTestFrame extends JFrame { public TimerTestFrame() setSize(450, 300); setTitle("TimerTest");
addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) System.exit(0); } }); Container c = getContentPane(); c.setLayout(new GridLayout(2, 3));
c.add(new ClockCanvas("San Jose", "GMT-8")); c.add(new ClockCanvas("Taipei", "GMT+8")); c.add(new ClockCanvas("Berlin", "GMT+1")); c.add(new ClockCanvas("New York", "GMT-5")); c.add(new ClockCanvas("Cairo", "GMT+2")); c.add(new ClockCanvas("Bombay", "GMT+5")); } interface TimerListener
{ void timeElapsed(Timer t); } class Timer extends Thread private TimerListener target; private int interval; public Timer(int i, TimerListener t) target = t;
interval = i; setDaemon(true); } public void run() { try while (!interrupted()) sleep(interval); target.timeElapsed(this);
} catch(InterruptedException e) {} class ClockCanvas extends JPanel implements TimerListener
{ private int seconds = 0; private String city; private int offset; private GregorianCalendar calendar; private final int LOCAL = 16; public ClockCanvas(String c, String tz) city = c;
calendar = new GregorianCalendar(TimeZone.getTimeZone(tz)); Timer t = new Timer(1000, this); t.start(); setSize(125, 125); } public void paintComponent(Graphics g) { super.paintComponent(g); g.drawOval(0, 0, 100, 100);
double hourAngle = 2 * Math.PI * (seconds - 3 * 60 * 60) / (12 * 60 * 60); double minuteAngle = 2 * Math.PI * (seconds - 15 * 60) / (60 * 60); double secondAngle = 2 * Math.PI * (seconds - 15) / 60; g.drawLine(50, 50, 50 + (int)(30 * Math.cos(hourAngle)), 50 + (int)(30 * Math.sin(hourAngle))); g.drawLine(50, 50, 50 + (int)(40 * Math.cos(minuteAngle)), 50 + (int)(40 * Math.sin(minuteAngle)));
g.drawLine(50, 50, 50 + (int)(45 * Math.cos(secondAngle)), 50 + (int)(45 * Math.sin(secondAngle))); g.drawString(city, 0, 115); } public void timeElapsed(Timer t) { calendar.setTime(new Date()); seconds = calendar.get(Calendar.HOUR) * 60 * 60
+ calendar.get(Calendar.MINUTE) * 60 + calendar.get(Calendar.SECOND); repaint(); } 这个例子实现了一个多国时间的现实窗口。程序中,Timer 类是直接从Thread类继承的,而ClockCanvas类是通过实现Runnable接口来实现线程的功能的。显然,这是因为ClockCanvas类必须从JPanel类继承用来画出时钟。程序的运行结果如图10.3所示。
图 10.3
10.2 线 程 的 属 性 10.2.1 线程的状态 线程有四种状态,分别为 10.2 线 程 的 属 性 10.2.1 线程的状态 线程有四种状态,分别为 ● new(初始态):一个线程在调用new()方法之后,调用start()方法之前所处的状态。在初始态中,可以调用start()和stop()方法。
● rRunnable(可运行状态):一旦线程调用了start()方法,线程就转到Runnable()状态。注意,如果线程处于Runnable状态,它也有可能不在运行,这是因为还存在优先级和调度问题。 ● blocked(阻塞/挂起状态):线程处于阻塞状态。这是由两种可能性造成的:因挂起而暂停;由于某些原因而阻塞,例如等待IO请求的完成等。 ● dead(终止状态):线程转到退出状态。这有两种可能性:run()方法执行结束;调用了stop()方法。
一个Thread对象在它的生命周期中会处于各种不同的状态。图10.4形象地说明了这点。 尽管线程变为可运行的,但它并不立即开始运行。在一个只带有一个处理机的机器上,某一个时刻只能进行一个动作。在Java中,线程是抢占式的,但并不一定是分时的 (一个常见的概念错误是认为“抢占式”只不过是“分时”的一种别称而已)。抢占式调度模型是指可能有多个线程是可运行的,但只有一个线程在实际运行。
这个线程会一直运行,直至它不再是可运行的,或者另一个具有更高优先级的线程成为可运行的。对于后面一种情形,则是因低优先级线程被高优先级线程抢占了运行的机会。 一个线程可能因为各种原因而不再是可运行的:线程的代码可能执行了一个Thread.sleep()调用,要求这个线程暂停一段固定的时间;这个线程可能在等待访问某个资源,而且在这个资源可访问之前,这个线程无法继续运行。
图 10.4
所有可运行线程根据优先级保存在池中。当一个被阻塞的线程变成可运行时,它会被放回相应的可运行池。优先级最高的非空池中的线程会得到处理机时间(被运行)。 因为Java线程不一定是分时的,所有你必须确保你的代码中的线程会不时地给另外一个线程运行的机会。这可以通过在各种时间间隔中发出sleep()调用来做到。
来看如下程序段: public class Test implements Runnable { public void run() while (true) // do lots of interesting stuff // Give other threads a chance try
{ Thread.sleep(10); } catch (InterruptedException e) // This thread's sleep was interrupted // by another thread
注意try和catch块的使用。Thread 注意try和catch块的使用。Thread.sleep()和其他使线程暂停一段时间的方法是可中断的。线程可以调用另外一个线程的interrupt()方法,这将向暂停的线程发出一个InterruptedException。 Thread类的sleep()方法对当前线程操作,因此被称作Thread.sleep(x),它是一个静态方法。sleep()的参数指定以毫秒为单位的线程最小休眠时间,除非线程因为中断而提早恢复执行,否则它不会在这段时间之前恢复执行。
Thread类的另一个方法yield(),可以用来使具有相同优先级的线程获得执行的机会。如果具有相同优先级的其他线程是可运行的,yield()将把调用线程放到可运行池中并使另一个线程运行。如果没有相同优先级的可运行进程,yield()什么都不做。 sleep()调用会给较低优先级线程一个运行的机会。yield()方法只会给相同优先级线程一个执行的机会。
10.2.2 线程的调度 到目前为止,我们已经学习了创建和管理线程的基本知识。你需要做的就是启动一个线程,并让它运行。你的应用程序也许希望等待一个线程执行完毕,也许打算发送一个信息给线程,或者只打算让线程在处理之前休眠一会儿。线程类提供了四种对线程进行操作的重要方法:sleep()、join()、wait()和notify()。
sleep()方法是使线程停止一段时间的方法。在sleep时间间隔期满后,线程不一定立即恢复执行。这是因为在那个时刻,其他线程可能正在运行而且没有被调度为放弃执行,除非: (a) “醒来”的线程具有更高的优先级; (b) 正在运行的线程因为其他原因而阻塞。
如果一个应用程序需要执行很多时间,比如一个耗时很长的计算工作,你可以把该计算工作设计成线程。但是,假定还有另外一个线程需要计算结果,当计算结果出来后,如何让那个线程知道计算结果呢?解决该问题的一个方法是让第二个线程一直不停地检查一些变量的状态,直到这些变量的状态发生改变。这样的方式在Unix风格的服务器中常常用到。Java提供了一个更加简单的机制,即线程类中的join()方法。
join()方法使得一个线程等待另外一个线程结束后再执行。例如,一个GUI (或者其他线程)使用join()方法等待一个子线程执行完毕: CompleteCalcThread t = new CompleteCalcThread(); t.start(); // 做一会儿其他的事情 // 然后等待 t.join(); // 使用计算结果……
join()方法有三种格式: ● void join():等待线程执行完毕。 ● void join(long timeout):最多等待某段时间让线程完成。 ● void join(long milliseconds, int nanoseconds):最多等待某段时间(毫秒+纳秒),让线程完成。
线程API isAlive()同join()相关联时,是很有用的。一个线程在start(此时run()方法已经启动)之后,在stop之前的某时刻处于isAlive状态。 对于编写线程的程序员来说,还有其他两个有用的方法,即wait()和notify()。使用这两个API,我们可以精确地控制线程的执行过程。关于这两个方法的使用,将在后面详细解释。
10.2.3 线程的优先级 线程可以设定优先级,高优先级的线程可以安排在低优先级线程之前完成。一个应用程序可以通过使用线程中的setPriority(int)方法来设置线程的优先级大小。 对于多线程程序,每个线程的重要程度是不尽相同的,如多个线程在等待获得CPU时间时,往往需要优先级高的线程优先抢占到CPU时间得以执行;又如多个线程交替执行时,优先级决定了级别高的线程得到CPU的次数多一些且时间长一些。这样,高优先级的线程处理的任务效率就高一些。
Java中,线程的优先级从低到高以整数1~10表示,共分为10级。设置优先级是通过调用线程对象的setPriority()方法来进行的。设置优先级的语句为 Thread threadone=new Thread(); // 用Thread类的子类创建线程 Thread threadtwo=new Thread(); threadone.setPriority(6); // 设置threadone的优先级为6 threadtwo.setPriority(3); // 设置threadtwo的优先级为3
threadone.start(); threadtwo.start(); // strat()方法启动线程 这样,线程threadone将会优先于线程threadtwo执行,并将占有更多的CPU时间。该例中,优先级设置放在线程启动前。也可以在启动后进行优先级设置,以满足不同的优先级需求。
10.3 线 程 组 通常,一个程序可能包含若干线程,如何来管理这些线程呢?把这些线程按功能分类是个不错的办法。Java语言提供了线程组,线程组可以让你同时控制一组线程。实际上,线程组就是一种可以管理一组线程的类。
可以用构造方法ThreadGroup()来构造一个线程组,如下所示: String grounName = …; ThreadGroup g = new ThreadGroup(groupName); ThreadGroup()方法的参数表示一个线程组,因此该串参数必须是惟一的。也可以用Thread类的构造方法往一个指定的线程组里添加新的线程: Thread t = new Thread(g, threadName);
activeCount()方法用于检测某个指定线程组是否有线程处于活动状态: if (g.activeCount() = = 0) {// 线程g的所有线程都已停止} 要中断一个线程组中的所有线程,可以调用 ThreadGroup类的方法interrupt(): g.interrupt(); 线程组可以嵌套,即线程组可以拥有子线程组。缺省时,一个新创建的线程或线程组都属于当前线程组所属的线程组。
线程组的常用方法如下: ● ThreadGroup(String name):创建一个新线程组,它的父线程组是当前线程组。 ● ThreadGroup(ThreadGroup parent, String name):创建一个新线程组,其父线程组由parent参数指定。 ● int activeCount():返回当前线程组活动线程的上限。 ● int enumerate(Thread[] list):得到当前线程组各个活动线程的地址。 ● ThreadGroup getParent():得到当前线程组的父线程组。 ● Void interrupt():中断线程组中所有线程及其子线程组中所有线程。
10.4 多线程程序的开发 10.4.1 synchronized的基本概念 10.4 多线程程序的开发 10.4.1 synchronized的基本概念 关键字synchronized提供Java编程语言一种机制,允许程序员控制共享数据的线程。本节重点讨论其使用方法。 我们已经知道,进程允许两个或者更多个线程同时执行。实际上,这些线程也可以共享对象和数据,但在这种情形下,不同的线程在同一时间内不能存取同一数据,这是因为在开始设计Java的时候,就采用了线程的概念。
Java语言定义了一个特殊的关键字synchronized(同步),该关键字可以应用到代码块上(代码块也包括入口方法)。该关键字的目的是防止多个线程在同一时间执行同一代码块内的代码。 定义一个同步方法的格式如下: [public|private] synchronized {type} methodname(...) 一个简单的应用例子如下: public class someClass {
public void aMethod() { ... synchronized(this) // Synchronized code block }
同步化的关键字可以保证在同一时间内只有一个线程可以执行某代码段,而任何其他要用到该段代码的线程将被阻塞,直到第一个线程执行完该段代码,如图10.5所示对象锁标志synchronized到底是如何做到保证资源访问同步的呢?在Java技术中,每个对象都有一个和它相关联的标志。这个标志可以被认为是“锁标志”。synchronized关键字能保证多线程之间的同步运行,即允许独占地存取对象。当线程运行到synchronized语句时,它检查作为参数传递的对象,并在继续执行之前试图从对象获得锁标志。
图 10.5
意识到它自身并没有保护数据是很重要的。因为如果同一个对象的pop()方法没有受到synchronized的影响,且pop()是由另一个线程调用的,那么仍然存在破坏data的一致性的危险。如果要使锁有效,所有存取共享数据的方法必须在同一把锁上同步。 图10.6显示了如果pop()受到synchronized的影响,且另一个线程在原线程持有那个对象的锁时试图执行pop()方法时所发生的事情:
图 10.6
当线程试图执行synchronized(this)语句时,它试图从this对象获取锁标志。由于得不到标志,所以线程不能继续运行。然后,线程加入到与那个对象锁相关联的等待线程池中。当标志返回给对象时,某个等待这个标志的线程将得到这把锁并继续运行。 由于等待一个对象的锁标志的线程在得到标志之前不能恢复运行,所以让持有锁标志的线程在不再需要的时候返回标志是很重要的。
锁标志将自动返回给它的对象。持有锁标志的线程执行到synchronized()代码块末尾时将释放锁。Java技术特别注意了保证即使出现中断或异常而使得执行流跳出synchronized()代码块,锁也会自动返回。此外,如果一个线程对同一个对象两次发出synchronized调用,则在跳出最外层的块时,标志会正确地释放,而最内层的将被忽略。 这些规则使得与其他系统中的等价功能相比,管理同步块的使用简单了很多。
10.4.2 多线程的控制 线程有两个缺陷:死锁和饥饿。所谓死锁,就是一个或者多个线程,在一个给定的任务中,协同作用,互相干涉,从而导致一个或者更多线程永远等待下去。与此类似,所谓饥饿,就是一个线程永久性地占有资源,使得其他线程得不到该资源。
首先我们看一下死锁的问题。一个简单的例子就是:你到ATM机上取钱,却看到如下的信息“现在没有现金,请等会儿再试。”,你需要钱,所以你就等了一会儿再试,但是你又看到了同样的信息;与此同时,在你后面,一辆运款车正等待着把钱放进ATM机中,但是运款车到不了ATM取款机,因为你的汽车挡着道。在这种情况下,就发生了所谓的死锁。
在饥饿的情形下,系统并不处于死锁状态中,因为有一个进程仍在处理之中,只是其他进程永远得不到执行的机会而已。在什么样的环境下,会导致饥饿的发生,并没有预先确定好的规则。但一旦发生下面四种情况之一,就会导致死锁的发生。 ● 相互排斥:一个线程或者进程永远占有一共享资源,例如,独占该资源。 ● 循环等待:进程A等待进程B,而后者又在等待进程C,而进程C又在等待进程A。
● 部分分配:资源被部分分配。例如,进程A和B都需要访问一个文件,并且都要用到打印机,进程A获得了文件资源,进程B获得了打印机资源。 ● 缺少优先权:一个进程访问了某个资源,但是一直不释放该资源,即使该进程处于阻塞状态。 为了避免出现死锁的情况,我们就必须在多线程程序中做同步管理,为此必须编写使它们交互的程序。
java.lang.Object类中提供了两个用于线程通信的方法:wait()和notify()。如果线程对一个同步对象x发出一个wait()调用,则该线程会暂停执行,直到另一个线程对同一个同步对象x也发出一个wait()调用。 为了让线程对一个对象调用wait()或notify(),线程必须锁定那个特定的对象。也就是说,只能在它们被调用的实例的同步块内使用wait()和notify()。当某个线程执行包含对一个特定对象执行wait()调用的同步代码时,这个线程就被放到与那个对象相关的等待池中。此外,调用wait()的线程自动释放对象的锁标志。
对一个特定对象执行notify()调用时,将从对象的等待池中移走一个任意的线程,并放到锁池中。锁池中的对象一直在等待,直到可以获得对象的锁标记。notifyAll()方法将从等待池中移走所有等待那个对象的线程,并把它们放到锁池中。只有锁池中的线程能获取对象的锁标记,锁标记允许线程从上次因调用wait()而中断的地方开始继续运行。
在许多实现了wait()/notify()机制的系统中,醒来的线程必定是那个等待时间最长的线程。然而,在Java技术中,并不能保证这点。 应注意的是,不管是否有线程在等待,用户都可以调用notify()。如果对一个对象调用notify()方法,而在这个对象的锁标记等待池中并没有阻塞的线程,那么调用notify()将不起任何作用。对notify()的调用不会被存储。
下面给出一个线程交互的实例,用于说明如何使用synchronized关键字和wait()、notify()方法来解决线程的同步问题。 例10.4 DemoThread.java import java.lang.Runnable; import java.lang.Thread; public class DemoThread implements Runnable {
public DemoThread() { TestThread testthread1 = new TestThread(this,"1"); TestThread testthread2 = new TestThread(this,"2"); testthread2.start(); testthread1.start(); } public static void main(String[] args)
{ DemoThread demoThread1 = new DemoThread(); } public void run() TestThread t = (TestThread) Thread.currentThread(); try if (!t.getName().equalsIgnoreCase("1"))
{ synchronized(this) wait(); } while(true)
System.out.println("@time in thread" + t.getName()+ "="+ t.increaseTime()); if(t.getTime()%10 == 0) { synchronized(this) System.out.println("********************************"); notify(); if (t.getTime()==100)
break; wait(); } catch(Exception e) { e.printStackTrace();
class TestThread extends Thread { private int time = 0; public TestThread(Runnable r, String name) super(r, name); } public int getTime() return time;
} public int increaseTime() { return ++time; 本例实现了两个线程,每个线程输出1~100的数字。工作程序是:第一个线程输出1~10,停止,通知第二个线程;第二个线程输出1~10,停止,再通知第一个线程;第一个线程输出11~20……
在Java中,每个对象都有个对象锁标志(Object lock flag)与之想关联,当一个线程A调用对象的一段synchronized代码时,它首先要获取与这个对象关联的对象锁标志,然后才执行相应的代码,执行结束后,把这个对象锁标志返回给对象。因此,在线程A执行synchronized代码期间,如果另一个线程B也要执行同一对象的一段synchronized代码(不一定与线程A执行的相同),那么它必须等到线程A执行完后,才能继续。
在synchronized代码被执行期间,线程可以调用对象的wait()方法,释放对象锁标志,进入等待状态,并且可以调用notify()或者notifyAll()方法通知正在等待的其他线程。notify()方法通知等待队列中的第一个线程,notifyAll()方法通知的是等待队列中的所有线程。 程序的部分输出结果如下: @time in thread1=1 @time in thread1=2 @time in thread1=3 @time in thread1=4 @time in thread1=5
@time in thread1=6 @time in thread1=7 @time in thread1=8 @time in thread1=9 @time in thread1=10 ******************************** @time in thread2=1 @time in thread2=2 @time in thread2=3 @time in thread2=4 @time in thread2=5 @time in thread2=6
@time in thread2=7 @time in thread2=8 @time in thread2=9 @time in thread2=10 ******************************** 线程中还有三个控制线程执行的方法:suspend()、resume()和stop()方法。但从JDK1.2开始,Java标准不赞成使用它们来控制线程的执行,而是用wait()和notify()来代替它们。这里我们只对这三个方法做个简单地介绍。
● suspend()和resume()方法 resume()方法的惟一作用就是恢复被挂起的线程。所以,如果没有suspend(),resume()也就没有存在的必要。从设计的角度来看,有两个原因使得使用suspend()非常危险:它容易产生死锁;它允许一个线程控制另一个线程代码的执行。下面分别介绍这两种危险。 假设有两个线程:threadA和threadB。当正在执行它们的代码时,threadB获得一个对象的锁,然后继续它的任务。当threadA的执行代码调用threadB.suspend()时,将使threadB停止执行它的代码。
如果threadB. suspend()没有使threadB释放它所持有的锁,就会发生死锁。如果调用threadB 如果threadB.suspend()没有使threadB释放它所持有的锁,就会发生死锁。如果调用threadB.resume()的线程需要threadB仍持有的锁,这两个线程就会陷入死锁。 假设threadA调用threadB.suspend()。如果threadB被挂起时threadA获得控制,那么threadB就永远得不到机会来进行清除工作,例如使它正在操作的共享数据处于稳定状态。为了安全起见,只有threadB才可以决定何时停止它自己的代码。
你应该使用对同步对象调用wait()和notify()的机制来代替suspend()和resume()进行线程控制。这种方法是通过执行wait()调用来强制线程决定何时“挂起”自己。这使得同步对象的锁被自动释放,并给予线程一个在调用wait()之前稳定任何数据的机会。 ● stop()方法 stop()方法的情形是类似的,但结果有所不同。如果一个线程在持有一个对象锁的时候被停止,它将在终止之前释放它持有的锁。这避免了前面所讨论的死锁问题,但它又引入了其他问题。
在前面的范例中,如果线程在已将字符加入栈但还没有使下标值加1之后被停止,则在释放锁的时候会得到一个不一致的栈结构。 总会有一些关键操作需要不可分割地执行,而且在线程执行这些操作时被停止就会破坏操作的不可分割性。
一个关于停止线程的独立而又重要的问题涉及线程的总体设计策略。创建线程来执行某个特定作业,并存活于整个程序的生命周期。换言之,你不会这样来设计程序:随意地创建和处理线程,或创建无数个对话框或socket端点。每个线程都会消耗系统资源,而系统资源并不是无限的。这并不是暗示一个线程必须连续执行;它只是简单地意味着应当使用合适而安全的wait()和notify()机制来控制线程。
10.4.3 多线程之间的通信 Java 语言提供了各种各样的输入/输出流,使我们能够很方便地对数据进行操作。其中,管道(Pipe)流是一种特殊的流,用于在不同线程间直接传送数据(一个线程发送数据到输出管道,另一个线程从输入管道中读数据)。通过使用管道,就可实现不同线程间的通信,无需求助于类似临时文件之类的东西。
Java提供了两个特殊的、专门的类用于处理管道,它们就是PipedInputStream类和PipedOutputStream类。PipedInputStream代表了数据在管道中的输出端,也就是线程向管道读数据的一端;PipedOutputStream代表了数据在管道中的输入端,也就是线程向管道写数据的一端,这两个类一起使用可以提供数据的管道流。 为了创建一个管道流,我们必须首先创建一个PipedOutStream对象,然后创建PipedInputStream对象,前面我们已经介绍过。
一旦创建了一个管道,就可以像操作文件一样对管道进行数据的读/写。下面我们来看一个运用管道实现线程间通信的实例。 例10.5 PipeTest.java import java.util.*; import java.io.*; public class PipeTest { public static void main(String args[])
{ try /* set up pipes */ PipedOutputStream pout1 = new PipedOutputStream(); PipedInputStream pin1 = new PipedInputStream(pout1) PipedOutputStream pout2 = new PipedOutputStream(); PipedInputStream pin2 = new PipedInputStream(pout2);
/* construct threads */ Producer prod = new Producer(pout1); Filter filt = new Filter(pin1, pout2); Consumer cons = new Consumer(pin2); /* start threads */ prod.start(); filt.start(); cons.start();
} catch (IOException e){} class Producer extends Thread { private DataOutputStream out; private Random rand = new Random(); public Producer(OutputStream os)
out = new DataOutputStream(os); } public void run() { while (true) try double num = rand.nextDouble(); out.writeDouble(num); out.flush();
sleep(Math.abs(rand.nextInt() % 1000)); } catch(Exception e) { System.out.println("Error: " + e);
class Filter extends Thread { private DataInputStream in; private DataOutputStream out; private double total = 0; private int count = 0; public Filter(InputStream is, OutputStream os) in = new DataInputStream(is); out = new DataOutputStream(os);
} public void run() { for (;;) try double x = in.readDouble(); total += x; count++; if (count != 0) out.writeDouble(total / count);
} catch(IOException e) { System.out.println("Error: " + e); class Consumer extends Thread
private double old_avg = 0; private DataInputStream in; public Consumer(InputStream is) { in = new DataInputStream(is); } public void run() for(;;)
{ try double avg = in.readDouble(); if (Math.abs(avg - old_avg) > 0.01) System.out.println("Current average is " + avg); old_avg = avg; } catch(IOException e)
System.out.println("Error: " + e); } 上面的例子是一个示范管道流的程序。它拥有一个在随机时间产生随机数的生产者线程;一个用于读取输入数据并不断地计算它们的平均值的过滤线程;一个在屏幕上输出结果的消费者线程。这个程序不会自动结束,用户可以通过Ctrl+C键来终止它。