Java多线程详解 联系客服

发布时间 : 星期三 文章Java多线程详解更新完毕开始阅读baac13ca9e3143323868931c

228 Java面向对象程序设计 当foo()方法被调用时(如,使用X.foo()),调用线程必须获得X类的类锁。 9.5.3 线程间的同步控制

在多线程的程序中,除了要防止资源冲突外,有时还要保证线程的同步。下面通过生产者-消费者模型来说明线程的同步与资源共享的问题。

假设有一个生产者(Producer),一个消费者(Consumer)。生产者产生0~9的整数,将它们存储在仓库(CubbyHole)的对象中并打印出这些数来;消费者从仓库中取出这些整数并将其也打印出来。同时要求生产者产生一个数字,消费者取得一个数字,这就涉及到两个线程的同步问题。

这个问题就可以通过两个线程实现生产者和消费者,它们共享CubbyHole一个对象。如果不加控制就得不到预期的结果。

1. 不同步的设计

首先我们设计用于存储数据的类,该类的定义如下: 程序9.8 CubbyHole.java class CubbyHole{ private int content ;

public synchronized void put(int value){ content = value; }

public synchronized int get(){

return content ; } }

_____________________________________________________________________________▃ CubbyHole类使用一个私有成员变量content用来存放整数,put()方法和get()方法用来设置变量content的值。CubbyHole对象为共享资源,所以用synchronized关键字修饰。当put()方法或get()方法被调用时,线程即获得了对象锁,从而可以避免资源冲突。

这样当Producer对象调用put()方法是,它锁定了该对象,Consumer对象就不能调用get()方法。当put()方法返回时,Producer对象释放了CubbyHole的锁。类似地,当Consumer对象调用CubbyHole的get()方法时,它也锁定该对象,防止Producer对象调用put()方法。

接下来我们看Producer和Consumer的定义,这两个类的定义如下: 程序9.9 Producer.java

public class Producer extends Thread { private CubbyHole cubbyhole; private int number;

public Producer(CubbyHole c, int number) { cubbyhole = c;

this.number = number; }

public void run() {

for (int i = 0; i < 10; i++) { cubbyhole.put(i);

System.out.println(\ try {

sleep((int)(Math.random() * 100)); } catch (InterruptedException e) { } } } }

229 _____________________________________________________________________________▃ Producer类中定义了一个CubbyHole类型的成员变量cubbyhole,它用来存储产生的整数,另一个成员变量number用来记录线程号。这两个变量通过构造方法传递得到。在该类的run()方法中,通过一个循环产生10个整数,每次产生一个整数,调用cubbyhole对象的put()方法将其存入该对象中,同时输出该数。

下面是Consumer类的定义: 程序9.10 Consumer.java

public class Consumer extends Thread { private CubbyHole cubbyhole; private int number;

public Consumer(CubbyHole c, int number) { cubbyhole = c;

this.number = number; }

public void run() { int value = 0;

for (int i = 0; i < 10; i++) { value = cubbyhole.get();

System.out.println(\ } } }

_____________________________________________________________________________▃ 在Consumer类的run()方法中也是一个循环,每次调用cubbyhole的get()方法返回当前存储的整数,然后输出。

下面是主程序,在该程序的main()方法中创建一个CubbyHole对象c,一个Producer对象p1,一个Consumer对象c1,然后启动两个线程。

程序9.11 ProducerConsumerTest.java public class ProducerConsumerTest {

public static void main(String[] args) { CubbyHole c = new CubbyHole(); Producer p1 = new Producer(c, 1); Consumer c1 = new Consumer(c, 1); p1.start(); c1.start(); } }

_____________________________________________________________________________▃ 该程序中对CubbyHole类的设计,尽管使用了synchronized关键字实现了对象锁,但这还不够。程序运行可能出现下面两种情况:

如果生产者的速度比消费者快,那么在消费者来不及取前一个数据之前,生产者又产生了新的数据,于是消费者很可能会跳过前一个数据,这样就会产生下面的结果:

Consumer: 3 Producer: 4 Producer: 5 Consumer: 5 …

反之,如果消费者比生产者快,消费者可能两次取同一个数据,可能产生下面的结果: Producer: 4 Consumer: 4

230 Java面向对象程序设计 Consumer: 4 Producer: 5 …

2. 监视器模型

为了避免上述情况发生,就必须使生产者线程向CubbyHole对象中存储数据与消费者线程从CubbyHole对象中取得数据同步起来。为了达到这一目的,在程序中可以采用监视器(monitor)模型,同时通过调用对象的wait()方法和notify()方法实现同步。

下面是修改后的CubbyHole类的定义: 程序9.12 CubbyHole.java class CubbyHole{ private int content ;

private boolean available=false;

public synchronized void put(int value){ while(available==true){ try{

wait();

}catch(InterruptedException e){} }

content =value; available=true; notifyAll(); }

public synchronized int get(){ while(available==false){ try{

wait();

}catch(InterruptedException e){} }

available=false; notifyAll(); return content; } }

_____________________________________________________________________________▃ 这里有一个boolean型的私有成员变量available用来指示内容是否可取。当available为true时表示数据已经产生还没被取走,当available为false时表示数据已被取走还没有存放新的数据。

当生产者线程进入put()方法时,首先检查available的值,若其为false,才可执行put()方法,若其为true,说明数据还没有被取走,该线程必须等待。因此在put()方法中调用CubbyHole对象的wait()方法等待。调用对象的wait()方法使线程进入等待状态,同时释放对象锁。直到另一个线程对象调用了notify()或notifyAll()方法,该线程才可恢复运行。

类似地,当消费者线程进入get()方法时,也是先检查available的值,若其为true,才可执行get()方法,若其为false,说明还没有数据,该线程必须等待。因此在get()方法中调用CubbyHole对象的wait()方法等待。调用对象的wait()方法使线程进入等待状态,同时释放对象锁。

上述过程就是监视器模型,其中CubbyHole对象为监视器。通过监视器模型可以保证生产者线程和消费者线程同步,结果正确。

程序的运行结果如下:

231 特别注意:wait()、notify()和notifyAll()方法是Object类定义的方法,并且这些方法只能用在synchronized代码段中。它们的定义格式如下:

? public final void wait()

? public final void wait(long timeout)

? public final void wait(long timeout, int nanos) 当前线程必须具有对象监视器的锁,当调用该方法时线程释放监视器的锁。调用这些方法使当前线程进入等待(阻塞)状态,直到另一个线程调用了该对象的notify()方法或notifyAll()方法,该线程重新进入运行状态,恢复执行。

timeout和nanos为等待的时间的毫秒和纳秒,当时间到或其他对象调用了该对象的notify()方法或notifyAll()方法,该线程重新进入运行状态,恢复执行。

wait()的声明抛出了InterruptedException,因此程序中必须捕获或声明抛出该异常。 ? public final void notify() ? public final void notifyAll() 唤醒处于等待该对象锁的一个或所有的线程继续执行,通常使用notifyAll()方法。

在生产者/消费者的例子中,CubbyHole类的put和get方法就是临界区。当生产者修改它时,消费者不能问CubbyHole对象;当消费者取得值时,生产者也不能修改它。

9.6 线程组

所有Java线程都属于某个线程组(thread group)。线程组提供了一个将多个线程组织成一个线程组对象来管理的机制,如可以通过一个方法调用来启动线程组中的所有线程。 9.6.1 创建线程组

线程组是由java.lang包中的ThreadGroup类实现的。它的构造方法如下: ? public ThreadGroup(String name)

? public ThreadGroup(ThreadGroup parent, String name)

name为线程组名,parent为线程组的父线程组,若无该参数则新建线程组的父线程组为当前运行的线程的线程组。

当一个线程被创建时,运行时系统都将其放入一个线程组。创建线程时可以明确指定新建线程属于哪个线程组,若没有明确指定则放入缺省线程组中。一旦线程被指定属于哪个线程组,便不能改变,不能删除。 9.6.2 缺省线程组

如果在创建线程时没有在构造方法中指定所属线程组,运行时系统会自动将该线程放入创建该线程的线程所属的线程组中。那么当我们创建线程时没有指定线程组,它属于哪个线程组呢?

当Java应用程序启动时,Java运行时系统创建一个名main的ThreadGroup对象。除非另外指定,否则所有新建线程都属于main线程组的成员。

在一个线程组内可以创建多个线程,也可以创建其它的线程组。一个程序中的线程组和线程构成一个树型结构,如图9.6所示:

main