Java语言程序设计 课件 第18章 并发编程基础_第1页
Java语言程序设计 课件 第18章 并发编程基础_第2页
Java语言程序设计 课件 第18章 并发编程基础_第3页
Java语言程序设计 课件 第18章 并发编程基础_第4页
Java语言程序设计 课件 第18章 并发编程基础_第5页
已阅读5页,还剩78页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

Java语言程序设计第18章并发编程基础

1Java多线程23主要内容Java语言程序设计(第4版)清华大学出版社2022创建任务和线程线程的状态和调度45线程同步与对象锁线程协调6并发工具7案例:百米飞人大战Java语言程序设计18.1Java多线程Java语言内在支持多线程的程序设计。18.1Java多线程进程(Process)是一个程序关于某个数据集的一次运行。也就是说,一个进程就是一个正在执行的程序,是程序的一次运行活动。它包括程序计数器、寄存器和变量的当前值。线程(Thread)则是进程中的一个单个的顺序控制流。线程运行需要的资源通常少于进程,因此一般将线程称为轻量级进程。单线程的概念很简单,整个程序中只有一个执行线索。作为单个顺序控制流,线程必须在运行的程序中得到自己运行的资源,如必须有自己的执行栈和程序计数器。单线程与多线程多线程(multi-thread)是指在单个的程序内可以同时运行多个不同的线程完成不同的任务。单线程程序多线程程序假设我们要编写程序模拟两个运动员进行百米比赛,用两个循环表示跑的距离。考虑下面代码:Java多线程

for(inti=0;i<100;i++){System.out.println("运动员A="+i);}for(intj=0;j<100;j++){System.out.println("运动员B="+j);}很多应用程序是用多线程实现的,如服务器编程就要用到多线程。比如,Tomcat服务器内部采用的就是多线程,上百个客户访问同一个Web应用都是通过一个线程提供服务的。在我们的应用开发中,也经常需要编写多线程的程序,如银行账户的取款、存款管理,售票窗口的管理,尤其在网络编程中更是经常使用多线程技术。Java多线程Java语言程序设计18.2创建任务和线程为了创建新线程,应该首先定义该线程要执行的任务。一般来说,为了定义线程的任务,需要定义一个实现java.lang.Runnable接口的任务类。创建任务和线程Runnable接口只定义了一个方法,格式为:

publicabstractvoidrun()线程要执行的任务就写在run()方法中,Runnable对象称为任务对象。实现多线程的两种方法1.实现Runnable接口并实现它的run()方法。2.继承Thread类并覆盖它的run()方法。实现多线程定义一个类实现Runnable接口,然后将该类对象作为线程的任务对象。实现Runnable接口就是实现run()方法。18.2.1实现Runnable接口下面程序通过实现Runnable接口构造任务类。classRunnableNimplementsRunnable{

publicvoidrun(){for(vari=0;i<100;i++){System.out.println(Thread.currentThread().getName()+"="+i);try{//使当前线程睡眠一段时间Thread.sleep((int)(Math.random()*100));}catch(InterruptedExceptione){} }System.out.println(Thread.currentThread().getName()+"到达终点");}}程序18.1RunnableDemo.javapublicclassRunnableDemo{publicstaticvoidmain(String[]args){RunnableNtask=newRunnableN();varthread1=newThread(task,"运动员A");varthread2=newThread(task,"运动员B");thread1.start();thread2.start();}}通过继承Thread类,并覆盖run()方法定义任务代码,这时就可以用该类的实例作为线程的任务对象。下面的程序定义了ThreadDemo类,它继承Thread类并覆盖了run()方法。18.2.2继承Thread类publicclassThreadDemoextendsThread{publicThreadDemo(Stringname){super(name);

}@Overridepublicvoidrun(){for(inti=0;i<100;i++){System.out.println(getName()+"="+i);

}System.out.println(getName()+"到达终点");

}程序18.2ThreadDemo.javapublicstaticvoidmain(String[]args){Threadt1=newThreadDemo("运动员A");Threadt2=newThreadDemo("运动员B");t1.start();t2.start();

}}第一种方法实现Runnable接口的缺点是编程稍微复杂一点,但这种方法可以扩展其他的类,更符合面向对象的设计思想。两种方法比较第二种方法的继承Thread类的优点是比较简单,缺点是如果一个类已经继承了某个类,它就不能再继承Thread类(因为Java语言只支持单继承)。当Java应用程序的main()方法开始运行时,Java虚拟机就启动一个线程,该线程负责创建其他线程,因此称为主线程。请看下面的程序。18.2.3主线程与守护线程publicclassMainThreadDemo{publicstaticvoidmain(String[]args){vart=Thread.currentThread();//返回当前线程对象System.out.println(t);System.out.println(t.getName());t.setName("MyThread");System.out.println(t);}}程序18.3MainThreadDemo.javaJava语言程序设计18.3线程的状态与调度一个线程从创建、运行到结束总是处于某种状态,Java通过java.lang.Thread.State枚举定义的常量表示这些状态。18.3.1线程的状态NEW新建RUNNABLE可运行BLOCKED阻塞WAITING等待(无限)TIMED_WAITING等待指定时间(限时)TERMINATED终止1.新建状态当调用Thread类的构造方法创建一个线程对象后,它就处于新建状态(NEW)。处于新建状态的线程仅仅是空的线程对象,系统并没有为其分配资源。当线程处于该状态,仅能启动线程,调用任何其他方法是无意义的且会引发IllegalThreadStateException异常。2.可运行状态当线程调用start()方法即启动了线程。start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程处于可运行状态(RUNNABLE)。处于可运行状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。3.阻塞状态线程运行过程中,可能由于各种原因进入阻塞状态。所谓阻塞状态(BLOCKED)是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于可运行状态的线程就可以获得CPU时间,进入运行状态。4.等待状态当线程调用sleep(longmillis)方法使线程进入等待指定时间状态(TIMED_WAITING),直到等待时间过后,线程再次进入可运行状态。当线程调用wait()方法使当前线程进入等待状态(WAITING),直到另一个线程调用了该对象的notify()方法或notifyAll()方法,该线程重新进入运行状态,恢复执行。5.结束状态线程正常结束,即run()方法返回,线程运行就结束了,此时线程就处于结束状态(TERMINATED)。多个处于可运行状态的线程是由Java虚拟机的线程调度器(scheduler)来调度的。每个线程有一个优先级,当有多个线程处于可运行状态时,线程调度器根据线程的优先级调度线程运行。18.3.2线程的优先级和调度可以用下面方法设置和返回线程的优先级。

finalvoidsetPriority(intnewPriority)finalintgetPriority()线程的优先级的取值为1到10之间的整数,数值越大优先级越高。可以使用Thread类定义的常量来设置线程的优先级,常量

MIN_PRIORITY1NORM_PRIORITY5MAX_PRIORITY1018.3.2线程的优先级和调度当创建线程时,如果没有指定它的优先级,则它从创建该线程那里继承优先级。一般来说,只有在当前线程停止或由于某种原因被阻塞,较低优先级的线程才有机会运行。通常,在线程任务中(run()方法)通过一个循环来控制线程的结束。如果循环是一个确定次数的循环,则循环结束后,线程运行就结束了,线程进入终止状态。18.3.3控制线程的结束publicvoidrun(){inti=0;while(i<100){i++;System.out.println("i="+i);}}如果run()方法是一个不确定循环,一般是通过设置一个标志变量,在程序中通过改变标志变量的值实现结束线程。在Thread类中被标明为不推荐使用的方法:

voidstop()voidsuspend()voidresume()18.3.3控制线程的结束Java语言程序设计18.4线程同步与对象锁多线程环境下,多个线程需要共享数据资源,因此可能存在多个并发线程同时访问同一资源,这可能引起线程冲突的情况。假设有一个计数器Counter类,它作为共享数据资源。当两个操作运行在不同线程中,对同一个数据的两个操作就可能产生冲突。其原因是有些操作看起来是简单操作,但不是原子的,虚拟机需要用多个步骤完成。18.4.1线程冲突与原子操作publicclassCounter{

privateintcount=0;

publicvoidincrement(){count++;

}

publicvoiddecrement(){count--;

}

publicintgetCount(){

returncount;

}}程序18.6Counter.java如果多个线程共享一个Counter对象,可能会产生冲突,得不到期望的结果。当两个操作运行在不同线程中,对同一个数据的两个操作就可能产生冲突。其原因是有些操作看起来是简单操作,但不是原子的,虚拟机需要用多个步骤完成。如表达式count++就被分解为以下三步:18.4.1线程冲突与原子操作(1)检索count的当前值。(2)为检索到的值加1。(3)增加后的值存到count中。假设有一个Counter对象,两个线程A和B。线程A调用increment()方法,同时线程B也调用increment()方法。如果count的初值为0,它们交错的动作可能按下列顺序执行。

(1)线程A:检索count的值0。

(2)线程B:检索count的

值0。

(3)线程A:将count加1,count的结果为1。

(4)线程B:将count加1,count的结果为1。

(5)线程A:将结果存回count,count的值为1。

(6)线程B:将结果存回count,count的值为1。18.4.1线程冲突与原子操作count的结果本来应该为2,现在结果是1。原因是线程A计算的结果被线程B覆盖了,线程A的结果丢失了。这只是一种可能性。在不同的环境下,也可能线程B的结果丢失,或者根本不发生错误。因为结果是不可预测的,所以线程冲突很难被检测和修复。出现上述情况的原因是表达式count++不是原子操作。所谓原子操作,是指在执行过程中不能被线程调度器中断的操作。18.6节还将介绍原子变量。18.4.1线程冲突与原子操作要防止多个线程同时访问同一资源产生冲突,就是防止多个线程同时执行共享对象的方法代码。程序中这样的代码称作临界区(criticalsection)。18.4.2方法同步为保证临界区中的代码在一段时间内只被一个线程执行,应在线程开始执行临界区代码时,给临界区加锁,这样,其他线程在这个临界区被解锁之前,无法执行临界区的代码,在其被解锁之后,其他线程才可以锁定并执行其中的代码。Java的每个对象都有一个内在锁(intrinsiclock),有时也称作监视器锁(monitorlock)。获得对象的内在锁是能够独占该对象访问权的一种方式。获得对象的内在锁与锁定对象是一样的。18.4.2方法同步Java通过同步代码块实现内在锁,Java支持两种同步:1)方法同步2)块同步方法同步就是在定义方法时使用synchronized关键字。publicclassCounter{privateintcount=0;publicsynchronizedvoidincrement(){count++;}publicsynchronizedvoiddecrement(){count--;}publicsynchronizedintgetCount(){returncount;}}程序18.7Counter.java用synchronized关键字修饰方法,成为同步方法如果使用类库中的类或别人定义的类时,调用的方法没有使用synchronized关键字修饰,又要获得对象锁,可以使用下面的格式:18.4.3块同步synchronized(object){//方法调用}这种方式是在调用对象的非synchronized方法时,为了保证不出现线程冲突,首先将对象加锁。例如,下面代码利用非线程安全的Counter类作为计数器,为了在计数器递增的时候将计数器对象锁定,incrementCount()方法锁定Counter对象。18.4.3块同步Countercounter=newCounter();…publicvoidincrementCount(){synchronized(counter){//锁定counter对象 //执行需要同步的语句counter.increment();}}}在synchronized块中调用非同步方法每个类也可以有类锁。类锁控制对类的synchronizedstatic代码的访问。请看下面的例子。18.4.3块同步

publicclassSampleClass{staticintx,y;

staticsynchronizedvoidincrement(){x++;y++;}}Java语言程序设计18.5线程协调简单的生产者消费者模型要求生产者产生一个数字(0-9),消费者取得一个数字。不能出现生产者太快,或消费者太快的情况。生产者Producer5消费者ConsumerBoxBox.javaProducer.javaCustomer.javaProducerCustomerTest.java简单的生产者消费者模型首先设计用于存储数据的Box类,定义如下。程序18.8Box.javapublicclassBox{privateintdata;publicsynchronizedvoidput(intvalue){data=value;}publicsynchronizedintget(){returndata;}}18.5.1不正确的设计publicclassProducerextendsThread{privateBoxbox;//被共享的对象

publicProducer(Boxc){box=c;}程序18.9Producer.javapublicvoidrun(){for(vari=0;i<10;i++){box.put(i);//生产一个整数iSystem.out.println("Producer"+"put:"+i);try{sleep((int)(Math.random()*100));}catch(InterruptedExceptione){}}}}publicvoidrun(){intvalue=0;for(vari=0;i<10;i++){value=box.get();//消费一个整数iSystem.out.println("Consumer"+"got:"+value);try{sleep((int)(Math.random()*100));}catch(InterruptedExceptione){}}}}程序18.10Consumer.javapublicclassConsumerextendsThread{privateBoxbox;publicConsumer(Boxc){box=c;}publicclassProducerConsumerTest{publicstaticvoidmain(String[]args){Boxbox=newBox();Producerp1=newProducer(box);//将box对象传递给生产者Consumerc1=newConsumer(box);//将box对象传递给消费者p1.start();c1.start();}}程序18.11ProducerConsumerTest.java如果生产者的速度比消费者快,那么在消费者还没来得及取出前一个数据,生产者又产生了新的数据,于是消费者就会跳过前一个数据,这样就会产生下面的结果:18.5.1不正确的设计Consumergot:3Producerput:4Producerput:5Consumergot:5…反之,如果消费者的速度比生产者快,那么在生产者还没有产生下一个数据前,消费者可能两次取出同一个数据,这样就会产生下面的结果:18.5.1不正确的设计Producerput:4Consumergot:4Consumergot:4Producerput:5…为了避免上述情况发生,就必须使生产者线程向Box对象中存储数据与消费者线程从Box对象中取得数据协调起来。为达到这一目的,在程序中可以采用监视器(monitor)模型,同时通过调用对象的wait()方法和notify()或notifyAll()方法实现同步。18.5.2监视器模型下面是修改后的Box类的定义。publicclassBox{privateintdata;privatebooleanavailable=false;//用来表示数据是否可用publicsynchronizedvoidput(intvalue){while(available==true){//数据没被取出try{

wait();//当前线程等待}catch(InterruptedExceptione){e.printStackTrace(System.out);}}程序18.12Box.javadata=value;//产生数据available=true;notifyAll();//通知所有等待的线程继续执行}publicsynchronizedintget(){while(available==false){//还没有数据try{

wait();//当前线程等待}catch(InterruptedExceptione){e.printStackTrace(System.out);}}available=false;

notifyAll();//通知所有等待的线程继续执行returndata;//取出数据}}Java语言程序设计18.6并发工具虽然Java语言为编写多线程程序提供了内在的支持,如Thread类和synchronized关键字,但是它们很难正确使用。在java.util.concurrent包和子包中提供了并发工具。有些工具是为了替代Java内置的线程和同步特征。本节讨论几个比较重要的类型。18.6.1原子变量原子操作(atomicoperation)是一组操作,对系统的其他部分而言,它们组合在一起,就像一个操作一样,不会导致线程冲突。正如前面的例子所证明,整数自增运算不是一个原子操作。为了实现某些操作的原子操作,java.util.concurrent.atomic包中提供了一些类,这些类定义了一些方法以原子方式执行各种操作,例如,AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference等。18.6.1原子变量AtomicInteger对象将一个整数封装在内部,并提供在这个整数上的一些原子操作,例如addAndGet()、incrementAndGet()、decrementAndGet()、getAndIncrement()、get()等。getAndIncrement()和incrementsAndGet()方法返回的是不同的结果,前者返回原子变量的当前值,然后将这个值递增,incrementsAndGet()方法先递增原子变量的值,然后返回递增后的值,即获得值、增加1。执行下面代码后,x的值是10,y的值是12。18.6.1原子变量

AtomicIntegercounter=newAtomicInteger(10);intx=counter.getAndIncrement();//x=10inty=counter.incrementAndGet();//y=12publicclassAtomicCounter{AtomicIntegercount=newAtomicInteger(0);publicvoidincrement(){ count.getAndIncrement();}publicvoiddecrement(){ count.decrementAndGet();}publicintgetCount(){ returncount.get();}}程序18.13AtomicCounter.java该计数器使用AtomicInteger原子变量对象计数,定义的increment()、decrement()和getCount()方法无需使用synchronized关键字修饰,因为在AtomicInteger对象上的操作都是原子的。创建线程需要付出一定开销,为每个任务都创建一个线程,这会造成程序性能的下降。从Java5.0开始,程序中如果需要执行多个子任务,应该优先使用线程执行器(Executor)对象。18.6.2Executor和ExecutorService线程执行器对象是java.util.concurrent.Executor或者它的子接口ExecutorService的一个实现,通过它的execute()方法来执行多个Runnable任务。Executor接口只定义了一个execute()方法,格式如下:

publicvoidexecute(Runnabletask)ExecutorService是Executor接口的一个扩展,它添加了终止方法和执行Callable的方法。Callable和Runnable类似,只不过它可以返回一个值,并且便于通过Future接口来完成删除的任务。一般来说,不需要自己编写Executor接口(或ExecutorService接口)的实现,使用工具类Executors的静态方法就可得到Executor实例。18.6.2Executor和ExecutorServicepublicstaticExecutorServicenewSingleThreadExecutor()publicstaticExecutorServicenewCachedThreadPool()publicstaticExecutorServicenewFixedThreadPool(intnumOfThread)下面代码是将Runnable任务提交给Executor执行的示例:Runnabletask=()->{…};//创建Runnable任务实例Executorexecutor=…;//创建执行器对象executor.execute(task);//将任务提交给执行器执行18.6.2Executor和ExecutorService下面程序创建两个任务,然后创建一个Executor对象并调用它的execute()方法执行任务。程序中使用Lambda表达式创建Runnable任务对象。Runnablehellos=()->{for(inti=1;i<=100;i++) System.out.println("hello"+i); };Runnablegoodbyes=()->{for(inti=1;i<=100;i++) System.out.println("goodbye"+i); };//创建线程执行器对象Executorexecutor=Executors.newCachedThreadPool();executor.execute(hellos);executor.execute(goodbyes);程序18.14ExecutorDemo.javaCallable<V>是一项任务,定义了一个call()方法,返回一个值,并抛出一个异常。publicinterfaceCallable<V>{Vcall()throwsException}Callable与Runnable类似,只不过Runnable的run()方法不带返回值或抛出异常。18.6.3Callable和Future要执行Callable任务,也需要一个ExecutorService实例,可以使用Executors类的newCachedThreadPool()方法或newFixedThreadPool()方法返回ExecutorService对象,然后调用它的submit()方法将任务提交给执行器。18.6.3Callable和Future

ExecutorServiceexecutor–Executors.newCachedThreadPool();Callbale<V>task=…;//创建任务对象

Future<V>result=executor.submit(task);//将任务提交给执行器ExecutorService接口的submit()方法返回一个Future<V>对象。任务提交后的某个时刻执行任务得到一个结果封装在Future<V>对象中,通过它的get()方法可以获取Callable<V>任务(即调用call()方法)的返回值。有两个重载的get()方18.6.3Callable和FuturepublicV

get()throwsInterruptedException,ExecutionException:publicV

get(long

timeout,TimeUnit

unit)throwsExecutionException,TimeoutException,InterruptedException,用修饰符synchronized锁定一个共享资源局限性。1)试图获取这种锁的线程是无法后退,如果无法获得锁,就会无限期地阻塞。2)锁定和解锁仅限于方法和块:无法在一个方法中将资源锁定,在另一个方法释放它。18.6.4使用Lock锁定对象并发工具提供了一些更高级的锁。如Lock接口,它提供了可以克服Java内置锁局限性的方法。Lock接口提供了lock()和unlock()方法,这意味着只要保留对某个锁的引用,就可以在程序中的任何位置释放该锁。

为了确保unlock()方法总是能被调用,在调用lock()方法之后,用finally子句中调用unlock()方法。

LockaLock=newReentrantLock();…aLock.lock();//加锁try{//临界区}finally{aLock.unlock();//释放锁}18.6.4使用Lock锁定对象Java语言程序设计18.7案例:百米飞人大战运动会中百米比赛是最激动人心、最刺激的一项比赛。本案例使用多线程模拟多人进行的百米比赛。在百米比赛中,每个运动员可以看做一个线程,他们都独立跑完100米,最后到达终点的名次不同。问题描述运动运跑完100米,这个任务用一个Runnable对象实现,每个运动员用一个Thread线程对象实现。我们定义一个Task类实现Runnable接口来实现任务对象。在主类中,使用Thread线程类创建8个对象模拟8名运动员,然后启动线程运行。设计思路Tasktask=newTask();Thread[]player=newThread[8];for(inti=0;i<8;i++){ player[i]=newThread(task,"P"+i); player[i].start();}还可以使用线程池执行线程任务。具体使用Executors类的newFixedThreadPool()方法创建可执行8个任务的线程池,然后通过线程池执行每个任务。代码如下:设计思路Executorexecutor=Executors.newFixedThreadPool(8);for(inti=0;i<8;i++){task[i]=newTask("P"+i);

executor.execute(task[i]);} classTaskimplementsRunnable{publicvoidrun(){for(vari=0;i<100;i++){System.out.println(Thread.currentThread().getName()+"..."+i);try{//让当前线程睡眠一段时间Thread.sleep((int)(Math.random()*10));}catch(InterruptedExceptione){} }System.out.println(Thread.currentThread().getName()+"到达终点");}}程序18.15HundredRace.javapublicclassHandredContext{ publicstaticvoidmain(String[]args){ Tasktask=newTask(); Thread[]playe

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论