Java程序设计基础-并发编程_第1页
Java程序设计基础-并发编程_第2页
Java程序设计基础-并发编程_第3页
Java程序设计基础-并发编程_第4页
Java程序设计基础-并发编程_第5页
已阅读5页,还剩66页未读 继续免费阅读

下载本文档

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

文档简介

新一代信息技术"十三五"系列规划Java程序设计基础教程第一零章并发编程支持多线程是现代操作系统地一大特点,多线程地操作系统因为可以真正意义上地实现多任务同时运行,极大地提升了操作系统地处理速度。跨台地特导致Java无法像C/C++这些语言一样通过调用系统API来实现多线程程序,所以它在语言本身加了对多线程地支持。这些功能都以面向对象地方式来实现,更加易于理解与使用。

一零.一线程与程在操作系统,通常将程看作是系统资源分配与运行地基本单位,一个任务就是一个程。程拥有独立地系统资源,包含CPU,内存与输入输出端口等,例如打开地浏览器与Word文档,这些相对独立地资源表明了程具有动态,并发,独立与异步等特点。线程(thread)是"程"某个单一顺序地控制流,被称为轻量级程(lightweightprocesses),是比程更小地执行单位,也是程序执行流最小地单位。一个标准地线程由线程ID,当前指令指针(PC),寄存器集合与堆栈组成。线程是程地一个实体,是被系统独立调度与分配地基本单位,线程在运行地资源归属于程,同属于一个程地所有线程享该程所拥有地系统资源。一个线程可以创建与撤销另一个线程,同一个程地多个线程也可以并发执行。由于程所有资源是固定地且线程间存在相互制约,使得线程可能处于就绪,阻塞与运行等状态,令线程地执行呈现出间断。线程之间可以享代码与数据,实时通信,行必要地同步操作等。一个程序都至少拥有一个程;每个程拥有一个或者多个线程。每个线程都有自己独立地资源与生命周期。程与线程地最大区别在于,程是由操作系统来控制地,而线程则是由程来控制地。程都是相互独立地,各自享有各自地内存空间,因此程间地通信是昂贵且受限地,程间地转换也是需要开销地;线程则享程地内存空间,线程通信是便宜地且线程间地转换也是低成本地,这种低成本低开销地通信也可能会产生意想不到地错误:当多个线程访问同一个变量时,获取到地值是不一样地!不过,也不必担心,这些问题可以通过同步机制与锁机制来消除。一零.二线程地创建多线程技术是Java语言地重要特之一,Java台提供了一套广泛且功能强大地API,工具与技术。Java编写地程序都运行在Java虚拟机(JVM)。在JVM内部,程序地多任务是通过线程来实现地。在同一个JVM程,有且只有一个程,那就是JVM本身,在JVM环境,所有地程序代码都是以线程来运行地。Java地线程有两种实现方式,一种是继承Thread类,一种是实现Runnable接口。但是无论是哪种方式,线程都要使用到Thread类及其有关方法。一零.二.一继承Thread类Thread类是一个实体类,该类封装了线程地行为,想要利用Thread创建一个线程,需要创建一个从Thread类导出地子类,并实现Thread地run()方法,在run()方法内部可以根据需要编写相应地实现逻辑,最后调用Thread类地start()方法来启动。Thread地构造方法有很多种,每种构造方法用途各异,如表一零-一所示。构造方法说明Thread()构造一个线程对象Thread(Runnabletarget)构造一个线程对象,target是被创建线程地目地对象,它实现了Runnable接口地run()方法Thread(Stringname)以指定名称构造一个线程对象Thread(ThreadGroupgroup,Runnabletarget)在指定线程组构造一个线程对象,使用目地对象地target地run()方法Thread(Runnabletarget,Stringname)以指定名称构造一个线程对象,使用目地对象target地run()方法Thread(ThreadGroupgroup,Runnabletarget,Stringname)在指定地线程组创建一个指定名称地线程,使用目地对象target地run()方法Thread(ThreadGroupgroup,Runnabletarget,Stringname,longstackSize)在指定线程组构造一个线程对象,以name作为线程地名字,使用目地对象target地run()方法,stackSize指定堆栈大小表一零-一Thread类地构造方法Thread也提供了很多辅助方法,以让线程正常运行与方便程序员对线程地控制,其常用方法如表一零-二所示。方法名说明staticintactiveCount()返回线程组正在运行地线程地数目voidcheckAccess()确定当前运行地线程是否有权限修改线程staticThreadcurrentThread()返回当前正在执行地线程voiddestroy()销毁线程,但不回收资源staticvoiddumpStack()显示当前线程地堆栈信息longgetId()返回当前线程地id值StringgetName()返回当前线程地名称intgetPriority()返回当前线程地优先级Thread.StategetState()返回当前线程地状态ThreadGroupgetThreadGroup()返回当前线程所属地线程组voidinterrupt()断线程booleanisAlive()判断当前线程是否存活booleanisDaemon()判断当前线程是否是守护线程booleanisInterrupted()判断本线程是否被断voidjoin()等待直到线程死亡voidjoin(longmillis)等待最多millis毫秒,直到线程死亡voidrun()如果类是使用单独地Runnable对象构造地,将调用Runnable对象地run()方法,否则本方法不做任何事情就返回了,如果是子类继承Thread类,请务必实现本方法以覆盖父类voidsetDaemon(booleanon)将当前线程设置为守护线程voidsetName(Stringname)将当前线程名称修改为namevoidsetPriority(intnewPriority)设置当前线程地优先级staticvoidsleep(longmillis)线程休眠millis毫秒voidstart()启动线程,JVM会自动调用run()方法staticvoidyield()暂停当前线程,同时允许其它线程运行表一零-二常用地Thread方法在以前地案例,当需要执行当前类时,每个类都有一个main()方法。该方法是类地入口,JVM会找到该入口方法并运行,此时产生了一个线程,该线程便是主线程。当main()方法运行结束后,主线程运行完成,JVM也就随即退出了。JVM负责对程,线程行管理,JVM分配时间片(CPU时间)给线程,线程按照系统地设定轮流获取时间片执行,切换时间很短,在对线程运行效率要求不严格地场景下可以忽略不计。案例一零-一Thread实现多线程运行结果如图一零-一所示。图一零-一运行结果由于每个线程运行地次数较少,所以线程默认优先级下地运行随机不是很明显,但通过方框标注地线程Thread-三地运行可以看出,实际上线程运行并不是顺序地。案例一零-二Thread地部分方法使用运行结果如图一零-二所示。图一零-二运行结果案例一零-三start方法与run方法运行结果如图一零-三所示。图一零-三运行结果启动Thread类时,需要要使用start()方法启动一个线程,如果直接调用run()方法,则JVM认为这只是一次普通地方法调用,而非需要启动一个线程在执行run()方法内部地逻辑。读者在使用线程地时候切记。在start()方法调用后也可以看出,运行地是两个线程地代码,而且它们之间互不干扰地同时执行。所以一些工作给线程去做地时候,启动一个新线程地线程可以做自己想做地其它事情,而无需等到新线程地执行结束。一零.二.二实现Runnable接口实现多线程地另一个方式是实现Runnable接口。Runnable只有一个方法,即run()方法,该方法需要由一个实现了此接口地类来实现。实现了Runnable接口地类地对象需要由Thread类地一个实例内部运行它,其本身不能直接运行。案例一零-四Runnable实现多线程运行结果如图一零-四所示。图一零-四运行结果图一零-四只摘取部分地输出内容,从内容上看,实现Runnable与继承Thread都能达到相同目地,都能启动一个新线程。唯一地区别是Runnable对象需要包装成Thread对象后才能运行。如果查看Thread与Runnable类源码会发现,Thread类实际上是Runnable地一个实现类。可能有读者会对Runnable接口地存在产生疑问,毕竟这个接口只有一个run()方法。Runnable地存在是因为Java地类有且只能有一个直接父类,如果只是提供了Thread类,那么想要继承其它类且需要同时继承Thread类地这个子类,在实现这种继承逻辑上会产生很多困难,而Runnable则避免了这种尴尬局面地出现,在Java,一个类是可以实现多个接口地。一零.三线程地调度在JVM,线程只有在获取了CPU分配地时间片后才会真正地执行,在线程创建后到死亡地这个过程还有其它地线程状态,这些状态组成了线程地生命周期。

一零.三.一线程地生命周期如同生命体一般,线程也有生命周期,线程地生命周期是从线程新建开始,一直持续到线程死亡。在新建与死亡之间,线程还有就绪,阻塞与运行状态,一个线程会在这五种状态间转换,最终完成自己地使命。线程地状态及转换关系如图一零-五所示。图一零-五Java线程状态转换图线程各个状态地说明如下。新建:当创建一个Thread类与它地子类,对象后,线程就处于新建状态,这种状态地线程并不具备运行地能力,该操作对于系统而言,仅仅消耗普通对象创建时会消耗地非CPU资源。就绪:当处于新建状态地线程调用start()方法被启动之后,线程将入线程队列等待CPU时间片,行执行。此时地线程才具备了运行地能力,一旦获取了时间片线程就执行。运行:就绪状态地线程获取了时间片之后,就入了运行状态,此时线程会执行run()方法内地代码逻辑。线程一旦入运行状态,就与启动该线程地线程没有任何关系了,两者行运行,互不影响。阻塞:线程在运行地过程因资源无法满足,前驱任务没有完成或者被调用阻塞方法都会导致线程入阻塞状态。阻塞状态地线程会让出CPU,然后等待,直到引起阻塞地条件不存在了,线程会重新入就绪状态,等待CPU时间片。死亡:不具备继续运行能力地线程就处于死亡状态。线程在运行完毕后会自然入死亡状态正常死亡,在运行过程也会因为异常退出而导致非正常死亡。需要说明地是,在大部分系统都支持线程优先级地设定。在相同地情况下,优先级高地线程会优先获得CPU时间片行执行。一零.三.二线程地优先级同VIP与超级VIP一样,线程也是有优先级地,线程地优先级可以通过方法getPriority()获取,为了使重要地事情优先完成,Java也提供了setPriority()方法给线程设定优先级。但是需要指出地是,JVM是运行在所属系统上地一个线程,线程地创建与执行还是需要基于对应地系统地,所以,在一些不支持线程优先级策略地系统,Java设定地优先级并不起作用,这一点是读者一定要引起注意地。案例一零-五线程优先级运行结果如图一零-六所示。图一零-六运行结果运行结果如图一零-六所示。从案例一零-五地输出结果可以看出,在Java线程是有默认优先级地,默认情况下线程地优先级为五,是普通优先级。Java定义了线程地优先级为一~一零,数字越大,优先级越高。对于优先级,读者需要注意以下几点。(一)并不是线程优先级高地线程一定会比线程优先级低地线程先执行,它只是会比线程优先级低地线程有更多地机会先执行。(二)Java地线程优先级取决于JVM运行地系统,线程优先级策略也依赖于系统,这导致了可能在一个系统优先级不同地线程在另一个系统优先级相同,甚至对于某些不支持线程优先级调度策略地系统,Java定义地优先级完全无效。一零.三.三线程插队线程地魅力是充分地利用CPU,使得程序在单位时间内充分地利用CPU而提升程序地处理效率。但由于线程运行顺序地不确定加上当代操作系统核心数地提升,导致在某些情况下线程无法明确前驱任务是否完成。为了保证前驱任务完成后才执行当前线程,可以调用join()方法。join()会阻塞当前线程直到插队线程执行完毕之后才会继续执行。案例一零-六线程插队运行结果如图一零-七所示。图一零-七运行结果一零.三.四线程休眠Thread类有sleep()方法。该方法可以让当前线程休眠并让出CPU,使得其它线程可以获取CPU行执行。对于周期很强地系统,调用线程休眠是最好地形式,线程休眠时只会等待休眠结束且不占用CPU资源,等到线程休眠结束后会入就绪状态等待时间片继续执行。案例一零-七线程休眠运行结果如图一零-八所示。图一零-八运行结果一零.三.五同步与互斥寄宿学校都会有排队打水地场景,许多同时等待一个开水阀准备接开水。当前面一个接水完毕后,后面一个才能开始接水,如果接水地动作不是同步地,那么就会出现问题。案例一零-八非同步接水运行结果如图一零-九所示。图一零-九运行结果通过案例一零-八不难发现,没有添加同步地接水场景有些莫名奇妙,明明王一先开始打水,结果却是王零第一个打完水,而且,王一还没有接完水,后面地就开始了接水,场面混乱不堪。synchronized是Java地关键字,是一种同步锁。在多线程场景,它用于控制线程对同一个代码片段是否可以并发执行。它修饰地对象有以下几种。修饰代码块:被修饰地代码块被称为同步语句块,其作用地范围是大括号{}括起来地代码,作用地对象是调用这个代码块地对象。修饰方法:被修饰地方法称为同步方法,其作用地范围是整个方法,作用地对象是调用这个方法地对象。修饰静态方法:其作用地范围是整个静态方法,作用地对象是这个类地所有对象。修饰类:其作用地范围是synchronized后面括号括起来地部分,作用地对象是这个类地所有对象。对于成员变量地修饰,相当于修饰代码块,作用于类地一个实例,对另一个实例不起作用;对于静态变量地修饰类似于静态方法,作用于类地所有实例。案例一零-九同步接水运行结果如图一零-一零所示。图一零-一零运行结果该案例使用地是synchronized修饰静态成员变量地方式。使用该方式会对这个类地所有对象行同步控制,也就是说,每一次只会有一个该类地对象执行synchronized修饰地代码内容,其它线程对该类地这个对象与该类地其它对象都需要等待当前线程执行完毕方可执行。有时候为了实现这种同步,也会使用信号量行控制,具体案例如下:案例一零-一零线程互斥地计数器运行结果如图一零-一一所示。图一零-一一运行结果其flag相当于一个信号量,当有线程访问公资源地时候会首先检测信号量,如果可用,则修改信号量防止其它线程入,否则就入等待,当访问完成之后修改信号量,并将所有处于该信号量等待状态地线程唤醒,给其它线程获取该信号量地机会。案例一零-一一生产者-消费者模型运行结果如图一零-一二所示。图一零-一二运行结果生产-消费者模型是线程同步最著名地同步问题,在该模型,生产者负责生产数据,但数据需要在可缓存地数量之内,如果超出库存则需要等待数据被消费后再插入;消费者消费库存数据则恰恰相反,如果库存空了则需要等待,等到有库存以后再行消费。从案例一零-一一可以发现,虽然消费者与生产者在消费与生产地层面上是异步行地,但是它们之间需要保持同步,生产者不能在库存满了之后还继续增加库存,消费者也不能在一个空地库存获取产品。一零.三.六死锁问题在日常生活偶尔会碰到这种情况,买肉地说:"我只有拿到了肉我才会给卖肉地钱!"而卖肉地则说:"我只有拿到了钱才会给买肉地肉!"这种争执如果得不到劝与必然导致买肉地买不到肉,卖肉地卖不出去肉,这种"死脑筋"地场景在计算机系统被称为死锁。死锁是指多个程因竞争资源而造成地一种相互等待地僵局,如果没有外力地作用,必然导致无限地等待。例如,A程占用了输入设备,在释放前请求了打印机设备,但是打印机被B程占用,B在释放前需要请求输入设备,这样,A程与B程就会无休止地等待,入死锁状态。死锁是由系统资源地竞争导致系统资源不足以及资源分配不当或程运行过程请求与释放资源地顺序不当导致地。死锁地产生有四个必要条件。互斥条件:一个资源每次只能被一个程使用,即一段时间内这个资源只能被一个程占用,其它程请求资源,请求线程只能等待。请求与保持条件:程已经保持了至少一个资源,但又提出了新地资源请求,而该资源已被其它程占用,此时请求程被阻塞,但对自己已获得地资源保持不放。不可剥夺条件:程所获得地资源在未使用完毕之前,不能被其它程强行夺走,即只能由获得该资源地程自己来释放(只能是主动释放)。循环等待条件:若干程间形成首尾相接循环等待资源地关系。死锁只能在上述四个条件都满足地条件下才能产生。案例一零-一二线程死锁运行结果如图一零-一三所示。图一零-一三运行结果这是比较简单地竞争导致地死锁,案例一零-一二,线程t一获得了一个对象锁objALock,释放前请求objBLock锁,而t二线程则是获取了objBLock锁,释放前请求objALock锁,由于双方都要求在获取对方地锁后释放锁,导致了类似于先给钱还是先给肉地矛盾而产生死锁。死锁产生地条件有四个,所以想要避免死锁,只需要破坏四个条件地任意一个就能实现。例如:可以避免嵌套锁,嵌套锁是死锁产生地高发场景;避免无限期等待,可以设置等待超时时间;一次只对一个资源获取锁,当需要获取另一个锁地时候,先释放当前锁。一零.四多线程理解了线程地创建,同步与死锁问题之后,就是领会多线程真正魅力地时候了,相较于串行执行地简单与耗时,多线程则稍显复杂且高效。军事天才拿破仑可以同时听取数位将军地汇报并做出相应地军事部署,就是因为它具有多线程可以同时处理多个任务地能力。一零.四.一线程池技术Java地线程池技术是运行场景最多地并发框架,几乎所有需要异步或者并发执行任务地程序都可以使用线程池技术。合理使用线程池技术可以降低线程创建与销毁造成地消耗,提高相应速度与提高线程地可管理。线程池地处理流程如下。(一)线程池判断核心线程池是否都在执行任务,如果不是,创建一个新地线程来执行任务,如果核心线程池里地线程都在执行任务,则入下一个流程。(二)线程池判断工作队列是否已经满了。如果没有满,将新提地任务存储到这个工作队列,如果满了,则入下一个流程。(三)线程池判断线程池地线程是否都处于工作状态,如果没有,创建一个新地工作线程来执行任务,如果满了,则给饱与策略来处理这个任务。Java通过Executors提供如下四种线程池。(一)newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,如无可回收,则创建线程。(二)newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出地线程会在队列等待。(三)newScheduledThreadPool:创建一个定长线程池,支持定时及周期任务执行。(四)newSingleThreadExecutor:创建一个单线程化地线程池,它只会用唯一地工作线程来执行任务,保证所有地任务按照指定顺序(FIFO,LIFO,优先级)执行。缓存线程池使用得比较普遍,而计划任务线程池地功能相对比较特殊,下面就对这两个线程池做一个简单地实例说明。案例一零-一三缓存线程池运行结果如图一零-一四所示。图一零-一四运行结果从案例运行可以看出,缓存线程池地线程在执行完成一个任务之后,会继续执行下一个任务,其pool-一-thread-一与pool-一-thread-二又执行了不止一次。缓存线程池地工作原理大致是如果有空闲线程,使用空闲线程执行新任务,否则判断线程池线程是否已经是最大线程数,如果不是,则创建一个新线程执行任务,否则,入等待队列。案例一零-一四计划任务线程池运行结果如图一零-一五所示。图一零-一五运行结果案例使用地是固定周期执行地计划任务线程池。其第一个参数是执行任务(一般是一个线程),第二个参数是执行后多久行第一次任务执行,第二个任务是其后每次执行间隔是多久,最后一个参数是设置时间单元,本案例使用地是秒,读者可以参考自己地需求,修改成分钟或小时。一零.四.二Callable与Future一.Callable并发编程一般使用Runnable,然后将其给线程池处理,这种情况是不需要知道线程执行结果地。但是万一将军说我汇报完了还想知道对应军事部署怎么办?这时候Java就会告诉妳,妳可以试试Callable接口。Callable用法与Runnable类似,只不过调用地是call()方法,而不是run()方法,该方法有一个泛型返回值类型,可根据需要指定。案例一零-一五Callable地用法运行结果如图一零-一六所示。图一零-一六运行结果Callable支持返回值,并可以被ExecutorService运行,ExecutorService继承自Executors,而Executors对于一个线程,如果是无需返回地,直接使用execute()方法执行,对于Callable,则使用submit()方法执行。Executors地submit()方法会返回一个Future类型地对象。二.FutureFuture对象用于存放Callable对象执行后地返回值,对于这个返回值,可以使用get()方法获取,get()方法是阻塞地,直到Callable地执行结果已经出来,如果不想阻塞,可以调用isDone()查询结果是否已经得出。案例一零-一六Future地

温馨提示

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

评论

0/150

提交评论