第8章多线程.docx

上传人:b****2 文档编号:24574374 上传时间:2023-05-29 格式:DOCX 页数:18 大小:35.87KB
下载 相关 举报
第8章多线程.docx_第1页
第1页 / 共18页
第8章多线程.docx_第2页
第2页 / 共18页
第8章多线程.docx_第3页
第3页 / 共18页
第8章多线程.docx_第4页
第4页 / 共18页
第8章多线程.docx_第5页
第5页 / 共18页
点击查看更多>>
下载资源
资源描述

第8章多线程.docx

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

第8章多线程.docx

第8章多线程

第8章多线程

8.1线程

8.2多线程的互斥与同步

幻灯片2

8.1线程

8.1.1程序、进程与线程

●程序是一段静态的代码,它是应用软件执行的蓝本。

平常所说的多任务就是在操作系统中同时运行几个相同或不相同的应用程序,每个程序占用一个进程。

●进程是程序的一次动态执行过程,它对应从代码加载、执行到执行完毕的一个完整过程,这个过程也是进程本身从产生、发展到消亡的过程。

作为执行蓝本的同一段程序,可以被多次加载到系统的不同内存区域分别执行,形成不同的进程。

●线程是比进程更小的执行单位。

一个进程在其执行过程中,可以产生多个线程。

幻灯片3

●每个进程都有一段专有的内存区域,即使是多次启动同一段程序产生的不同进程也是如此。

●同一进程的各线程之间可以共享相同的内存空间(包括代码空间和数据空间),并利用这些共享的内存来完成数据交换、实时通信和必要的同步工作。

●多任务与线程是两个不同的概念。

前者是针对操作系统而言的,表示操作系统可以同时运行多个应用程序;后者是针对一个程序而言的,表示在一个程序内部可以同时运行多个线程。

幻灯片4

8.1.2线程的创建

●Java的线程是通过java.lang.Thread类来实现的。

当我们生成一个Thread类的对象之后,一个新的线程就产生了。

●此线程实例表示Java解释器中的真正的线程,通过它可以启动线程、终止线程、线程挂起等。

幻灯片5

构造方法

●publicThread(ThreadGroupgroup,Runnabletarget,Stringname);

●其中,

●①group指明该线程所属的线程组;

●②target为实际执行线程体的目标对象,它必须实现接口Runnable;

●③name为线程名。

●Java中的每个线程都有自己的名称,Java提供了不同Thread类构造器,允许给线程指定名称。

如果name为null时,则Java自动提供唯一的名称。

幻灯片6

当上述构造方法的某个参数为null时,我们可得到下面的几个构造方法:

publicThread();

publicThread(Runnabletarget);

publicThread(Runnabletarget,Stringname);

publicThread(Stringname);

publicThread(ThreadGroupgroup,Runnabletarget);

publicThread(ThreadGroupgroup,Stringname);

幻灯片7

●一个类声明实现Runnable接口就可以充当线程体,在接口Runnable中只定义了一个方法run():

●   publicvoidrun();

●任何实现接口Runnable的对象都可以作为一个线程的目标对象,类Thread本身也实现了接口Runnable,因此我们可以通过两种方法实现线程体。

幻灯片8

●①定义一个线程类,它继承线程类Thread并重写其中的方法run(),这时在初始化这个类的实例时,目标target可为null,表示由这个实例来执行线程体。

由于Java只支持单重继承,用这种方法定义的类不能再继承其它父类。

●②提供一个实现接口Runnable的类作为一个线程的目标对象,在初始化一个Thread类或者Thread子类的线程对象时,把目标对象传递给这个线程实例,由该目标对象提供线程体run()。

这时,实现接口Runnable的类仍然可以继承其它父类。

●每个线程都是通过某个特定Thread对象的方法run()来完成其操作的,方法run()称为线程体。

幻灯片9

例:

通过继承类Thread构造线程体

●importjava.io.*;

●publicclassTest{

●publicstaticvoidmain(Stringargs[]){

●newSimpleThread("First").start();

●//第一个线程的名字为First

●newSimpleThread("Second").start();

●//第二个线程的名字为Second

●}

●}

幻灯片10

运行结果:

0First

0Second

1First

2First

1Second

3First

4First

2Second

5First

6First

7First

3Second

4Second

8First

5Second

9First

6Second

7Second

DONE!

First

8Second

9Second

DONE!

Second

classSimpleThreadextendsThread{

publicSimpleThread(Stringstr){

super(str);//调用其父类的构造方法

}

publicvoidrun(){//重写run方法

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

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

//打印次数和线程的名字

try{

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

//线程睡眠,把控制权交出去

}catch(InterruptedExceptione){}

}

System.out.println("DONE!

"+getName());

//线程执行结束

}

}

幻灯片11

●仔细分析一下运行结果,会发现两个线程是交错运行的,感觉就象是两个线程在同时运行。

但是如果计算机只有一个CPU,在某个时刻只能是只有一个线程在运行,而java语言在设计时就充分考虑到线程的并发调度执行。

●对于程序员来说,在编程时要注意给每个线程执行的时间和机会,主要是通过让线程睡眠的办法(调用sleep()方法)来让当前线程暂停执行,然后由其它线程来争夺执行的机会。

●如果上面的程序中没有用到sleep()方法,则就是第一个线程先执行完毕,然后第二个线程再执行完毕。

所以用活sleep()方法是学习线程的一个关键。

幻灯片12

例:

通过接口构造线程体

importjava.util.*;

importjava.awt.*;

importjava.applet.*;

publicclassClockextendsAppletimplementsRunnable{//实现接口

ThreadclockThread;

publicvoidstart(){//该方法是Applet的方法,不是线程的方法

if(clockThread==null){

clockThread=newThread(this,"Clock");

/*线程体是Clock对象本身,线程名字为"Clock"*/

clockThread.start();//启动线程

}

}

幻灯片13

publicvoidrun(){//run()方法中是线程执行的内容

while(clockThread!

=null){

repaint();//刷新显示画面

try{

clockThread.sleep(1000);

//睡眠1秒,即每隔1秒执行一次

}catch(InterruptedExceptione){}

}

}

publicvoidpaint(Graphicsg){

Datenow=newDate();//获得当前系统时间

g.drawString(now.getHours()+":

"+now.getMinutes()+":

"

+now.getSeconds(),5,10);//显示当前时间

}

publicvoidstop(){//该方法是Applet的方法,不是线程的方法

clockThread.stop();

clockThread=null;

}

}

幻灯片14

●test.html

●heigth=60width=200>

●上面这个例子是通过每隔1秒种就执行线程的刷新画面功能,显示当前的时间;看起来的效果就是一个时钟,每隔1秒就变化一次。

由于采用的是实现接口Runnable的方式,所以该类Clock还继承了Applet,Clock就可以Applet的方式运行。

幻灯片15

●采用间接创建线程的原因:

●第一个理由是我们并不改变线程本身的性质,仅覆盖run方法,并没有增加新的功能,因此将Thread扩展子类并不恰当,这不太符合类扩展规范。

●第二个理由是:

如果实现Runnable接口,它可能使我们所设计的类扩展其它类型而变得更为有用。

幻灯片16

8.1.3线程的生命周期

●新建的线程在一个完整的生命周期中通常要经历如下5种状态。

●①创建状态

●使用new运算符创建一个线程后,该线程仅仅是一个空对象,系统没有分配资源,称该线程处于创建状态(newthread)。

●②可运行状态

●使用start()方法启动一个线程后,系统为该线程分配了除CPU外的所需资源,使该线程处于可运行状态(Runnable)。

幻灯片17

●③运行中状态

●Java运行系统通过调度选中一个Runnable的线程,使其占有CPU并转为运行中状态(Running)。

此时,系统真正执行线程的run()方法。

●④阻塞状态

●一个正在运行的线程因某种原因不能继续运行时,进入阻塞状态(Blocked)。

●⑤死亡状态

●线程结束后是死亡状态(Dead)。

幻灯片18

8.1.4线程的调度与优先级

1.线程的调度

●Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程。

线程调度器按照线程的优先级决定应调度哪些线程来执行。

●线程调度器按线程的优先级高低选择高优先级线程(进入运行中状态)执行,同时线程调度是抢先式调度,即如果在当前线程执行过程中,一个更高优先级的线程进入可运行状态,则这个线程立即被调度执行。

幻灯片19

●抢先式调度又分为:

时间片方式和独占方式。

●在时间片方式下,当前活动线程执行完当前时间片后,如果有其他处于就绪状态的相同优先级的线程,系统会将执行权交给其他就绪态的同优先级线程;当前活动线程转入等待执行队列,等待下一个时间片的调度。

●在独占方式下,当前活动线程一旦获得执行权,将一直执行下去,直到执行完毕或由于某种原因主动放弃CPU,或者是有一高优先级的线程处于就绪状态。

幻灯片20

yield

1.生产,出产,带来2.屈服,放弃;不再反对,让出

●下面几种情况下,当前线程会放弃CPU:

●①线程调用了yield()或sleep()方法主动放弃;

●②由于当前线程进行I/O访问,外存读写,等待用户输入等操作,导致线程阻塞;或者是为等候一个条件变量,以及线程调用wait()方法;

●③抢先式系统下,由高优先级的线程参与调度;时间片方式下,当前时间片用完,由同优先级的线程参与调度。

幻灯片21

2.线程的优先级

●线程的优先级用数字来表示,范围从1到10,即Thread.MIN_PRIORITY到Thread.MAX_PRIORITY。

一个线程的缺省优先级是5,即Thread.NORM_PRIORITY。

下述方法可以对优先级进行操作:

●  intgetPriority();//得到线程的优先级

●  voidsetPriority(intnewPriority);

●  //当线程被创建后,可通过此方法改变线程的优先级

幻灯片22

例:

生成三个不同线程,其中一个线程在最低优先级下运行,而另两个线程在最高优先级下运行。

importjava.io.*;

publicclasstest{

publicstaticvoidmain(String[]args){

Threadt1=newMyThread("T1");

t1.setPriority(Thread.MIN_PRIORITY);//设置优先级为最小

t1.start();

Threadt2=newMyThread("T2");

t2.setPriority(Thread.MAX_PRIORITY);//设置优先级为最大

t2.start();

Threadt3=newMyThread("T3");

t3.setPriority(Thread.MAX_PRIORITY);//设置优先级为最大

t3.start();

}

}

幻灯片23

classMyThreadextendsThread{

Stringmessage;

MyThread(Stringmessage){

this.message=message;

}

publicvoidrun(){

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

System.out.println(message+""+getPriority());

//获得线程的优先级

}

}

运行结果:

T11

T210

T210

T210

T310

T310

T310

T11

T11

注意:

并不是在所有系统中运行Java程序时都采用时间片策略调度线程,所以一个线程在空闲时应该主动放弃CPU,以使其他同优先级和低优先级的线程得到执行。

幻灯片24

8.2多线程的互斥与同步

●前面所提到的线程都是独立的,而且异步执行,也就是说每个线程都包含了运行时所需要的数据或方法,而不需要外部的资源或方法,也不必关心其它线程的状态或行为。

但是经常有一些同时运行的线程需要共享数据,此时就需考虑其他线程的状态和行为,否则就不能保证程序的运行结果的正确性。

幻灯片25

例:

classStack{

   intidx=0;//堆栈指针的初始值为0

   char[]data=newchar[6];//堆栈有6个字符的空间

   publicvoidpush(charc){//压栈操作

    data[idx]=c;//数据入栈

    idx++;//指针向上移动一位

   }

   publiccharpop(){//出栈操作

     idx--;//指针向下移动一位

     returndata[idx];//数据出栈

   }

}

幻灯片26

●两个线程A和B在同时使用Stack的同一个实例对象,A正在往堆栈里push一个数据,B则要从堆栈中pop一个数据。

如果由于线程A和B在对Stack对象的操作上的不完整性,会导致操作的失败,具体过程如下所示:

●①操作之前

●     data=|p|q|||||idx=2

●②A执行push中的第一个语句,将r推入堆栈;

●     data=|p|q|r||||idx=2

●③A还未执行idx++语句,A的执行被B中断,B执行pop方法,返回q:

●     data=|p|q|r||||idx=1

●④A继续执行push的第二个语句:

●     data=|p|q|r||||idx=2

●最后的结果相当于r没有出栈。

产生这种问题的原因在于对共享数据访问的操作的不完整性。

幻灯片27

8.2.1互斥锁

●为解决操作的不完整性问题,在Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。

每个对象都对应于一个可称为"互斥锁"的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

关键字synchronized来与对象的互斥锁联系。

当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问。

幻灯片28

publicvoidpush(charc){

    synchronized(this){//this表示Stack的当前对象

       data[idx]=c;

       idx++;

    }

 }

 publiccharpop(){

    synchronized(this){//this表示Stack的当前对象

       idx--;

       returndata[idx];

    }

 }

幻灯片29

●synchronized除了象上面讲的放在对象前面限制一段代码的执行外,还可以放在方法声明中,表示整个方法为同步方法。

●  publicsynchronizedvoidpush(charc){

●  …

●  }

●如果synchronized用在类声明中,则表明该类中的所有方法都是synchronized的。

幻灯片30

8.2.2多线程的同步

●我们将通过多线程同步的模型:

生产者-消费者问题来说明怎样实现多线程的同步。

●我们把系统中使用某类资源的线程称为消费者,产生或释放同类资源的线程称为生产者。

在下面的Java的应用程序中,生产者线程向文件中写数据,消费者从文件中读数据,这样,在这个程序中同时运行的两个线程共享同一个文件资源。

通过这个例子我们来了解怎样使它们同步。

幻灯片31

例:

importjava.io.*;

publicclassTest{

publicstaticvoidmain(Stringargs[]){

SyncStackstack=newSyncStack();

//下面的消费者类对象和生产者类对象

所操作的是同一个同步堆栈对象

Runnablesource=newProducer(stack);

Runnablesink=newConsumer(stack);

Threadt1=newThread(source);//线程实例化

Threadt2=newThread(sink);//线程实例化

t1.start();//线程启动

t2.start();//线程启动

}

}

幻灯片32

classSyncStack{//同步堆栈类

privateintindex=0;//堆栈指针初始值为0

privatechar[]buffer=newchar[6];//堆栈有6个字符的空间

publicsynchronizedvoidpush(charc){//加上互斥锁

while(index==buffer.length){//堆栈已满,不能压栈

try{

this.wait();//等待,直到有数据出栈

}catch(InterruptedExceptione){}

}

this.notify();//通知其它线程把数据出栈

buffer[index]=c;//数据入栈

index++;//指针向上移动

}

幻灯片33

●publicsynchronizedcharpop(){//加上互斥锁

●while(index==0){//堆栈无数据,不能出栈

●try{

●this.wait();//等待其它线程把数据入栈

●}catch(InterruptedExceptione){}

●}

●this.notify();//通知其它线程入栈

●index--;//指针向下移动

●returnbuffer[index];//数据出栈

●}

●}

幻灯片34

classProducerimplementsRunnable{//生产者类

SyncStacktheStack;//生产者类生成的字母都保存到同步堆栈中

publicProducer(SyncStacks){

theStack=s;

}

publicvoidrun(){

charc;

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

c=(char)(Math.random()*26+'A');//随机产生20个字符

theStack.push(c);//把字符入栈

System.out.println("Produced:

"+c);//打印字符

try{

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

/*每产生一个字符线程就睡眠*/

}catch(InterruptedExceptione){}

}

}

}

幻灯片35

classConsumerimplementsRunnable{//消费者类

SyncStacktheStack;//消费者类获得的字符都来自同步堆栈

publicConsumer(SyncStacks){

theStack=s;

}

publicvoidrun(){

charc;

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

c=theStack.pop();//从堆栈中读取字符

System.out.println("Consumed:

"+c);//打印字符

try{

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

/*每读取一个字符线程就睡眠*/

}catch(InterruptedExceptione){}

}

}

}

程序执行结果:

Produced:

P

Consumed:

P

Produced:

D

Consumed:

D

Produced:

V

Consumed:

V

Produced:

D

Consumed:

D

Produced:

N

Consumed:

N

Produced:

W

Consumed:

W

幻灯片36

●类Producer是生产者模型,其中的run()方法中定义了生产者线程所做的操作,循环调用push()方法,将生产的20个字母送入堆栈中,每次执行完push操作后,调用sleep()方法睡眠一段随机时间,以给其他线程执行的机会。

类C

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

当前位置:首页 > IT计算机 > 电脑基础知识

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

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