Java多线程详解.docx

上传人:b****6 文档编号:5963603 上传时间:2023-01-02 格式:DOCX 页数:27 大小:67.56KB
下载 相关 举报
Java多线程详解.docx_第1页
第1页 / 共27页
Java多线程详解.docx_第2页
第2页 / 共27页
Java多线程详解.docx_第3页
第3页 / 共27页
Java多线程详解.docx_第4页
第4页 / 共27页
Java多线程详解.docx_第5页
第5页 / 共27页
点击查看更多>>
下载资源
资源描述

Java多线程详解.docx

《Java多线程详解.docx》由会员分享,可在线阅读,更多相关《Java多线程详解.docx(27页珍藏版)》请在冰豆网上搜索。

Java多线程详解.docx

Java多线程详解

Java多线程编程

Java语言的一个重要特点是内在支持多线程的程序设计。

多线程是指在单个的程序内可以同时运行多个不同的线程完成不同的任务。

多线程的程序设计具有广泛的应用。

本章主要讲授线程的概念、如何创建多线程的程序、线程的生存周期与状态的改变、线程的同步与互斥等内容。

9.1线程与线程类

9.1.1线程的概念

线程的概念来源于计算机的操作系统的进程的概念。

进程是一个程序关于某个数据集的一次运行。

也就是说,进程是运行中的程序,是程序的一次运行活动。

线程和进程的相似之处在于,线程和运行的程序都是单个顺序控制流。

有些教材将线程称为轻量级进程(lightweightprocess)。

线程被看作是轻量级进程是因为它运行在一个程序的上下文内,并利用分配给程序的资源和环境。

作为单个顺序控制流,线程必须在运行的程序中得到自己运行的资源,如必须有自己的执行栈和程序计数器。

线程内运行的代码只能在该上下文内。

因此还有些教程将执行上下文(executioncontext)作为线程的同义词。

所有的程序员都熟悉顺序程序的编写,如我们编写的名称排序和求素数的程序就是顺序程序。

顺序程序都有开始、执行序列和结束,在程序执行的任何时刻,只有一个执行点。

线程(thread)则是进程中的一个单个的顺序控制流。

单线程的概念很简单,如图9.1所示。

多线程(multi-thread)是指在单个的程序内可以同时运行多个不同的线程完成不同的任务,图9.2说明了一个程序中同时有两个线程运行。

一个线程

两个线程

图9.1单线程程序示意图图9.2多线程程序示意图

有些程序中需要多个控制流并行执行。

例如,

for(inti=0;i<100;i++)

System.out.println("RunnerA="+i);

for(intj=0;j<100;j++)

System.out.println("RunnerB="+j);

上面的代码段中,在只支持单线程的语言中,前一个循环不执行完不可能执行第二个循环。

要使两个循环同时执行,需要编写多线程的程序。

很多应用程序是用多线程实现的,如HotJavaWeb浏览器就是多线程应用的例子。

在HotJava浏览器中,你可以一边滚动屏幕,一边下载Applet或图像,可以同时播放动画和声音等。

9.1.2Thread类和Runnable接口

多线程是一个程序中可以有多段代码同时运行,那么这些代码写在哪里,如何创建线程对象呢?

首先,我们来看Java语言实现多线程编程的类和接口。

在java.lang包中定义了Runnable接口和Thread类。

Runnable接口中只定义了一个方法,它的格式为:

∙publicabstractvoidrun()

这个方法要由实现了Runnable接口的类实现。

Runnable对象称为可运行对象,一个线程的运行就是执行该对象的run()方法。

Thread类实现了Runnable接口,因此Thread对象也是可运行对象。

同时Thread类也是线程类,该类的构造方法如下:

∙publicThread()

∙publicThread(Runnabletarget)

∙publicThread(Stringname)

∙publicThread(Runnabletarget,Stringname)

∙publicThread(ThreadGroupgroup,Runnabletarget)

∙publicThread(ThreadGroupgroup,Stringname)

∙publicThread(ThreadGroupgroup,Runnabletarget,Stringname)

target为线程运行的目标对象,即线程调用start()方法启动后运行那个对象的run()方法,该对象的类型为Runnable,若没有指定目标对象,则以当前类对象为目标对象;name为线程名,group指定线程属于哪个线程组(有关线程组的概念请参考9.6节)。

Thread类的常用方法有:

∙publicstaticThreadcurrentThread()返回当前正在执行的线程对象的引用。

∙publicvoidsetName(Stringname)设置线程名。

∙publicStringgetName()返回线程名。

∙publicstaticvoidsleep(longmillis)throwsInterruptedException

∙publicstaticvoidsleep(longmillis,intnanos)throwsInterruptedException

使当前正在执行的线程暂时停止执行指定的毫秒时间。

指定时间过后,线程继续执行。

该方法抛出InterruptedException异常,必须捕获。

∙publicvoidrun()线程的线程体。

∙publicvoidstart()由JVM调用线程的run()方法,启动线程开始执行。

∙publicvoidsetDaemon(booleanon)设置线程为Daemon线程。

∙publicbooleanisDaemon()返回线程是否为Daemon线程。

∙publicstaticvoidyield()使当前执行的线程暂停执行,允许其他线程执行。

∙publicThreadGroupgetThreadGroup()返回该线程所属的线程组对象。

∙publicvoidinterrupt()中断当前线程。

∙publicbooleanisAlive()返回指定线程是否处于活动状态。

9.2线程的创建

本节介绍如何创建和运行线程的两种方法。

线程运行的代码就是实现了Runnable接口的类的run()方法或者是Thread类的子类的run()方法,因此构造线程体就有两种方法:

∙继承Thread类并覆盖它的run()方法;

∙实现Runnable接口并实现它的run()方法。

9.2.1继承Thread类创建线程

通过继承Thread类,并覆盖run()方法,这时就可以用该类的实例作为线程的目标对象。

下面的程序定义了SimpleThread类,它继承了Thread类并覆盖了run()方法。

程序9.1SimpleThread.java

publicclassSimpleThreadextendsThread{

publicSimpleThread(Stringstr){

super(str);

}

publicvoidrun(){

for(inti=0;i<100;i++){

System.out.println(getName()+"="+i);

try{

sleep((int)(Math.random()*100));

}catch(InterruptedExceptione){}

}

System.out.println(getName()+"DONE");

}

}

_____________________________________________________________________________▃

SimpleThread类继承了Thread类,并覆盖了run()方法,该方法就是线程体。

程序9.2ThreadTest.java

publicclassThreadTest{

publicstaticvoidmain(Stringargs[]){

Threadt1=newSimpleThread("RunnerA");

Threadt2=newSimpleThread("RunnerB");

t1.start();

t2.start();

}

}

_____________________________________________________________________________▃

在ThreadTest类的main()方法中创建了两个SimpleThread类的线程对象并调用线程类的start()方法启动线程。

构造线程时没有指定目标对象,所以线程启动后执行本类的run()方法。

注意,实际上ThreadTest程序中有三个线程同时运行。

请试着将下段代码加到main()方法中,分析程序运行结果。

for(inti=0;i<100;i++){

System.out.println(Thread.currentThread().getName()+"="+i);

try{

Thread.sleep((int)(Math.random()*500));

}catch(InterruptedExceptione){}

System.out.println(Thread.currentThread().getName()+"DONE");

}

从上述代码执行结果可以看到,在应用程序的main()方法启动时,JVM就创建一个主线程,在主线程中可以创建其他线程。

再看下面的程序:

程序9.3MainThreadDemo.java

publicclassMainThreadDemo{

publicstaticvoidmain(Stringargs[]){

Threadt=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.4ThreadTest.java

classT1implementsRunnable{

publicvoidrun(){

for(inti=0;i<15;i++)

System.out.println("RunnerA="+i);

}

}

classT2implementsRunnable{

publicvoidrun(){

for(intj=0;j<15;j++)

System.out.println("RunnerB="+j);

}

}

publicclassThreadTest{

publicstaticvoidmain(Stringargs[]){

Threadt1=newThread(newT1(),"ThreadA");

Threadt2=newThread(newT2(),"ThreadB");

t1.start();

t2.start();

}

}

_____________________________________________________________________________▃

下面是一个小应用程序,利用线程对象在其中显示当前时间。

程序9.5ThreadTest.java

//

//

importjava.awt.*;

importjava.util.*;

importjavax.swing.*;

importjava.text.DateFormat;

publicclassClockDemoextendsJApplet{

privateThreadclockThread=null;

privateClockPanelcp=newClockPanel();

publicvoidinit(){

getContentPane().add(cp);

}

publicvoidstart(){

if(clockThread==null){

clockThread=newThread(cp,"Clock");

clockThread.start();

}

}

publicvoidstop(){

clockThread=null;

}

}

classClockPanelextendsJPanelimplementsRunnable{

publicvoidpaintComponent(Graphicsg){

super.paintComponent(g);

Calendarcal=Calendar.getInstance();

Datedate=cal.getTime();

DateFormatdateFormatter=DateFormat.getTimeInstance();

g.setColor(Color.BLUE);

g.setFont(newFont("TimesNewRoman",Font.BOLD,36));

g.drawString(dateFormatter.format(date),50,50);

}

publicvoidrun(){

while(true){

repaint();

try{

Thread.sleep(1000);

}catch(InterruptedExceptione){}

}

}

}

_____________________________________________________________________________▃

该小应用程序的运行结果如图9.3所示:

图9.3ClockDemo的运行结果

9.3线程的状态与调度

9.3.1线程的生命周期

线程从创建、运行到结束总是处于下面五个状态之一:

新建状态、就绪状态、运行状态、阻塞状态及死亡状态。

线程的状态如图9.4所示:

图9.4线程的五种状态

下面以前面的Java小程序为例说明线程的状态:

1.新建状态(NewThread)

当Applet启动时调用Applet的start()方法,此时小应用程序就创建一个Thread对象clockThread。

publicvoidstart(){

if(clockThread==null){

clockThread=newThread(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运行时系统的线程调度程序(threadscheduler)来调度的。

3.运行状态(Running)

当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法,这里run()方法中是一个循环,循环条件是true。

publicvoidrun(){

while(true){

repaint();

try{

Thread.sleep(1000);

}catch(InterruptedExceptione){}

}

4.阻塞状态(Blocked)

线程运行过程中,可能由于各种原因进入阻塞状态。

所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。

有关阻塞状态在后面详细讨论。

5.死亡状态(Dead)

线程的正常结束,即run()方法返回,线程运行就结束了,此时线程就处于死亡状态。

本例子中,线程运行结束的条件是clockThread为null,而在小应用程序的stop()方法中,将clockThread赋值为null。

即当用户离开含有该小应用程序的页面时,浏览器调用stop()方法,将clockThread赋值为null,这样在run()的while循环时条件就为false,这样线程运行就结束了。

如果再重新访问该页面,小应用程序的start()方法又会重新被调用,重新创建并启动一个新的线程。

publicvoidstop(){

clockThread=null;

}

程序不能像终止小应用程序那样通过调用一个方法来结束线程(小应用程序通过调用stop()方法结束小应用程序的运行)。

线程必须通过run()方法的自然结束而结束。

通常在run()方法中是一个循环,要么是循环结束,要么是循环的条件不满足,这两种情况都可以使线程正常结束,进入死亡状态。

例如,下面一段代码是一个循环:

publicvoidrun(){

inti=0;

while(i<100){

i++;

System.out.println("i="+i);

}

}

当该段代码循环结束后,线程就自然结束了。

注意一个处于死亡状态的线程不能再调用该线程的任何方法。

9.3.2线程的优先级和调度

Java的每个线程都有一个优先级,当有多个线程处于就绪状态时,线程调度程序根据线程的优先级调度线程运行。

可以用下面方法设置和返回线程的优先级。

∙publicfinalvoidsetPriority(intnewPriority)设置线程的优先级。

∙publicfinalintgetPriority()返回线程的优先级。

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)。

然而,基于JavaThread类的实现系统可能是支持分时的,因此编写代码时不要依赖分时。

当系统中的处于就绪状态的线程都具有相同优先级时,线程调度程序采用一种简单的、非抢占式的轮转的调度顺序。

(2)时间片轮转调度策略

有些系统的线程调度采用时间片轮转(round-robin)调度策略。

这种调度策略是从所有处于就绪状态的线程中选择优先级最高的线程分配一定的CPU时间运行。

该时间过后再选择其他线程运行。

只有当线程运行结束、放弃(yield)CPU或由于某种原因进入阻塞状态,低优先级的线程才有机会执行。

如果有两个优先级相同的线程都在等待CPU,则调度程序以轮转的方式选择运行的线程。

9.4线程状态的改变

一个线程在其生命周期中可以从一种状态改变到另一种状态,线程状态的变迁如图9.5所示:

yield()

run()

scheduler

start()

 

sleep()

I/O操作

join()

wait()

 

图9.5线程状态的改变

9.4.1控制线程的启动和结束

当一个新建的线程调用它的start()方法后即进入就绪状态,处于就绪状态的线程被线程调度程序选中就可以获得CPU时间,进入运行状态,该线程就开始运行run()方法。

控制线程的结束稍微复杂一点。

如果线程的run()方法是一个确定次数的循环,则循环结束后,线程运行就结束了,线程对象即进入死亡状态。

如果run()方法是一个不确定循环,早期的方法是调用线程对象的stop()方法,然而由于该方法可能导致线程死锁,因此从1.1版开始,不推荐使用该方法结束线程。

一般是通过设置一个标志变量,在程序中改变标志变量的值实现结束线程。

请看下面的例子:

程序9.6ThreadStop.jav

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 自然科学

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1