Chapter16(My--Java教案)第11章 线 程
本章讨论线程,它允许一个程序同时执行多个任务。
第一节 线 程
1 什么是线程?
一个关于计算机的简化的视图是:它有一个执行计算的处理机、包含处理机所执行的程序的ROM(只读存储器)、包含程序所要操作的数据的RAM(只读存储器)。在这个简化视图中,只能执行一个作业。一个关于最现代计算机比较完整的视图允许计算机在同时执行一个以上的作业。
你不需关心这一点是如何实现的,只需从编程的角度考虑就可以了。如果你要执行一个以上的作业,这类似有一台以上的计算机。在这个模型中,线程或执行上下文,被认为是带有自己...
第11章 线 程
本章讨论线程,它允许一个程序同时执行多个任务。
第一节 线 程
1 什么是线程?
一个关于计算机的简化的视图是:它有一个执行计算的处理机、包含处理机所执行的程序的ROM(只读存储器)、包含程序所要操作的数据的RAM(只读存储器)。在这个简化视图中,只能执行一个作业。一个关于最现代计算机比较完整的视图允许计算机在同时执行一个以上的作业。
你不需关心这一点是如何实现的,只需从编程的角度考虑就可以了。如果你要执行一个以上的作业,这类似有一台以上的计算机。在这个模型中,线程或执行上下文,被认为是带有自己的程序代码和数据的虚拟处理机的封装。java.lang.Thread类允许用户创建并控制他们的线程。
注-在这章中,使用“Thread”时是指java.lang.Thread而使用“thread”时是指执行上下文。
2 线程的三个部分
进程是正在执行的程序。一个或更多的线程构成了一个进程。一个线程由三个主要部分组成:
1) 一个虚拟处理机
2)CPU执行的代码
3)代码操作的数据
代码可以或不可以由多个线程共享,这和数据是独立的。两个线程如果执行同一个类的实例代码,则它们可以共享相同的代码。
数据可以或不可以由多个线程共享,这和代码是独立的。两个线程如果共享对一个公共对象的存取,则它们可以共享相同的数据。
虚拟处理机封装在Thread类的一个实例里。构造线程时,定义其上下文的代码和数据是由传递给它的构造函数的对象指定的。
第二节 Java编程中的线程
1 创建线程
一个Thread类构造函数带有一个参数,它是Runnable的一个实例。一个Runnable是由一个实现了Runnable接口(提供了一个public void run()方法)的类产生的。
例如:
public class ThreadTest
{
public static void main(String args[])
{
Xyz r = new Xyz();
Thread t = new Thread(r);
t.start( );
}
}
class Xyz implements Runnable
{
int i;
public void run()
{
while (true)
{
System.out.println("Hello " + i++);
if (i == 50)
break;
}
}
}
首先,main()方法构造了Xyz类的一个实例r。实例r有它自己的数据,在这里就是整数i。因为实例r是传给Thread的类构造函数的,所以r的整数i就是线程运行时刻所操作的数据。线程总是从它所装载的Runnable实例(在本例中,这个实例就是r。)的run()方法开始运行。
一个多线程编程环境允许创建基于同一个Runnable实例的多个线程。这可以通过以下方法来做到:
Thread t1= new Thread(r);
Thread t2= new Thread(r);
此时,这两个线程共享数据和代码。
总之,线程通过Thread对象的一个实例引用。线程从装入的Runnble实例的run()方法开始执行。线程操作的数据从传递给Thread构造函数的Runnable的特定实例处获得。
2 启动线程
一个新创建的线程并不自动开始运行。你必须调用它的start()方法。例如,你可以发现上例中第7行代码中的命令:
t . start( ) ;
调用start()方法使线程所代
的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。这并不意味着线程就会立即运行。
3 线程调度
一个Thread对象在它的生命周期中会处于各种不同的状态。下图形象地说明了这点:
尽管线程变为可运行的,但它并不立即开始运行。在一个只带有一个处理机的机器上,在一个时刻只能进行一个动作。下节描述了如果有一个以上可运行线程时,如何分配处理机。
在Java中,线程是抢占式的,但并不一定是分时的。
抢占式调度模型是指可能有多个线程是可运行的,但只有一个线程在实际运行。这个线程会一直运行,直至它不再是可运行的,或者另一个具有更高优先级的线程成为可运行的。对于后面一种情形,低优先级线程被高优先级线程抢占了运行的机会。
一个线程可能因为各种原因而不再是可运行的。线程的代码可能执行了一个Thread.sleep()调用,要求这个线程暂停一段固定的时间。这个线程可能在等待访问某个资源,而且在这个资源可访问之前,这个线程无法继续运行。
所有可运行线程根据优先级保存在池中。当一个被阻塞的线程变成可运行时,它会被放回相应的可运行池。优先级最高的非空池中的线程会得到处理机时间(被运行)。
因为Java线程不一定是分时的,所有你必须确保你的代码中的线程会不时地给另外一个线程运行的机会。这可以通过在各种时间间隔中发出sleep()调用来做到。
public class Xyz 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
}
}
}
}
注意:
1)try和catch块的使用。Thread.sleep( )和其它使线程暂停一段时间的方法是可中断的。线程可以调用另外一个线程的interrupt()方法,这将向暂停的线程发出一个InterruptedException。
2)Thread类的sleep()方法对当前线程操作,因此被称作Thread.sleep(x),它是一个静态方法。sleep()的参数指定以毫秒为单位的线程最小休眠时间。除非线程因为中断而提早恢复执行,否则它不会在这段时间之前恢复执行。
Thread类的另一个方法yield( ),可以用来使具有相同优先级的线程获得执行的机会。如果具有相同优先级的其它线程是可运行的,yield()将把调用线程放到可运行池中并使另一个线程运行。如果没有相同优先级的可运行进程,yield()什么都不做。
3)sleep()调用会给较低优先级线程一个运行的机会。yield()方法只会给相同优先级线程一个执行的机会。
第三节 线程的基本控制
1 终止一个线程
当一个线程结束运行并终止时,它就不能再运行了。
可以用一个指示run()方法必须退出的标志来停止一个线程。
public class Xyz implements Runnable
{
private boolean timeToQuit=false;
public void run()
{
while(! timeToQuit)
{
...
}
// clean up before run() ends
}
public void stopRunning()
{
timeToQuit=true;
}
}
public class ControlThread
{
private Runnable r = new Xyz();
private Thread t = new Thread(r);
public void startThread()
{
t.start();
}
public void stopThread()
{
// use specific instance of Xyz
r.stopRunning();
}
}
在一段特定的代码中,可以使用静态Thread方法currentThread()来获取对当前线程的引用,例如:
public class Xyz implements Runnable
{
public void run()
{
while (true)
{
// lots of interesting stuff
// Print name of the current thread
System.out.println( "Thread" +
Thread.currentThread().getName() +
"completed" );
}
}
}
2 测试一个线程
有时线程可处于一个未知的状态。isAlive()方法用来确定一个线程是否仍是活的。活着的线程并不意味着线程正在运行;对于一个已开始运行但还没有完成任务的线程,这个方法返回true。当一个程序调用Thread的start()方法时,在一个新线程调用run()之前有一个时间段(为了初始化)。run()返回后,在JVM清除线程之前有一段时间通过。JVM认为线程立即激活优先于线程调用run(),在线程执行run()期间和run()返回后。在这时间间隔期间,Thread的isAlive()方法返回一个布尔真值。否则,方法返回一个假值。
isAlive()在一个线程需要在第一个线程能够检查其它线程的结果之前等待另一个线程完成其run()方法的情形下证明是有帮助的。实质上,那些需要等待的线程输入一个while循环。当isAlive()为其它线程返回真值时,等待线程调用sleep(long millis)周期性地休眠 (避免浪费更多的CPU循环)。一旦isAlive()返回假值,等待线程便检查其它线程的结果。
3 延迟线程
存在可以使线程暂停执行的机制。也可以恢复运行,就好象什么也每发生过一样,线程看上去就象在很慢地执行一条指令。
1)sleep()
sleep()方法是使线程停止一段时间的方法。在sleep时间间隔期满后,线程不一定立即恢复执行。这是因为在那个时刻,其它线程可能正在运行而且没有被调度为放弃执行,除非
a)“醒来”的线程具有更高的优先级
b)正在运行的线程因为其它原因而阻塞
2)join()
join()方法使当前线程停下来等待,直至另一个调用join方法的线程终止。例如:
public void doTask()
{
TimerThread tt = new TimerThread (100);
tt.start ();
...
// Do stuff in parallel with the other thread for a while
...
// Wait here for the timer thread to finish
try {
tt.join ();
} catch (InterruptedException e)
{
// tt came back early
}
...
// Now continue in this thread
...
}
可以带有一个以毫秒为单位的时间值来调用join方法,例如:
void join (long timeout);
其中join()方法会挂起当前线程。挂起的时间或者为timeout毫秒,或者挂起当前线程直至它所调用的线程终止。
第四节 创建线程的其它方法
到目前为止,你已经知道如何用实现了Runnable的分离类来创建线程上下文。事实上,这不是唯一的方法。Thread类自身实现了Runnable接口,所以可以通过扩展Thread类而不是实现Runnable来创建线程。
public class MyThread extends Thread
{
public void run()
{
while (running)
{
// do lots of interesting stuff
try {
sleep(100);
} catch (InterruptedException e)
{
// sleep interrupted
}
}
}
public static void main(String args[])
{
Thread t = new MyThread();
t.start();
}
}
例子
为了示范sleep(long millis),写了一个CalcPI1应用程序。这个应用程序开始了一个新线程便于用一个数学运算法则计算数学常量pi的值。当新线程计算时,开始线程通过调用sleep(long millis)中止10毫秒。在开始线程醒后,它将打印pi的值,其中新线程存贮在变量pi中。
CalcPI1.java
class CalcPI1
{
public static void main (String [] args)
{
MyThread mt = new MyThread ();
mt.start ();
try
{
Thread.sleep (10);
}
catch (InterruptedException e) {}
System.out.println ("pi = " + mt.pi);
}
}
class MyThread extends Thread
{
boolean negative = true;
double pi;
public void run ()
{
for (int i = 3; i < 100000; i += 2)
{
if (negative)
pi -= (1.0 / i);
else
pi += (1.0 / i);
negative = !negative;
}
pi += 1.0;
pi *= 4.0;
System.out.println ("Finished calculating PI");
}
}
如果你运行这个程序,你将看到输出如下:pi = -0.2146197014017295
什么输出不正确呢?因为开始线程醒得太快了。在新线程刚开始计算pi时,开始线程就醒过来读取pi的当前值并打印其值。可以通过将10毫秒延迟增加为更长的值来进行补偿。
CalcPI2.java
class CalcPI2
{
public static void main (String [] args)
{
MyThread mt = new MyThread ();
mt.start ();
while (mt.isAlive ())
try
{
Thread.sleep (10);
}
catch (InterruptedException e){}
System.out.println ("pi = " + mt.pi);
}
}
class MyThread extends Thread
{
boolean negative = true;
double pi;
public void run ()
{
for (int i = 3; i < 100000; i += 2)
{
if (negative)
pi -= (1.0 / i);
else
pi += (1.0 / i);
negative = !negative;
}
pi += 1.0;
pi *= 4.0;
System.out.println ("Finished calculating PI");
}
}
CalcPI2的开始线程在10毫秒时间间隔休眠,直到mt.isAlive ()返回假值。当那些发生时,开始线程从它的while循环中退出并打印pi的内容。如果你运行这个程序,你将看到如下的输出(但不一定一样):pi = 3.1415726535897894
因为while循环/isAlive()方法/sleep()方法技术证明是有用的,Sun将其打包进三个方法组成的一个组合里:join(),join(long millis)和join(long millis, int nanos)。
CalcPI3.java
class CalcPI3
{
public static void main (String [] args)
{
MyThread mt = new MyThread ();
mt.start ();
try
{
mt.join ();
}
catch (InterruptedException e){
}
System.out.println ("pi = " + mt.pi);
}
}
class MyThread extends Thread
{
boolean negative = true;
double pi;
public void run ()
{
for (int i = 3; i < 100000; i += 2)
{
if (negative)
pi -= (1.0 / i);
else
pi += (1.0 / i);
negative = !negative;
}
pi += 1.0;
pi *= 4.0;
System.out.println ("Finished calculating PI");
}
}
CalcPI3的开始线程等待与MyThread对象有关被mt引用的线程结束。接着开始线程打印pi的值,其值与CalcPI2的输出一样。
1 使用那种方法
实现Runnable的优点
· 从面向对象的角度来看,Thread类是一个虚拟处理机严格的封装,因此只有当处理机模型修改或扩展时,才应该继承类。正因为这个原因和区别一个正在运行的线程的处理机、代码和数据部分的意义,本教程采用了这种方法。
· 由于Java技术只允许单一继承,所以如果你已经继承了Thread,你就不能再继承其它任何类,例如Applet。在某些情况下,这会使你只能采用实现Runnable的方法。
· 因为有时你必须实现Runnable,所以你可能喜欢保持一致,并总是使用这种方法。
继承Thread的优点
· 当一个run()方法体现在继承Thread类的类中,用this指向实际控制运行的Thread实例。因此,代码不再需要使用如下控制:
Thread.currentThread().join();
而可以简单地用:
join();
因为代码简单了一些,许多Java编程语言的程序员使用扩展Thread的机制。
注意:如果你采用这种方法,在你的代码生命周期的后期,单继承模型可能会给你带来困难。
第五节 使用Java技术中的synchronized
本节讨论关键字synchronized的使用。它提供Java编程语言一种机制,允许程序员控制共享数据的线程。
1 问题
想象一个表示栈的类。这个类最初可能象下面那样:
public class MyStack
{
int idx = 0;
char [] data = new char[6];
public void push(char c)
{
data[idx] = c;
idx++;
}
public char pop()
{
idx--;
return data[idx];
}
}
现在想象两个线程都有对这个类里的一个单一实例的引用。一个线程将数据推入栈,而另一个线程,或多或少独立地,将数据弹出栈。通常看来,数据将会正确地被加入或移走。然而,这存在着潜在的问题。
假设线程a正在添加字符,而线程b正在移走字符。线程a已经放入了一个字符,但还没有使下标加1。因为某个原因,这个线程被剥夺(运行的机会)。这时,对象所表示的数据模型是不一致的。
buffer |p|q|r| | | |
idx = 2 ^
特别地,一致性会要求idx=3,或者还没有添加字符。
如果线程a恢复运行,那就可能不造成破坏,但假设线程b正等待移走一个字符。在线程a等待另一个运行的机会时,线程b正在等待移走一个字符的机会。
pop()方法所指向的条目存在不一致的数据,然而pop方法要将下标值减1。
buffer |p|q|r| | | |
idx = 1 ^
这实际上将忽略了字符“r”。此后,它将返回字符“q”。至此,从其行为来看,就好象没有推入字母“r”。现在看一看如果线程a继续运行,会发生什么。
线程a从上次中断的地方开始运行,即在push()方法中,它将使下标值加1。现在你可以看到:
buffer |p|q|r| | | |
idx = 2 ^
注意这个配置隐含了:“q”是有效的,而含有“r”的单元是下一个空单元。
这是一个当多线程共享数据时会经常发生的问题的一个简单范例。需要有机制来保证共享数据在任何线程使用它完成某一特定任务之前是一致的。
2 对象锁标志
在Java技术中,每个对象都有一个和它相关联的标志。这个标志可以被认为是“锁标志”。 synchronized关键字使能和这个标志的交互,即允许独占地存取对象。看一看下面修改过的代码片断:
public void push(char c)
{
synchronized(this)
{
data[idx] = c;
idx++;
}
}
当线程运行到synchronized语句,它检查作为参数传递的对象,并在继续执行之前试图从对象获得锁标志。对象锁标志:
意识到它自身并没有保护数据是很重要的。因为如果同一个对象的pop()方法没有受到synchronized的影响,且pop()是由另一个线程调用的,那么仍然存在破坏data的一致性的危险。如果要使锁有效,所有存取共享数据的方法必须在同一把锁上同步。
下图显示了如果pop()受到synchronized的影响,且另一个线程在原线程持有那个对象的锁时试图执行pop()方法时所发生的事情:
当线程试图执行synchronized(this)语句时,它试图从this对象获取锁标志。由于得不到标志,所以线程不能继续运行。然后,线程加入到与那个对象锁相关联的等待线程池中。当标志返回给对象时,某个等待这个标志的线程将得到这把锁并继续运行。
3 释放锁标志
由于等待一个对象的锁标志的线程在得到标志之前不能恢复运行,所以让持有锁标志的线程在不再需要的时候返回标志是很重要的。
锁标志将自动返回给它的对象。持有锁标志的线程执行到synchronized()代码块末尾时将释放锁。Java技术特别注意了保证即使出现中断或异常而使得执行流跳出synchronized()代码块,锁也会自动返回。此外,如果一个线程对同一个对象两次发出synchronized调用,则在跳出最外层的块时,标志会正确地释放,而最内层的将被忽略。
这些规则使得与其它系统中的等价功能相比,管理同步块的使用简单了很多。
4 synchronized--放在一起
正如所暗示的那样,只有当所有对易碎数据的存取位于同步块内,synchronized()才会发生作用。
所有由synchronized块保护的易碎数据应当标记为private。考虑来自对象的易碎部分的数据的可存取性。如果它们不被标记为private,则它们可以由位于类定义之外的代码存取。这样,你必须确信其他程序员不会省略必需的保护。
一个方法,如果它全部属于与这个实例同步的块,它可以把synchronized关键字放到它的头部。下面两段代码是等价的:
public void push(char c)
{
synchronized(this) {
:
}
}
public synchronized void push(char c)
{
:
}
5 死锁
如果程序中有多个线程竞争多个资源,就可能会产生死锁。当一个线程等待由另一个线程持有的锁,而后者正在等待已被第一个线程持有的锁时,就会发生死锁。在这种情况下,除非另一个已经执行到synchronized块的末尾,否则没有一个线程能继续执行。由于没有一个线程能继续执行,所以没有一个线程能执行到块的末尾。
Java技术不监测也不试图避免这种情况。因而保证不发生死锁就成了程序员的责任。避免死锁的一个通用的经验法则是:决定获取锁的次序并始终遵照这个次序。按照与获取相反的次序释放锁。
第六节 线程交互-wait()和notify()
经常创建不同的线程来执行不相关的任务。然而,有时它们所执行的任务是有某种联系的,为此必须编写使它们交互的程序。
1 wait()和notify()
java.lang.Object类中提供了两个用于线程通信的方法:wait()和notify()。
如果线程对一个同步对象x发出一个wait()调用,该线程会暂停执行,直到另一个线程对同一个同步对象x也发出一个wait()调用。
为了让线程对一个对象调用wait()或notify(),线程必须锁定那个特定的对象。也就是说,只能在它们被调用的实例的同步块内使用wait()和notify()。对于这个实例来说,需要一个以synchronized(cab)开始的块来允许执行cab.wait()和cab.notify()调用。
关于池
当线程执行包含对一个特定对象执行wait()调用的同步代码时,那个线程被放到与那个对象相关的等待池中。此外,调用wait()的线程自动释放对象的锁标志。可以调用不同的wait():
wait() 或 wait(long timeout);
对一个特定对象执行notify()调用时,将从对象的等待池中移走一个任意的线程,并放到锁池中,那里的对象一直在等待,直到可以获得对象的锁标记。 notifyAll()方法将从等待池中移走所有等待那个对象的线程并放到锁池中。只有锁池中的线程能获取对象的锁标记,锁标记允许线程从上次因调用wait()而中断的地方开始继续运行。
在许多实现了wait()/notify()机制的系统中,醒来的线程必定是那个等待时间最长的线程。然而,在Java技术中,并不保证这点。
注意,不管是否有线程在等待,都可以调用notify()。如果对一个对象调用notify()方法,而在这个对象的锁标记等待池中并没有阻塞的线程,那么notify()调用将不起任何作用。对notify()的调用不会被存储。
2 同步的监视模型
协调两个需要存取公共数据的线程可能会变得非常复杂。你必须非常小心,以保证可能有另一个线程存取数据时,共享数据的状态是一致的。因为线程不能在其他线程在等待这把锁的时候释放合适的锁,所以你必须保证你的程序不发生死锁,
3 放在一起
下面将给出一个线程交互的实例,它说明了如何使用wait()和notify()方法来解决一个经典的生产者-消费者问题。
我们先看一下栈对象的大致情况和要存取栈的线程的细节。然后再看一下栈的详情,以及 于栈的状态来保护栈数据和实现线程通信的机制。
实例中的栈类称为SyncStack,用来与核心java.util.Stack相区别,它提供了如下公共的API:
public synchronized void push(char c);
public synchronized char pop();
生产者线程运行如下方法:
public void run()
{
char c;
for (int i = 0; i < 200; i++)
{
c = (char)(Math.random() * 26 + 'A');
theStack.push(c);
System.out.println("Producer" + num + ": " + c);
try {
Thread.sleep((int)(Math.random() * 300));
} catch (InterruptedException e) { }
}
}
这将产生200个随机的大写字母并将其推入栈中,每个推入操作之间有0到300毫秒的随机延迟。每个被推入的字符将显示到控制台上,同时还显示正在执行的生产者线程的标识。
消费者线程运行如下方法:
public void run()
{
char c;
for (int i = 0; i < 200; i++)
{
c = theStack.pop();
System.out.println(" Consumer" + num + ": " + c);
try {
Thread.sleep((int)(Math.random() * 300));
} catch (InterruptedException e) { }
}
}
上面这个程序从栈中取出200个字符,每两个取出操作的尝试之间有0到300毫秒的随机延迟。每个被弹出的字符将显示在控制台上,同时还显示正在执行的消费者线程的标识。
现在考虑栈类的构造。你将使用Vector类创建一个栈,它看上去有无限大的空间。按照这种设计,你的线程只要在栈是否为空的基础上进行通信即可。
SyncStack类
一个新构造的SyncStack对象的缓冲应当为空。下面这段代码用来构造你的类:
public class SyncStack
{
private Vector buffer = new Vector(400,200);
public synchronized char pop()
{ }
public synchronized void push(char c)
{ }
}
现在考虑push()和pop()方法。为了保护共享缓冲,它们必须均为synchronized。此外,如果要执行pop()方法时栈为空,则正在执行的线程必须等待。若执行push()方法后栈不再为空,正在等待的线程将会得到
。
pop()方法如下:
public synchronized char pop()
{
char c;
while (buffer.size() == 0)
{
try {
this.wait();
} catch (InterruptedException e) { }
}
c = ((Character)buffer.remove(buffer.size()-1)).charValue();
return c;
}
注意这里显式地调用了栈对象的wait(),这说明了如何对一个特定对象进行同步。如果栈为空,则不会弹出任何数据,所以一个线程必须等到栈不再为空时才能弹出数据。
由于一个interrupt()的调用可能结束线程的等待阶段,所以wait()调用被放在一个try/catch块中。对于本例,wait()还必须放在一个循环中。如果wait()被中断,而栈仍为空,则线程必须继续等待。
栈的pop()方法为synchronized是出于两个原因。首先,将字符从栈中弹出影响了共享数据buffer。其次,this.wait()的调用必须位于关于栈对象的一个同步块中,这个块由this表示。
你将看到push()方法如何使用this.notify()方法将一个线程从栈对象的等待池中释放出来。一旦线程被释放并可随后再次获得栈的锁,该线程就可以继续执行pop()完成从栈缓冲区中移走字符任务的代码。
需要考虑的另一点是错误检查。你可能已经注意到没有显式的代码来保证栈不发生下溢。这不是必需的,因为从栈中移走字符的唯一方法是通过pop()方法,而这个方法导致正在执行的线程在没有字符的时候会进入wait()状态。因此,错误检查不是必要的。push()在影响共享缓冲方面与此类似,因此也必须被同步。此外,由于push()将一个字符加入缓冲区,所以由它负责通知正在等待非空栈的线程。这个通知的完成与栈对象有关。
push()方法如下:
public synchronized void push(char c)
{
this.notify();
Character charObj = new Character(c);
buffer.addElement(charObj);
}
对this.notify()的调用将释放一个因栈空而调用wait()的单个线程。在共享数据发生真正的改变之前调用notify()不会产生任何结果。只有退出该synchronized块后,才会释放对象的锁,所以当栈数据在被改变时,正在等待锁的线程不会获得这个锁。
4 SyncStack范例
现在,生产者、消费者和栈代码必须组装成一个完整的类。还需要一个测试工具将这些代码集成为一体。特别要注意,SyncTest是如何只创建一个由所有线程共享的栈对象的。
SyncTest.java :
package mod14;
public class SyncTest
{
public static void main(String args[])
{
SyncStack stack = new SyncStack();
Producer p1 = new Producer(stack);
Thread prodT1= new Thread(p1);
prodT1.start();
Producer p2 = new Producer(stack);
Thread prodT2= new Thread(p2);
prodT2.start();
Consumer c1 = new Consumer(stack);
Thread consT1 = new Thread(c1);
consT1.start();
Consumer c2 = new Consumer(stack);
Thread consT2 = new Thread(c2);
constT2.start();
}
}
Producer.java :
package mod14;
public class Producer implements Runnable
{
private SyncStack theStack;
private int num;
private static int counter = 1;
public Producer (SyncStack s)
{
theStack = s;
num = counter++;
}
public void run()
{
char c;
for (int i = 0; i < 200; i++)
{
c = (char)(Math.random() * 26 + `A');
theStack.push(c);
System.out.println("Producer" + num + ": " + c);
try {
Thread.sleep((int)(Math.random() * 300));
} catch (InterruptedException e) { }
}
}
}
Consumer.java :
package mod14;
public class Consumer implements Runnable
{
private SyncStack theStack;
private int num;
private static int counter = 1;
public Consumer (SyncStack s)
{
theStack = s;
num = counter++;
}
public void run()
{
char c;
for (int i=0; i < 200; i++)
{
c = theStack.pop();
System.out.println("Consumer" + num + ": " + c);
try {
Thread.sleep((int)(Math.random() * 300));
} catch (InterruptedException e) { }
}
}
}
SyncStack.java :
package mod14;
import java.util.Vector;
public class SyncStack
{
private Vector buffer = new Vector(400,200);
public synchronized char pop()
{
char c;
while (buffer.size() == 0)
{
try {
this.wait();
} catch (InterruptedException e) { }
}
c = ((Character)buffer.remove(buffer.size()- 1).charValue();
return c;
}
public synchronized void push(char c)
{
this.notify();
Character charObj = new Character(c);
buffer.addelement(charObj);
}
}
运行javamodB.SyncTest的输出如下。请注意每次运行线程代码时,结果都会有所不同。
Producer2: F
Consumer1: F
Producer2: K
Consumer2: K
Producer2: T
Producer1: N
Producer1: V
Consumer2: V
Consumer1: N
Producer2: V
Producer2: U
Consumer2: U
Consumer2: V
Producer1: F
Consumer1: F
Producer2: M
Consumer2: M
Consumer2: T
――――――――――――――――――――――――――――――――――――
本文档为【Chapter16(My--Java教案)】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑,
图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。
本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。
网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。