线程与线程类
9.1.1 线程的概念
线程的概念来源于计算机的操作系统的进程的概念。进程是一个程序关于某个数据集的一次运行。也就是说,进程是运行中的程序,是程序的一次运行活动。
线程和进程的相似之处在于,线程和运行的程序都是单个顺序控制流。有些教材将线程称为轻量级进程(light weight process)。线程被看作是轻量级进程是因为它运行在一个程序的上下文内,并利用分配给程序的资源和环境。
作为单个顺序控制流,线程必须在运行的程序中得到自己运行的资源,如必须有自己的执行栈和程序计数器。线程内运行的代码只能在该上下文内。因此还有些教程将执行上下文(execution context)作为线程的同义词。
所有的程序员都熟悉顺序程序的编写,如我们编写的名称排序和求素数的程序就是顺序程序。顺序程序都有开始、执行序列和结束,在程序执行的任何时刻,只有一个执行点。线程(thread)则是进程中的一个单个的顺序控制流。单线程的概念很简单,如图9.1所示。
多线程(multi-thread)是指在单个的程序内可以同时运行多个不同的线程完成不同的任务,图9.2说明了一个程序中同时有两个线程运行。
图9.1 单线程程序示意图 图9.2 多线程程序示意图
有些程序中需要多个控制流并行执行。例如,
for(int i = 0; i < 100; i++)
System.out.println("Runner A = " + i);
for(int j = 0; j < 100; j++ )
System.out.println("Runner B = "+j);
上面的代码段中,在只支持单线程的语言中,前一个循环不执行完不可能执行第二个循环。要使两个循环同时执行,需要编写多线程的程序。
很多应用程序是用多线程实现的,如Hot Java Web浏览器就是多线程应用的例子。在Hot Java 浏览器中,你可以一边滚动屏幕,一边下载Applet或图像,可以同时播放动画和声音等。
9.1.2 Thread类和Runnable接口
多线程是一个程序中可以有多段代码同时运行,那么这些代码写在哪里,如何创建线程对象呢?
首先,我们来看Java语言实现多线程编程的类和接口。在java.lang包中定义了Runnable接口和Thread类。
Runnable接口中只定义了一个方法,它的格式为:
- public abstract void run()
这个方法要由实现了Runnable接口的类实现。Runnable对象称为可运行对象,一个线程的运行就是执行该对象的run()方法。
Thread类实现了Runnable接口,因此Thread对象也是可运行对象。同时Thread类也是线程类,该类的构造方法如下:
- public Thread()
- public Thread(Runnable target)
- public Thread(String name)
- public Thread(Runnable target, String name)
- public Thread(ThreadGroup group, Runnable target)
- public Thread(ThreadGroup group, String name)
- public Thread(ThreadGroup group, Runnable target, String name)
target为线程运行的目标对象,即线程调用start()方法启动后运行那个对象的run()方法,该对象的类型为Runnable,若没有指定目标对象,则以当前类对象为目标对象;name为线程名,group指定线程属于哪个线程组(有关线程组的概念请参考9.6节)。
Thread类的常用方法有:
- public static Thread currentThread() 返回当前正在执行的线程对象的引用。
- public void setName(String name) 设置线程名。
- public String getName() 返回线程名。
- public static void sleep(long millis) throws InterruptedException
- public static void sleep(long millis, int nanos) throws InterruptedException
使当前正在执行的线程暂时停止执行指定的毫秒时间。指定时间过后,线程继续执行。该方法抛出InterruptedException异常,必须捕获。
- public void run() 线程的线程体。
- public void start() 由JVM调用线程的run()方法,启动线程开始执行。
- public void setDaemon(boolean on) 设置线程为Daemon线程。
- public boolean isDaemon() 返回线程是否为Daemon线程。
- public static void yield() 使当前执行的线程暂停执行,允许其他线程执行。
- public ThreadGroup getThreadGroup() 返回该线程所属的线程组对象。
- public void interrupt() 中断当前线程。
- public boolean isAlive() 返回指定线程是否处于活动状态。
9.2 线程的创建
本节介绍如何创建和运行线程的两种方法。线程运行的代码就是实现了Runnable接口的类的run()方法或者是Thread类的子类的run()方法,因此构造线程体就有两种方法:
- 继承Thread类并覆盖它的run()方法;
- 实现Runnable接口并实现它的run()方法。
9.2.1 继承Thread类创建线程
通过继承Thread类,并覆盖run()方法,这时就可以用该类的实例作为线程的目标对象。下面的程序定义了SimpleThread类,它继承了Thread类并覆盖了run()方法。
程序9.1 SimpleThread.java
public class SimpleThread extends Thread{
public SimpleThread(String str){
super(str);
}
public void run(){
for(int i=0; i<100; i++){
System.out.println(getName()+" = "+ i);
try{
sleep((int)(Math.random()*100));
}catch(InterruptedException e){}
}
System.out.println(getName()+ " DONE");
}
}
_____________________________________________________________________________▃
SimpleThread类继承了Thread类,并覆盖了run()方法,该方法就是线程体。
程序9.2 ThreadTest.java
public class ThreadTest{
public static void main(String args[]){
Thread t1 = new SimpleThread("Runner A");
Thread t2 = new SimpleThread("Runner B");
t1.start();
t2.start();
}
}
_____________________________________________________________________________▃
在ThreadTest类的main()方法中创建了两个SimpleThread类的线程对象并调用线程类的start()方法启动线程。构造线程时没有指定目标对象,所以线程启动后执行本类的run()方法。
注意,实际上ThreadTest程序中有三个线程同时运行。请试着将下段代码加到main()方法中,分析程序运行结果。
for(int i=0; i<100; i++){
System.out.println(Thread.currentThread().getName()+"="+ i);
try{
Thread.sleep((int)(Math.random()*500));
}catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+ " DONE");
}
从上述代码执行结果可以看到,在应用程序的main()方法启动时,JVM就创建一个主线程,在主线程中可以创建其他线程。
再看下面的程序:
程序9.3 MainThreadDemo.java
public class MainThreadDemo{
public static void main(String args[]){
Thread t = Thread.currentThread();
t.setName("MyThread");
System.out.println(t);
System.out.println(t.getName());
System.out.println(t.getThreadGroup().getName());
}
}
_____________________________________________________________________________▃
该程序输出结果为:
Thread[MyThread, 5, main]
MyThread
main
上述程序在main()方法中声明了一个Thread对象t,然后调用Thread类的静态方法currentThread()获得当前线程对象。然后重新设置该线程对象的名称,最后输出线程对象、线程组对象名和线程对象名。
9.2.2 实现Runnable接口创建线程
可以定义一个类实现Runnable接口,然后将该类对象作为线程的目标对象。实现Runnable接口就是实现run()方法。
下面程序通过实现Runnable接口构造线程体。
程序9.4 ThreadTest.java
class T1 implements Runnable{
public void run(){
for(int i=0;i<15;i++)
System.out.println("Runner A="+i);
}
}
class T2 implements Runnable{
public void run(){
for(int j=0;j<15;j++)
System.out.println("Runner B="+j);
}
}
public class ThreadTest{
public static void main(String args[]){
Thread t1=new Thread(new T1(),"Thread A");
Thread t2=new Thread(new T2(),"Thread B");
t1.start();
t2.start();
}
}
_____________________________________________________________________________▃
下面是一个小应用程序,利用线程对象在其中显示当前时间。
程序9.5 ThreadTest.java
//<applet code="ClockDemo.class" height="200" width="300">
//</applet>
import java.awt.*;
import java.util.*;
import javax.swing.*;
import java.text.DateFormat;
public class ClockDemo extends JApplet{
private Thread clockThread = null;
private ClockPanel cp=new ClockPanel();
public void init(){
getContentPane().add(cp);
}
public void start() {
if (clockThread == null) {
clockThread = new Thread(cp, "Clock");
clockThread.start();
}
}
public void stop() {
clockThread = null;
}
}
class ClockPanel extends JPanel implements Runnable{
public void paintComponent(Graphics g) {
super.paintComponent(g);
Calendar cal = Calendar.getInstance();
Date date = cal.getTime();
DateFormat dateFormatter = DateFormat.getTimeInstance();
g.setColor(Color.BLUE);
g.setFont(new Font("TimesNewRoman",Font.BOLD,36));
g.drawString(dateFormatter.format(date), 50, 50);
}
public void run() {
while (true) {
repaint();
try {
Thread.sleep(1000);
} catch (InterruptedException e){ }
}
}
}
_____________________________________________________________________________▃
该小应用程序的运行结果如图9.3所示:
图9.3 ClockDemo的运行结果
9.3 线程的状态与调度
9.3.1 线程的生命周期
线程从创建、运行到结束总是处于下面五个状态之一:新建状态、就绪状态、运行状态、阻塞状态及死亡状态。线程的状态如图9.4所示:
图9.4 线程的五种状态
下面以前面的Java小程序为例说明线程的状态:
1. 新建状态(New Thread)
当Applet启动时调用Applet的start()方法,此时小应用程序就创建一个Thread对象clockThread。
public void start() {
if (clockThread == null) {
clockThread = new Thread(cp, "Clock");
clockThread.start();
}
}
当该语句执行后clockThread就处于新建状态。处于该状态的线程仅仅是空的线程对象,并没有为其分配系统资源。当线程处于该状态,你仅能启动线程,调用任何其他方法是无意义的且会引发IllegalThreadStateException异常(实际上,当调用线程的状态所不允许的任何方法时,运行时系统都会引发IllegalThreadStateException异常)。
注意cp作为线程构造方法的第一个参数,该参数必须是实现了Runnable接口的对象并提供线程运行的run()方法,第二个参数是线程名。
2. 就绪状态(Runnable)
一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,如clockThread.start(); 语句就是启动clockThread线程。start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。
处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。
3. 运行状态(Running)
当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法,这里run()方法中是一个循环,循环条件是true。
public void run() {
while (true) {
repaint();
try {
Thread.sleep(1000);
} catch (InterruptedException e){}
}
4. 阻塞状态(Blocked)
线程运行过程中,可能由于各种原因进入阻塞状态。所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。有关阻塞状态在后面详细讨论。
5. 死亡状态(Dead)
线程的正常结束,即run()方法返回,线程运行就结束了,此时线程就处于死亡状态。本例子中,线程运行结束的条件是clockThread为null,而在小应用程序的stop()方法中,将clockThread赋值为null。即当用户离开含有该小应用程序的页面时,浏览器调用stop()方法,将clockThread赋值为null,这样在run()的while循环时条件就为false,这样线程运行就结束了。如果再重新访问该页面,小应用程序的start()方法又会重新被调用,重新创建并启动一个新的线程。
public void stop() {
clockThread = null;
}
程序不能像终止小应用程序那样通过调用一个方法来结束线程(小应用程序通过调用stop()方法结束小应用程序的运行)。线程必须通过run()方法的自然结束而结束。通常在run()方法中是一个循环,要么是循环结束,要么是循环的条件不满足,这两种情况都可以使线程正常结束,进入死亡状态。
例如,下面一段代码是一个循环:
public void run(){
int i = 0;
while(i<100){
i++;
System.out.println("i = " + i );
}
}
当该段代码循环结束后,线程就自然结束了。注意一个处于死亡状态的线程不能再调用该线程的任何方法。
9.3.2 线程的优先级和调度
Java的每个线程都有一个优先级,当有多个线程处于就绪状态时,线程调度程序根据线程的优先级调度线程运行。
可以用下面方法设置和返回线程的优先级。
- public final void setPriority(int newPriority) 设置线程的优先级。
- public final int getPriority() 返回线程的优先级。
newPriority为线程的优先级,其取值为1到10之间的整数,也可以使用Thread类定义的常量来设置线程的优先级,这些常量分别为:Thread.MIN_PRIORITY、Thread.NORM_PRIORITY、Thread.MAX_PRIORITY,它们分别对应于线程优先级的1、5和10,数值越大优先级越高。当创建Java线程时,如果没有指定它的优先级,则它从创建该线程那里继承优先级。
一般来说,只有在当前线程停止或由于某种原因被阻塞,较低优先级的线程才有机会运行。
前面说过多个线程可并发运行,然而实际上并不总是这样。由于很多计算机都是单CPU的,所以一个时刻只能有一个线程运行,多个线程的并发运行只是幻觉。在单CPU机器上多个线程的执行是按照某种顺序执行的,这称为线程的调度(scheduling)。
大多数计算机仅有一个CPU,所以线程必须与其他线程共享CPU。多个线程在单个CPU是按照某种顺序执行的。实际的调度策略随系统的不同而不同,通常线程调度可以采用两种策略调度处于就绪状态的线程。
(1) 抢占式调度策略
Java运行时系统的线程调度算法是抢占式的 (preemptive)。Java运行时系统支持一种简单的固定优先级的调度算法。如果一个优先级比其他任何处于可运行状态的线程都高的线程进入就绪状态,那么运行时系统就会选择该线程运行。新的优先级较高的线程抢占(preempt)了其他线程。但是Java运行时系统并不抢占同优先级的线程。换句话说,Java运行时系统不是分时的(time-slice)。然而,基于Java Thread类的实现系统可能是支持分时的,因此编写代码时不要依赖分时。当系统中的处于就绪状态的线程都具有相同优先级时,线程调度程序采用一种简单的、非抢占式的轮转的调度顺序。
(2) 时间片轮转调度策略
有些系统的线程调度采用时间片轮转(round-robin)调度策略。这种调度策略是从所有处于就绪状态的线程中选择优先级最高的线程分配一定的CPU时间运行。该时间过后再选择其他线程运行。只有当线程运行结束、放弃(yield)CPU或由于某种原因进入阻塞状态,低优先级的线程才有机会执行。如果有两个优先级相同的线程都在等待CPU,则调度程序以轮转的方式选择运行的线程。
9.4 线程状态的改变
一个线程在其生命周期中可以从一种状态改变到另一种状态,线程状态的变迁如图9.5所示:
图9.5 线程状态的改变
9.4.1 控制线程的启动和结束
当一个新建的线程调用它的start()方法后即进入就绪状态,处于就绪状态的线程被线程调度程序选中就可以获得CPU时间,进入运行状态,该线程就开始运行run()方法。
控制线程的结束稍微复杂一点。如果线程的run()方法是一个确定次数的循环,则循环结束后,线程运行就结束了,线程对象即进入死亡状态。如果run()方法是一个不确定循环,早期的方法是调用线程对象的stop()方法,然而由于该方法可能导致线程死锁,因此从1.1版开始,不推荐使用该方法结束线程。一般是通过设置一个标志变量,在程序中改变标志变量的值实现结束线程。请看下面的例子:
程序9.6 ThreadStop.java
import java.util.*;
class Timer implements Runnable{
boolean flag=true;
public void run(){
while(flag){
System.out.print("\r\t"+new Date()+"...");
try{
Thread.sleep(1000);
}catch(InterruptedException e){}
}
System.out.println("\n"+Thread.currentThread().getName()+" Stop");
}
public void stopRun(){
flag = false;
}
}
public class ThreadStop{
public static void main(String args[]){
Timer timer = new Timer();
Thread thread = new Thread(timer);
thread.setName("Timer");
thread.start();
for(int i=0;i<100;i++){
System.out.print("\r"+i);
try{
Thread.sleep(100);
}catch(InterruptedException e){}
}
timer.stopRun();
}
}
_____________________________________________________________________________▃
该程序在Timer类中定义了一个布而变量flag,同时定义了一个stopRun()方法,在其中将该变量设置为false。在主程序中通过调用该方法,从而改变该变量的值,使得run()方法的while循环条件不满足,从而实现结束线程的运行。
说明 在Thread类中除了stop()方法被标注为不推荐(deprecated) 使用外,suspend()方法和resume()方法也被标明不推荐使用,这两个方法原来用作线程的挂起和恢复。
9.4.2 线程阻塞条件
处于运行状态的线程除了可以进入死亡状态外,还可能进入就绪状态和阻塞状态。下面分别讨论这两种情况:
(1) 运行状态到就绪状态
处于运行状态的线程如果调用了yield()方法,那么它将放弃CPU时间,使当前正在运行的线程进入就绪状态。这时有几种可能的情况:如果没有其他的线程处于就绪状态等待运行,该线程会立即继续运行;如果有等待的线程,此时线程回到就绪状态状态与其他线程竞争CPU时间,当有比该线程优先级高的线程时,高优先级的线程进入运行状态,当没有比该线程优先级高的线程时,但有同优先级的线程,则由线程调度程序来决定哪个线程进入运行状态,因此线程调用yield()方法只能将CPU时间让给具有同优先级的或高优先级的线程而不能让给低优先级的线程。
一般来说,在调用线程的yield()方法可以使耗时的线程暂停执行一段时间,使其他线程有执行的机会。
(2) 运行状态到阻塞状态
有多种原因可使当前运行的线程进入阻塞状态,进入阻塞状态的线程当相应的事件结束或条件满足时进入就绪状态。使线程进入阻塞状态可能有多种原因:
① 线程调用了sleep()方法,线程进入睡眠状态,此时该线程停止执行一段时间。当时间到时该线程回到就绪状态,与其他线程竞争CPU时间。
Thread类中定义了一个interrupt()方法。一个处于睡眠中的线程若调用了interrupt()方法,该线程立即结束睡眠进入就绪状态。
② 如果一个线程的运行需要进行I/O操作,比如从键盘接收数据,这时程序可能需要等待用户的输入,这时如果该线程一直占用CPU,其他线程就得不到运行。这种情况称为I/O阻塞。这时该线程就会离开运行状态而进入阻塞状态。Java语言的所有I/O方法都具有这种行为。
③ 有时要求当前线程的执行在另一个线程执行结束后再继续执行,这时可以调用join()方法实现,join()方法有下面三种格式:
- public void join() throws InterruptedException 使当前线程暂停执行,等待调用该方法的线程结束后再执行当前线程。
- public void join(long millis) throws InterruptedException 最多等待millis毫秒后,当前线程继续执行。
- public void join(long millis, int nanos) throws InterruptedException 可以指定多少毫秒、多少纳秒后继续执行当前线程。
上述方法使当前线程暂停执行,进入阻塞状态,当调用线程结束或指定的时间过后,当前线程线程进入就绪状态,例如执行下面代码:
t.join();
将使当前线程进入阻塞状态,当线程t执行结束后,当前线程才能继续执行。
④ 线程调用了wait()方法,等待某个条件变量,此时该线程进入阻塞状态。直到被通知(调用了notify()或notifyAll()方法)结束等待后,线程回到就绪状态。
⑤ 另外如果线程不能获得对象锁,也进入就绪状态。
后两种情况在下一节讨论。
9.5 线程的同步与共享
前面程序中的线程都是独立的、异步执行的线程。但在很多情况下,多个线程需要共享数据资源,这就涉及到线程的同步与资源共享的问题。
9.5.1 资源冲突
下面的例子说明,多个线程共享资源,如果不加以控制可能会产生冲突。
程序9.7 CounterTest.java
class Num{
private int x=0;
private int y=0;
void increase(){
x++;
y++;
}
void testEqual(){
System.out.println(x+","+y+":"+(x==y));
}
}
class Counter extends Thread{
private Num num;
Counter(Num num){
this.num=num;
}
public void run(){
while(true){
num.increase();
}
}
}
public class CounterTest{
public static void main(String[] args){
Num num = new Num();
Thread count1 = new Counter(num);
Thread count2 = new Counter(num);
count1.start();
count2.start();
for(int i=0;i<100;i++){
num.testEqual();
try{
Thread.sleep(100);
}catch(InterruptedException e){ }
}
}
}
_____________________________________________________________________________▃
上述程序在CounterTest类的main()方法中创建了两个线程类Counter的对象count1和count2,这两个对象共享一个Num类的对象num。两个线程对象开始运行后,都调用同一个对象num的increase()方法来增加num对象的x和y的值。在main()方法的for()循环中输出num对象的x和y的值。程序输出结果有些x、y的值相等,大部分x、y的值不相等。
出现上述情况的原因是:两个线程对象同时操作一个num对象的同一段代码,通常将这段代码段称为临界区(critical sections)。在线程执行时,可能一个线程执行了x++语句而尚未执行y++语句时,系统调度另一个线程对象执行x++和y++,这时在主线程中调用testEqual()方法输出x、y的值不相等。
这里可能出现x的值小于y的值的情况,为什么?
9.5.2 对象锁的实现
上述程序的运行结果说明了多个线程访问同一个对象出现了冲突,为了保证运行结果正确(x、y的值总相等),可以使用Java语言的synchronized关键字,用该关键字修饰方法。用synchronized关键字修饰的方法称为同步方法,Java平台为每个具有synchronized代码段的对象关联一个对象锁(object lock)。这样任何线程在访问对象的同步方法时,首先必须获得对象锁,然后才能进入synchronized方法,这时其他线程就不能再同时访问该对象的同步方法了(包括其他的同步方法)。
通常有两种方法实现对象锁:
(1) 在方法的声明中使用synchronized关键字,表明该方法为同步方法。
对于上面的程序我们可以在定义Num类的increase()和testEqual()方法时,在它们前面加上synchronized关键字,如下所示:
synchronized void increase(){
x++;
y++;
}
synchronized void testEqual(){
System.out.println(x+","+y+":"+(x==y)+":"+(x<y));
}
一个方法使用synchronized关键字修饰后,当一个线程调用该方法时,必须先获得对象锁,只有在获得对象锁以后才能进入synchronized方法。一个时刻对象锁只能被一个线程持有。如果对象锁正在被一个线程持有,其他线程就不能获得该对象锁,其他线程就必须等待持有该对象锁的线程释放锁。
如果类的方法使用了synchronized关键字修饰,则称该类对象是线程安全的,否则是线程不安全的。
如果只为increase()方法添加synchronized 关键字,结果还会出现x、y的值不相等的情况,请考虑为什么?
(2) 前面实现对象锁是在方法前加上synchronized 关键字,这对于我们自己定义的类很容易实现,但如果使用类库中的类或别人定义的类在调用一个没有使用synchronized关键字修饰的方法时,又要获得对象锁,可以使用下面的格式:
synchronized(object){
//方法调用
}
假如Num类的increase()方法没有使用synchronized 关键字,我们在定义Counter类的run()方法时可以按如下方法使用synchronized为部分代码加锁。
public void run(){
while(true){
synchronized (num){
num.increase();
}
}
}
同时在main()方法中调用testEqual()方法也用synchronized关键字修饰,这样得到的结果相同。
synchronized(num){
num.testEqual();
}
对象锁的获得和释放是由Java运行时系统自动完成的。
每个类也可以有类锁。类锁控制对类的synchronized static代码的访问。请看下面的例子:
public class X{
static int x, y;
static synchronized void foo(){
x++;
y++;
}
}
当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("Producer #" + this.number + " put: " + i);
try {
sleep((int)(Math.random() * 100));
} catch (InterruptedException e) { }
}
}
}
_____________________________________________________________________________▃
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 #" + this.number + " got: " + value);
}
}
}
_____________________________________________________________________________▃
在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
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对象为监视器。通过监视器模型可以保证生产者线程和消费者线程同步,结果正确。
程序的运行结果如下:
特别注意: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所示:
图9.6 线程组的树型结构
如果在Applet中创建线程,新线程组可能不是main线程组,它依赖于使用的浏览器或Applet查看器。
创建属于某个线程组的线程可以通过下面构造方法实现
- public Thread(ThreadGroup group, Runnable target)
- public Thread(ThreadGroup group, String name)
- public Thread(ThreadGroup group, Runnable target, String name)
如下面代码创建的myThread线程属于myThreadGroup线程组。
ThreadGroup myGroup = new ThreadGroup("My Group of Threads");
Thread myThread = new Thread(myGroup, "a thread for my group");
为了得到线程所属的线程组可以调用Thread的getThreadGroup()方法,该方法返回ThreadGroup对象。可以通过下面方法获得线程所属线程组名。
myThread.getThreadGroup().getName()
一旦得到了线程组对象,就可查询线程组的有关信息,如线程组中其他线程、也可仅通过调用一个方法就可实现修改线程组中的线程,如挂起、恢复或停止线程。
9.6.3 线程组操作方法
线程组类提供了有关方法可以对线程组操作。
- public final String getName() 返回线程组名。
- public final ThreadGroup getParent() 返回线程组的父线程组对象。
- public final void setMaxPriority(int pri) 设置线程组的最大优先级。线程组中的线程不能超过该优先级。
- public final int getMaxPriority() 返回线程组的最大优先级。
- public boolean isDestroyed() 测试该线程组对象是否已被销毁。
- public int activeCount() 返回该线程组中活动线程的估计数。
- public int activeGroupCount() 返回该线程组中活动线程组的估计数。
- public final void destroy() 销毁该线程组及其子线程组对象。当前线程组的所有线程必须已经停止。
9.7 小 结
Java语言内在支持多线程的程序设计。线程是进程中的一个单个的顺序控制流,多线程是指单个程序内可以同时运行多个线程。
在Java程序中创建多线程的程序有两种方法。一种是继承Thread类并覆盖其run()方法,另一种是实现Runnable接口并实现其run()方法。
线程从创建、运行到结束总是处于下面五个状态之一:新建状态、就绪状态、运行状态、阻塞状态及死亡状态。Java的每个线程都有一个优先级,当有多个线程处于就绪状态时,线程调度程序根据线程的优先级调度线程运行。
线程都是独立的、异步执行的线程,但在很多情况下,多个线程需要共享数据资源,这就涉及到线程的同步与资源共享的问题。
所有Java线程都属于某个线程组。线程组提供了一个将多个线程组织成一个线程组对象来管理的机制,如可以通过一个方法调用来启动线程组中的所有线程。