资源描述
第七章 多线程
7.1 多线程的概念
多线程编程的含义是可将程序任务分成几个并行的子任务。特别是在网络编程中,你会发现很多功能是可以并发执行的。比如网络传输速度较慢,用户输入速度较慢,你可以用两个独立的线程去完成这个功能,而不影响正常的显示或其他功能。多线程是与单线程比较而言的,普通的WINDOWS采用单线程程序结构,其工作原理是:主程序有一个消息循环,不断从消息队列中读入消息来决定下一步所要干的事情,一般是一个子函数,只有等这个子函数执行完返回后,主程序才能接收另外的消息来执行。比如子函数功能是在读一个网络数据,或读一个文件,只有等读完这数据或文件才能接收下一个消息。在执行这个子函数过程中你什么也不能干。但往往读网络数据和等待用户输入有很多时间处于等待状态,多线程利用这个特点将任务分成多个并发任务后,就可以解决这个问题。
7.1.1 Java线程的模型
Java的设计思想是建立在当前大多数操作系统都实现了线程调度。Java虚拟机的很多任务都依赖线程调度,而且所有的类库都是为多线程设计的。实时上,Java支持Macintosh和Ms-dos 的平台; 所以迟迟未出来就是因为这两个平台都不支持多线程。Java利用多线程实现了整个执行环境是异步的。
在Java程序里没有主消息循环。 如果一个线程等待读取网络数据,它可以运行但不停 止系统的其他线程执行。用于处理用户输入的线程大多时间是等待用户敲键盘或击鼠标。还可以使动画的每一帧时间停顿一秒而并不使系统暂停。 一个线程启动后,它可以被挂起, 暂时不让它执行。 挂起的线程可以重新恢复执行。任何时间线程都可以被停止,被停止的线程就不能再重新启动。
Java语言里,线程表现为线程类,线程类封装了所有需要的线程操作控制。必须很清晰 地区分开线程对象和运行线程,你可以将线程对象看作是运行线程的控制面板。在线程对象 里有很多函数来控制一个线程是否运行, 睡眠,挂起或停止。线程类是控制线程行为的唯一的手段。
一个Java程序启动后,就已经有一个线程在运行。你可通过调用Thread.currentThread 函数来查看当前运行的是哪一个线程。当得到一个线程的控制柄,就可以作一个很有趣的事情,即使单线程也一样。下面这个例子让你知道怎样操纵当前线程。
Filename:testthread
public class testthread {
public static void main(String[] args) {
Thread t =Thread.currentThread();//调用Thread.currentThread 函数
t.setName("This Thread is running");
System.out.println("The running thread:" + t);
try {
for (int i=0;i<5;i++) {
System.out.println("Sleep time "+i);
Thread.sleep(1000);
}
}
catch (InterruptedException e) {
System.out.println("thread has wrong");
}
}
}
执行结果:
java testthread
The running thread:Thread[This Thread is running,5,main]
Sleep time 0
Sleep time 1
Sleep time 2
Sleep time 3
Sleep time 4
7.1.2 启动接口
一个线程并不激动人心,多个线程才有实际意义。怎样创建更多的线程呢?我们需要创建线程类的另一个实例。当我们构造了线程类的一个新的实例,我们必须告诉它在新的线程里应执行哪一段程序。你可以在任意实现了启动接口的对象上启动一个线程。启动接口是一个抽象接口,来表示本对象有一个函数想异步执行。要实现启动接口,一个类只需要有一个叫run的函数。下面是创建一个新线程的例子:
Filename:twothread.java
public class twothread implements Runnable {
public twothread() {
Thread t1 =Thread.currentThread();
t1.setName("The first main thread");
System.out.println("The running thread:" + t1);
Thread t2 = new Thread(this,"the second thread");//创建了一个Thread对象
System.out.println("creat another thread");
t2.start();
try {
System.out.println("first thread will sleep");
Thread.sleep(3000);
}
catch (InterruptedException e) {
System.out.println("first thread has wrong");
}
System.out.println("first thread exit");
}
public void run() {
try {
for (int i=0;i<5;i++) {
System.out.println("Sleep time for thread 2:"+i);
Thread.sleep(1000);
}
}
catch (InterruptedException e) {
System.out.println("thread has wrong");
}
System.out.println("second thread exit");
}
public static void main(String[] args) {
new twothread();
}
}
执行结果:java twothread
The running thread:Thread[The first main thread,5,main]
creat another thread first thread will sleep
Sleep time for thread 2:0
Sleep time for thread 2:1
Sleep time for thread 2:2
first thread exit Sleep time for thread 2:3
Sleep time for thread 2:4
second thread exit
main线程用new Thread(this, "the second thread")创建了一个Thread对象,通过传递第一个参 数来标明新线程来调用this对象的run函数。 然后我们调用start函数,它将使线程从run函数开始执行。
7.1.3 同步
因为多线程给提供了程序的异步执行的功能,所以在必要时必须还提出一种同步机制。 例如,想两个线程通讯并共享一个复杂的数据结构,你需要一种机制让他们相互牵制并正确执 行。 为这个目的,Java用一种叫监视器(monitor)的机制实现了进程间的异步执行。可以将监视器看作是一个很小的盒子,它只能容纳一个线程。一个线程进入一个监视器,所有其他 线程必须等到第一个线程退出监视器后才能进入。这个监视器可以设计成保护共享的数据不被多个线程同时操作。
大多数多线程系统将这个监视器设计成对象,Java提出了一种更清晰的解决方案。没有Monitor类;每个对象通过将他们的成员函数定义成synchronized来定义自己的显式监视器,一个线程执行在一个synchronized函数里,其他任何线程都不能 调用同一个对象的synchronized函数。
7.1.4 消息
当程序被分成几个逻辑线程,你必须清晰的知道这些线程之间应怎样相互通讯。Java 提出了wait和notify等功能来使线程之间相互交谈。一个线程可以进入 某一个对象的synchronized 函数进入等待状态,直到其他线程显式地将它唤醒。可以有多个线程进入同一个函数并等待同一个唤醒消息。
7.2 Java线程例子
7.2.1 显式定义线程
在我们的单线程应用程序里,我们并没有看见线程,因为Java能自动创建和控制你的线程。如果你使用了理解Java语言的浏览器,你就已经看到使用多线程的Java程序了。你也许注意到两个小程序可以同时运行,或在你移动滚动条时小程序继续执行。这并不是表明小程序是多线程的,但说明这个浏览器是多线程的。多线程应用程序(或applet)可以 使 用 好几个执行上下文来完成它们的工作。多线程利用了很多任务包含单独的可分离的子任务 的特点。每一个线程完成一个子任务。
但是,每一个线程完成子任务时还是顺序执行的。一个多线程程序允许各个线程尽快执行完它们。这种特点会有更好的实时输入反应。
7.2.2 多线程例子
下面这个例子创建了三个单独的线程,它们分别打印自己的“Hello World":
//Define our simple threads.They will pause for a short time
//and then print out their names and delay times
public class TestThread extends Thread {
private String whoami; //定义其属性
private int delay;
//Our constructor to store the name (whoami)
//and time to sleep (delay)
public TestThread(String s, int d) {//定义了一个线程的构造函数
whoami = s;
delay = d;
}
//Run - the thread method similar to main()
//When run is finished, the thread dies.
//Run is called from the start() method of Thread
public void run() {//运行线程
//Try to sleep for the specified time
try {
sleep(delay); //让线程进行睡眠
}
catch(InterruptedException e) {
}
//Now print out our name
System.out.println("Hello World!"+whoami+""+delay);
}
}
/** * Multimtest. A simple multithread thest program */
public static void main(String[] args) {
TestThread t1,t2,t3;
//Create our test threads
t1 = new TestThread("Thread1",(int)(Math.readom()*2000)); //实例化线程
t2 = new TestThread("Thread2",(int)(Math.readom()*2000));
t3 = new TestThread("Thread3",(int)(Math.readom()*2000));
//Start each of the threads
t1.start(); //启动线程
t2.start();
t3.start();
}
}
7.2.3 启动一个线程
程序启动时总是调用main()函数,因此main()是我们创建和启动线程的地方:
t1 = new TestThread("Thread1",(int)(Math.readom()*2000));
这一行创建了一个新的线程。后面的两个参数传递了线程的名称和线程在打印信息前的延 时时间。因为我们直接控制线程,我们必须直接启动它:
t1.start();
7.2.4 操作线程
如果创建线程正常,t1应包含一个有效的执行线程。我们在线程的run()函数里控制线程。 当进 入run()函数,我们便可执行里面的任何程序。run()好象main()一样。 当执行完,这个线程也就结束了。在这个例子里,我们试着延迟一个随机的时间(通过参数传递);
sleep(delay);
sleep()函数只是简单地告诉线程休息多少个毫秒时间。如果你想推迟一个线程的执行, 应使用sleep()函数。 当线程睡眠是sleep()并不占用系统资源。其它线程可继续工作。当延迟时间完毕,它将打印"Hello World"和线程名称及延迟时间。
7.2.5 暂停一个线程
我们经常需要挂起一个线程而不指定多少时间。例如,如果你创建了一个含有动画线程的小程序。也许你让用户暂停动画至到他们想恢复为止。你并不想将动画线程仍调,但想让它停止。象这种类似的线程你可用suspend()函数来控制:
t1.suspend();
这个函数并不永久地停止了线程,你还可用resume()函数重新激活线程:
t1.resume();
7.2.6 停止一个线程
线程的最后一个控制是停止函数stop()。 我们用它来停止线程的执行:
t1.stop();
注意:这并没有消灭这个线程,但它停止了线程的执行。并且这个线程不能用t1.start()重新启动。在我们的例子里,我们从来不用显式地停止一个线程。我们只简单地让它执行完而已。很多复杂的线程例子将需要我们控制每一个线程。在这种情况下会使用到stop()函数。如果需要,你可以测试你的线程是否被激活。一个线程已经启动而且没有停止被认为是激活的。t1.isAlive() 如果t1是激活的,这个函数将返回true.
7.2.7 动画例子
下面是一个包含动画线程的applet例子:
import java.awt.*;
import java.awt.image.ImageProducer;//抽象类Image是表示图形图像的所有类的超类。ImageProducer可为Image生成图像数据的对象的接口。每幅图像都包含一个用于在需要时重构图像的 ImageProducer(例如在缩放 Image 的新大小时,或者在请求 Image 的宽度或高度时)。
import java.applet.Applet;
public class atest3 extends Applet implements Runnable {
Image images[];
MediaTracker tracker;
int index = 0;
Thread animator;
int maxWidth,maxHeight;
//Our off-screen components for double buffering.
Image offScrImage;
Graphics offScrGC;
//Can we paint yes?
boolean loaded = false;
//Initialize the applet. Set our size and load the images
public void init() {
//Set up our image monitor
tracker = new MediaTracker(this);
//Set the size and width of our applet
maxWidth = 100;
maxHeight =100;
images = new Image[10];
//Set up the double-buffer and resize our applet
try {
offScrImage = createImage(maxWidth,maxHeight);
offScrGC = offScrImage.getGraphics();
offScrGC.setColor(Color.lightGray);
offScrGC.fillRect(0,0,maxWidth,maxHeight);
resize(maxWidth,maxHeight);
}
catch (Exception e) {
e.printStackTrace();
}
//load the animation images into an array
for (int i=0;i<10;i++) {
String imageFile = new String ("images/Duke/T" +String.valueOf(i+1) +".gif");
images[i] = getImage(getDocumentBase(),imageFile):
//Register this image with the tracker tracker.addImage(images[i],i);
}
try {
//Use tracker to make sure all the images are loaded
tracker.waitForAll();
}
catch (InterruptedException e) {
}
loaded = true;
}
//Paint the current frame.
public void paint (Graphics g) {
if (loaded) {
g.drawImage(offScrImage,0,0,this);
}
}
//Start ,setup our first image
public void start() {
if (tracker.checkID (index)) {
offScrGC.drawImage (images[index],0,0,this);
}
animator = new Thread(this);
animator.start();
}
//Run,do the animation work here.
//Grab an image, pause ,grab the next...
public void run() {
//Get the id of the current thread
Thread me = Thread.currentThread();
//If our animator thread exist,and is the current thread...
while ((animatr!= null) && (animator==me)) {
if ( tracker.checkID (index)) {
//Clear the background and get the next image
offScrGC.fillRect(0,0,100,100);
offScrGCdrawImage(images[index],0,0,this);
index++;
//Loop back to the beginning and keep going
if (index>= images.length) {
index = 0;
}
}
//Delay here so animation looks normal
try {
animator.sleep(200);
}
catch (InterruptedException e) {
//
}
//Draw the next frame
repaint();
}
}
}
7.3 多线程之间的通讯
7.3.1 生产者和消费者
多线程的一个重要特点是它们之间可以互相通讯。你可以设计线程使用公用对象,每个线程都可以独立操作公用对象。典型的线程间通讯建立在生产者和消费者模型上:一个线程产生输出;另一个线程使用输入。
buffer让我们创建一个简单的"Alphabet Soup"生产者和相应的消费者.
7.3.2 生 产 者
生产者将从thread类里派生:
class Producer extends Thread {
private Soup soup;
private String alphabet = " ABCDEFGHIJKLMNOPQRSTUVWXYZ";
public Producer(Soup s) {
//Keep our own copy of the shared object
this.soup = s;
}
public void run() {
char c; //Throw 10 letters into the soup
for (int i=0;i<10;i++) {
c = alphabet.charAt((int)(Math.random() *26));
soup.add(c); //print a record of osr addition
System.out.println("Added"+c + "to the soup.");
//wait a bit before we add the next letter
try {
sleep((int)(Math.random() *1000));
}
catch (InterruptedException e) {
//
}
}
}
}
注意我们创建了Soup类的一个实例。生产者用soup.add()函数来建立字符池。
7.3.3 消费者
让我们看看消费者的程序:
publicclass Consumer extends Thread {
private Soup soup;
public Consumer (Soup s) {
//keep our own copy of the shared object
this.soup = s;
}
public void run() {
char c;
//Eat 10 letters from the alphabet soup
for (int I=0 ;i<10;i++) {
//grab one letter
this.c = soup.eat();
//Print out the letter that we retrieved
System.out.println("Ate a letter: " +c); //
try {
sleep((int)(Math.raddom()*2000));
}
catch (InterruptedException e) {
}
` }
}
}
同理,象生产者一样,我们用soup.eat()来处理信息。那么,Soup类到底干什么呢?
7.3.4 监 视
Soup类执行监视两个线程之间传输信息的功能。监视是多线程中不可缺少的一部分,因为它保持了通讯的流畅。让我们看看Soup.java文件:
public class Soup {
private char buffer[] = new char[6];
private int next = 0; //Flags to keep track of our buffer status
private boolean isFull = false;
private boolean isEmpty = true;
public syschronized char eat() {
//We can't eat if there isn't anything in the buffer
while (isEmpty == true) {
try {
wait() ;//we'll exit this when isEmpty turns false
catch (InterruptedException e) {
}
}
//decrement the count,since we're going to eat one letter next--;
//Did we eat the last letter?
if (next== 0) {
isEmpty = true;
}
//We know the buffer can't be full,because we just ate
isFull = false;
notify(); //return the letter to the thread that is eating
return (buffer[next]);
}
//method to add letters to the buffer
public synchronized void add(char c) {
//Wait around until there's room to add another letter
while (isFull == true ) {
try{
wait();//This will exit when isFull turns false
}
catch (InterruptedException e) {
}
} //add the letter to the next available spot
buffer[next]=c; //Change the next available spot
next++; //Are we full;
if (next ==6) {
isFull =true;
}
isEmpty =false;
notify();
}
}
soup类包含两个重要特征:数据成员buffer[]是私有的,功能成员add()和eat()是公有的。
数据私有避免了生产者和消费者直接获得数据。直接访问数据可能造成错误。例如,如果消费者企图从空缓冲区里取出数据,你将得到不必要的异常,否则,你只能锁住进程。同步访问方法避免了破坏一个共享对象。当生产者向soup里加入一个字母时,消费者不能吃字 符,诸如此类。这种同步是维持共享对象完整性的重要方面。notify()函数将唤醒每一个等待线程。等待线程将继续它的访问。
7.3.5 联系起来
现在我们有一个生产者,一个消费者和一个共享对象,怎样实现它们的交互呢? 我们只需要一个简单的控制程序来启动所有的线程并确信每一个线程都是访问的同一个共享对象。下面是控制程序的代码:
SoupTest.java:
class SoupTest {
public static void main(String args[]) {
Soup s = new Soup();
Producer p1 = new Producer(s);
Consumer c1 = new Consumer(s);
p1.start();
c1.start();
}
}
7.3.6 监视生产者
生产者/消费者模型程序经常用来实现远程监视功能,它让消费者看到生产者同用户的交互 或同系统其它部分的交互。 例如,在网络中,一组生产者线程可以在很多工作站上运行。 生 产者可以打印文档,文档打印后,一个标志将保存下来。一个(或多个消费者将保存标志并 在晚上报告白天打印活动的情况。另外,还有例子在一个工作站是分出几个独立的窗口。 一 个窗口用作用户输入(生产者的另一个窗口作出对输入的反应消费者的。
7.4 线程API列表
下面是一个常用的线程类的方法函数列表:
类函数:以下是Thread的静态函数,即可以直接从Thread类调用。
currentThread 返回正在运行的Thread对象yield 停止运行当前线程,让系统运行下一个线 程sleep(int n)让当前线程睡眠n毫秒
对象函数:以下函数必须用Thread的实例对象来调用。
start函数告诉java运行系统为本线程建立一个执行环境,然后调用本线程的run()函数。
run是运行本线程的将要执行的代码,也是Runnable接口的唯一函数。当一个线程初始化后, 由start函数来调用它,一但函数返回,本线程也就终止了。
stop让某线程马上终止,系统将删除本线程的执行环境suspend与stop函数不同,
suspend将线程暂停执行,但系统不破坏线程的执行环境,你可以用resume来恢复本线程的执行
resume恢复被挂起的线程进入运行状态setPriority(int p)给线程设置优先级getPriority返回线程的优先级setName(String name) 给线程设置名称getName取线程的名称。
本章小结:
1.多线程是java语言的重要特点,java语言用Thread类封装了线程的所有操作。
2.线程的接口名为Runnable
3.线程之间同步机制为synchronized关键词;
4.线程之间通讯靠wait与notify消息
展开阅读全文