探索并发编程.docx_第1页
探索并发编程.docx_第2页
探索并发编程.docx_第3页
探索并发编程.docx_第4页
探索并发编程.docx_第5页
已阅读5页,还剩23页未读 继续免费阅读

下载本文档

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

文档简介

探索并发编程(一)-操作系统篇在多线程、多处理器甚至是分布式环境的编程时代,并发是一个不可回避的问题,很多程序员一碰到并发二字头皮就发麻,也包括我。既然并发问题摆在面前一个到无法回避的坎,倒不如拥抱它,把它搞清楚,决心花一定的时间从操作系统底层原理到Java的基础编程再到分布式环境等几个方面深入探索并发问题。先就从原理开始吧。并发产生的原因虽然从直观效果上,处理器是并行处理多项任务,但本质上一个处理器在某个时间点只能处理一个任务,属于串行执行。在单处理器的情况下,并发问题源于多道程序设计系统的一个基本特性:进程的相对执行速度不可预测,它取决于其他进程的活动、操作系统处理中断的方式以及操作系统的调度策略。在分布式环境下,并发产生的可能性就更大了,只要大家有依赖的共享资源,就有并发问题的出现,因为互相调用次序更加没法控制。并发带来的问题 全局资源的共享充满了危险。不同任务对同一个共享资源的读写顺序非常关键 操作系统很难对分配资源进行最优化管理。挂起的线程占有了其他活动线程需要的资源 定位错误非常困难。这种问题来源和触发的不确定性,导致定位问题非常困难 限制分布式系统横向扩展能力进程的交互进程的交互方式决定了并发问题产生的上下文,解决并发问题也需根据进程交互方式的不同而不同对待。一般进程交互分为以下三种:1)进程间相互独立这种情况下虽然进程间没有数据共享,所做事情也互不联系,但它们存在竞争关系。计算机中有些临界资源比如I/O设备、存储器、CPU时间和时钟等等都需要通过竞争得到,你占用的时候就得保证别人没法占用,因此首先得解决这种互斥的需求。另外,要处理好这种临界资源的调度策略,处理不当就有可能发生死锁和饥饿2)进程间通过共享合作这种情况下进程间虽然执行的过程是相互独立的,互不知道对方的执行情况,但互相之间有共享的数据。因此除了有以上互斥需求和死锁饥饿的可能,另外还会有数据一致性的问题。当多个进程非原子性操作同一个数据时候,互相之间操作时序不当就有可能造成数据不一致3)进程间通过通信合作这种情况下进程间通过消息互相通信,知晓各自的执行情况,不共享任何资源,因此就可以避免互斥和数据不一致问题,但仍然存在死锁和饥饿的问题并发问题的解决办法操作系统解决并发问题一般通过互斥,为了提供互斥的支持,需要满足以下需求: 一次只允许一个进程进入临界区 一个非临界区停止的进程必须不干涉其他进程 不允许出现一个需要访问临界区的进程被无限延迟 一个进程驻留在临界区中的时间必须是有限的 临界区空闲时,任何需要进入临界区的进程必须能够立即进入满足互斥的解决方案:1)硬件支持 中断禁用中断禁用简单说来就是在某一进程在临界区执行过程中禁用中断,不允许其他进程通过中断打断其执行。虽然这种方式可以保证互斥,但代价非常高,处理器被限制于只能交替执行程序,效率降低。另外不适用于多处理器环境。 专用机器指令从硬件的角度提供一些机器指令,用于保证多个动作的原子性,通过适用这些具有原子性的指令来控制临界区的访问。比如提供符合以下逻辑的原子性指令:1. booleantestset(inti)2. if(i=0)3. i=1;4. returntrue;5. else6. returnfalse;7. 8. 在控制临界区的时候可以通过忙等待来保证只有一个进程停留在临界区,伪代码如下所示:1. intbolt;2. voidonlyOneThread()3. while(!testset(bolt)4. /*等待*/5. 6. /*临界区*/7. bolt=0;8. 专用机器指令的优点是可以不限制处理器数量,也不限制临界区的数量,但它的问题是使用了忙等待,消耗处理器时间。并且也存在饥饿和死锁的问题2)信号量其原理是多个进程可以通过简单的信号进行合作,一个进程可以被迫在某一个位置停止,直到它收到一个特定的信号,再重新被唤起工作。这种方式最大优点就是解决了忙等待的问题。其核心和机器指令类似,通过提供原子性信号量控制方法,一般情况下提供等待和唤起两种操作原语,以较为简单的二元信号量原语为例,两种方法的伪代码如下:1. voidwait(semaphores)2. if(s.value=1)3. s.value=0;4. else5. /*停止此线程,并把线程放入s的线程等待队列(s.queue)里*/6. 7. 8. voidsignal(semaphores)9. if(s.queue.size()=0)10. s.value=1;11. else12. /*从s的线程等待队列(s.queue)里拿出一个线程,使其激活执行*/13. 14. 两个方法的实现关键在于其原子性,当然也可以借助专用机器指令的方法来保障其原子性,毕竟这两种方法的执行不长,使用忙等待也问题不大。再看互斥的问题,若使用信号量,则其具体实现如以下伪代码所示:1. voidonlyOneThread()2. wait(s);3. /*临界区*/4. signal(s);5. 3)管程信号量虽然解决了性能问题,但使得信号量的控制逻辑遍布在程序里,控制逻辑复杂以后很难整体上控制所有信号量。而管程的思路和面向对象类似,通过一个管程的概念把互斥和资源保护的细节封装在管程的内部,外部程序只需对管程的正确使用就能保证避免并发问题,管程的特点如下: 共享数据变量只能被管程的过程访问 一个进程通过调用管程的一个过程进入管程 只能有一个进程在管程中执行,其他进程被挂起,等待进入管程4)消息传递消息传递是通过消息通信的方式进程之间相互配合,满足互斥需求。这种方式最大好处就是可以运用与分布式环境。说到消息,抽象地看有两种操作方式:send和receive。从同步方式上看分为阻塞和非阻塞两种,其组合起来有以下 情况: 阻塞send,阻塞receive。发送进程和接收进程都被阻塞,直到信息交付,同步性最好 非阻塞send,阻塞receive。最为自然的一对组合 非阻塞send,非阻塞receive。那么通过实现以上send和receive原语操作,就可达到互斥的目的,以下面伪代码为例,其中receive为阻塞的,send为非阻塞的:1. voidonlyOneThread()2. receive(box,msg);3. /*临界区*/4. send(box,msg);5. 小结以上是从操作系统的底层来看待并发问题,平常的开发过程一般不需要了解,但透过其原理,我们可以发掘一些解决并发问题的思路。只有真正了解并发产生的原因和操作系统采取的办法,我们才能理解在更高一个层次(比如高级语言编程)为什么有那些控制和措施,为什么对一些代码要做并发控制。探索并发编程(二)-写线程安全的Java代码在写Java程序的时候,何时需要进行并发控制,关键在于判断这段程序或这个类是否是线程安全的。当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步,这个类的行为仍然是正确的,那么称这个类是线程安全的。我们设计类就是要在有潜在并发问题存在情况下,设计线程安全的类。线程安全的类可以通过以下手段来满足: 不跨线程共享变量 使状态变量为不可变的 在任何访问状态变量的时候使用同步。 每个共享的可变变量都需要由唯一一个确定的锁保护。满足线程安全的一些思路1)从源头避免并发问题很多开发者一想到有并发的可能就通过底层技术来解决问题,其实往往可以通过上层的架构设计和业务分析来避免并发场景。比如我们需要用多线程或分布式集群来计算一堆客户的相关统计值,由于客户的统计值是共享数据,因此会有并发潜在可能。但从业务上我们可以分析出客户与客户之间数据是不共享的,因此可以设计一个规则来保证一个客户的计算工作和数据访问只会被一个线程或一台工作机完成,而不是把一个客户的计算工作分配给多个线程去完成。这种规则很容易设计。当你从源头就避免了并发问题的可能,下面的工作就完全可以不用担心线程安全问题。2)无状态就是线程安全多线程编程或者分布式编程最忌讳有状态,一有状态就不但限制了其横向扩展能力,也是产生并发问题的起源。当你设计的类是无状态的,那么它永远都是线程安全的。因此在设计阶段需要考虑如何用无状态的类来满足你的业务需求3)分清原子性操作和复合操作所谓原子性,是说一个操作不会被其他线程打断,能保证其从开始到结束独享资源连续执行完这一操作。如果所有程序块都是原子性的,那么就不存在任何并发问题。而很多看上去像是原子性的操作正式并发问题高灾区。比如所熟知的计数器(count+)和check-then-act,这些都是很容易被忽视的,例如大家所常用的惰性初始化模式,以下代码就不是线程安全的:1. NotThreadSafe2. publicclassLazyInitRace3. privateExpensiveObjectinstance=null;4. publicExpensiveObjectgetInstance()5. if(instance=null)6. instance=newExpensiveObject();7. returninstance;8. 9. 这段代码具体问题在于没有认识到if(instance=null)和instance = new ExpensiveObject();是两条语句,放在一起就不是原子性的,就有可能当一个线程执行完if(instance=null)后会被中断,另一个线程也去执行if(instance=null),这次两个线程都会执行后面的instance = new ExpensiveObject();这也是这个程序所不希望发生的。虽然check-then-act从表面上看很简单,但却普遍存在与我们日常的开发中,特别是在数据库存取这一块。比如我们需要在数据库里存一个客户的统计值,当统计值不存在时初始化,当存在时就去更新。如果不把这组逻辑设计为原子性的就很有可能产生出两条这个客户的统计值。在单机环境下处理这个问题还算容易,通过锁或者同步来把这组复合操作变为原子操作,但在分布式环境下就不适用了。一般情况下是通过在数据库端做文章,比如通过唯一性索引或者悲观锁来保障其数据一致性。当然任何方案都是有代价的,这就需要具体情况下来权衡。另外,java1.5以后提供了一套提供原子性操作的类,有兴趣的可以研究一下它是如何在软件层面保证原子性的。4)锁的合理使用大家都知道可以用锁来解决并发问题,但在具体使用上还有很多讲究,比如: 每个共享的可变变量都需要由一个个确定的锁保护。 一旦使用了锁,就意味着这段代码的执行就丧失了操作系统多道程序的特性,会在一定程度上影响性能 锁不能解决在分布式环境共享变量的并发问题探索并发编程(三)-Java存储模型和共享对象很多程序员对一个共享变量初始化要注意可见性和安全发布(安全地构建一个对象,并其他线程能正确访问)等问题不是很理解,认为Java是一个屏蔽内存细节的平台,连对象回收都不需要关心,因此谈到可见性和安全发布大多不知所云。其实关键在于对Java存储模型,可见性和安全发布的问题是起源于Java的存储结构。Java存储模型原理有很多书和文章都讲解过Java存储模型,其中一个图很清晰地说明了其存储结构:由上图可知,jvm系统中存在一个主内存(Main Memory或Java Heap Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。这个存储模型很像我们常用的缓存与数据库的关系,因此由此可以推断JVM如此设计应该是为了提升性能,提高多线程的并发能力,并减少线程之间的影响。Java存储模型潜在的问题一谈到缓存,我们立马想到会有缓存不一致性问题,就是说当有缓存与数据库不一致的时候,就需要有相应的机制去同步数据。同理,Java存储模型也有这个问题,当一个线程在自己工作内存里初始化一个变量,当还没来得及同步到主存里时,如果有其他线程来访问它,就会出现不可预知的问题。另外,JVM在底层设计上,对与那些没有同步到主存里的变量,可能会以不一样的操作顺序来执行指令,举个实际的例子:1. publicclassPossibleReordering2. staticintx=0,y=0;3. staticinta=0,b=0;4. publicstaticvoidmain(Stringargs)5. throwsInterruptedException6. Threadone=newThread(newRunnable()7. publicvoidrun()8. a=1;9. x=b;10. 11. );12. Threadother=newThread(newRunnable()13. publicvoidrun()14. b=1;15. y=a;16. 17. );18. one.start();other.start();19. one.join();other.join();20. System.out.println(+x+,+y+);21. 22. 由于,变量x,y,a,b没有安全发布,导致会不以规定的操作顺序来执行这次四次赋值操作,有可能出现以下顺序:出现这个问题也可以理解,因为既然这些对象不可见,也就是说本应该隔离在各个线程的工作区内,那么对于有些无关顺序的指令,打乱顺序执行在JVM看来也是可行的。因此,总结起来,会有以下两种潜在问题: 缓存不一致性 重排序执行解决Java存储模型潜在的问题为了能让开发人员安全正确地在Java存储模型上编程,JVM提供了一个happens-before原则,有人整理得非常好,我摘抄如下: 在程序顺序中, 线程中的每一个操作, 发生在当前操作后面将要出现的每一个操作之前. 对象监视器的解锁发生在等待获取对象锁的线程之前. 对volitile关键字修饰的变量写入操作, 发生在对该变量的读取之前. 对一个线程的 Thread.start() 调用 发生在启动的线程中的所有操作之前. 线程中的所有操作 发生在从这个线程的 Thread.join()成功返回的所有其他线程之前.有了原则还不够,Java提供了以下工具和方法来保证变量的可见性和安全发布: 使用synchronized来同步变量初始化。此方式会立马把工作内存中的变量同步到主内存中 使用volatile关键字来标示变量。此方式会直接把变量存在主存中而不是工作内存中 final变量。常量内也是存于主存中另外,一定要明确只有共享变量才会有以上那些问题,如果变量只是这个线程自己使用,就不用担心那么多问题了搞清楚Java存储模型后,再来看共享对象可见性和安全发布的问题就较为容易了共享对象的可见性当对象在从工作内存同步到主内存之前,那么它就是不可见的。若有其他线程在存取不可见对象就会引发可见性问题,看下面一个例子:1. publicclassNoVisibility2. privatestaticbooleanready;3. privatestaticintnumber;4. privatestaticclassReaderThreadextendsThread5. publicvoidrun()6. while(!ready)7. Thread.yield();8. System.out.println(number);9. 10. 11. publicstaticvoidmain(Stringargs)12. newReaderThread().start();13. number=42;14. ready=true;15. 16. 按照正常逻辑,应该会输出42,但其实际结果会非常奇怪,可能会永远没有输出(因为ready为false),可能会输出0(因为重排序问题导致ready=true先执行)。再举一个更为常见的例子,大家都喜欢用只有set和get方法的pojo来设计领域模型,如下所示:1. NotThreadSafe2. publicclassMutableInteger3. privateintvalue;4. publicintget()returnvalue;5. publicvoidset(intvalue)this.value=value;6. 但是,当有多个线程同时来存取某一个对象时,可能就会有类似的可见性问题。为了保证变量的可见性,一般可以用锁、synchronized关键字、volatile关键字或直接设置为final共享变量发布共享变量发布和我们常说的发布程序类似,就是说让本属于内部的一个变量变为一个可以被外部访问的变量。发布方式分为以下几种: 将对象引用存储到公共静态域 初始化一个可以被外部访问的对象 将对象引用存储到一个集合里安全发布和保证可见性的方法类似,就是要同步发布动作,并使发布后的对象可见。线程安全其实当我们把这些变量封闭在本线程内访问,就可以从根本上避免以上问题,现实中存在很多例子通过线程封闭来安全使用本不是线程安全的对象,比如: swing的可视化组件和数据模型对象并不是线程安全的,它通过将它们限制到swing的事件分发线程中,实现线程安全 JDBC Connection对象没有要求为线程安全,但JDBC的存取模式决定了一个Connection只会同时被一个线程使用 ThreadLocal把变量限制在本线程中共享探索并发编程(四)-Java并发工具基于线程安全的一些原则来编程当然可以避免并发问题,但不是所有人都能写出高质量的线程安全的代码,并且如果代码里到处都是线程安全的控制也极大地影响了代码可读性和可维护性。因此,Java平台为了解决这个问题,提供了很多线程安全的类和并发工具,通过这些类和工具就能更简便地写线程安全的代码。归纳一下有以下几种: 同步容器类 并发容器类 生产者和消费者模式 阻塞和可中断方法 Synchronizer这些类和方法的使用都可以从JDK DOC查到,但在具体使用中还是有很多问题需要注意同步容器类同步容器类就是一些经过同步处理了的容器类,比如List有Vector,Map有Hashtable,查看其源码发现其保证线程安全的方式就是把每个对外暴露的存取方法用synchronized关键字同步化,这样做我们立马会想到有以下问题:1)性能有问题同步化了所有存取方法,就表明所有对这个容器对象的操作将会串行,这样做来得倒是干净,但性能的代价也是很可观的2)复合操作问题同步容器类只是同步了单一操作,如果客户端是一组复合操作,它就没法同步了,依然需要客户端做额外同步,比如以下代码:1. publicstaticObjectgetLast(Vectorlist)2. intlastIndex=list.size()-1;3. returnlist.get(lastIndex);4. 5. publicstaticvoiddeleteLast(Vectorlist)6. intlastIndex=list.size()-1;7. list.remove(lastIndex);8. getLast和deleteLast都是复合操作,由先前对原子性的分析可以判断,这依然存在线程安全问题,有可能会抛出ArrayIndexOutOfBoundsException的异常,错误产生的逻辑如下所示:解决办法就是通过对这些复合操作加锁3)迭代器并发问题Java Collection进行迭代的标准时使用Iterator,无论是使用老的方式迭代循环,还是Java 5提供for-each新方式,都需要对迭代的整个过程加锁,不然就会有Concurrentmodificationexception异常抛出。此外有些迭代也是隐含的,比如容器类的toString方法,或containsAll, removeAll, retainAll等方法都会隐含地对容器进行迭代并发容器类正是由于同步容器类有以上问题,导致这些类成了鸡肋,于是Java 5推出了并发容器类,Map对应的有ConcurrentHashMap,List对应的有CopyOnWriteArrayList。与同步容器类相比,它有以下特性: 更加细化的锁机制。同步容器直接把容器对象做为锁,这样就把所有操作串行化,其实这是没必要的,过于悲观,而并发容器采用更细粒度的锁机制,保证一些不会发生并发问题的操作进行并行执行 附加了一些原子性的复合操作。比如putIfAbsent方法 迭代器的弱一致性。它在迭代过程中不再抛出Concurrentmodificationexception异常,而是弱一致性。在并发高的情况下,有可能size和isEmpty方法不准确,但真正在并发环境下这些方法也没什么作用。 CopyOnWriteArrayList采用写入时复制的方式避开并发问题。这其实是通过冗余和不可变性来解决并发问题,在性能上会有比较大的代价,但如果写入的操作远远小于迭代和读操作,那么性能就差别不大了生产者和消费者模式大学时学习操作系统多会为生产者和消费者模式而头痛,也是每次考试肯定会涉及到的,而Java知道大家很憷这个模式的并发复杂性,于是乎提供了阻塞队列(BlockingQueue)来满足这个模式的需求。阻塞队列说起来很简单,就是当队满的时候写线程会等待,直到队列不满的时候;当队空的时候读线程会等待,直到队不空的时候。实现这种模式的方法很多,其区别也就在于谁的消耗更低和等待的策略更优。以LinkedBlockingQueue的具体实现为例,它的put源码如下:1. publicvoidput(Ee)throwsInterruptedException2. if(e=null)thrownewNullPointerException();3. intc=-1;4. finalReentrantLockputLock=this.putLock;5. finalAtomicIntegercount=this.count;6. putLock.lockInterruptibly();7. try8. try9. while(count.get()=capacity)10. notFull.await();11. catch(InterruptedExceptionie)12. notFull.signal();/propagatetoanon-interruptedthread13. throwie;14. 15. insert(e);16. c=count.getAndIncrement();17. if(c+10时,所有调用其await方法的线程都需等待,当通过其countDown方法将count降为0时所有等待的线程将会被唤起。使用实例如下所示:1. publicclassTestHarness2. publiclongtimeTasks(intnThreads,finalRunnabletask)3. throwsInterruptedException4. finalCountDownLatchstartGate=newCountDownLatch(1);5. finalCountDownLatchendGate=newCountDownLatch(nThreads);6. for(inti=0;inThreads;i+)7. Threadt=newThread()8. publicvoidrun()9. try10. startGate.await();11. try12. task.run();13. finally14. endGate.countDown();15. 16. catch(InterruptedExceptionignored)17. 18. ;19. t.start();20. 21. longstart=System.nanoTime();22. startGate.countDown();23. endGate.await();24. longend=System.nanoTime();25. returnend-start;26. 27. 2)SemaphoreSemaphore类实际上就是操作系统中谈到的信号量的一种实现,其原理就不再累述,可见探索并发编程-操作系统篇具体使用就是通过其acquire和release方法来完成,如以下示例:1. publicclassBoundedHashSet2. privatefinalSetset;3. privatefinalSemaphoresem;4. publicBoundedHashSet(intbound)5. this.set=Collections.synchronizedSet(newHashSet();6. sem=newSemaphore(bound);7. 8. publicbooleanadd(To)throwsInterruptedException9. sem.acquire();10. booleanwasAdded=false;11. try12. wasAdded=set.add(o);13. returnwasAdded;14. 15. finally16. if(!wasAdded)17. sem.release();18. 19. 20. publicbooleanremove(Objecto)21. booleanwasRemoved=set.remove(o);22. if(wasRemoved)23. sem.release();24. returnwasRemoved;25. 26. 3)关卡关卡和闭锁类似,也是阻塞一组线程,直到某件事情发生,而不同在于关卡是等到符合某种条件的所有线程都达到关卡点。具体使用上可以用CyclicBarrier来应用关卡以上是Java提供的一些并发工具,既然是工具就有它所适用的场景,因此需要知道它的特性,这样才能在具体场景下选择最合适的工具。探索并发编程(五)-Java多线程开发技巧很多开发者谈到Java多线程开发,仅仅停留在new Thread(.).start()或直接使用Executor框架这个层面,对于线程的管理和控制却不够深入,通过读Java并发编程实践了解到了很多不为我知但又非常重要的细节,今日整理如下。不应用线程池的缺点有些开发者图省事,遇到需要多线程处理的地方,直接new Thread(.).start(),对于一般场景是没问题的,但如果是在并发请求很高的情况下,就会有些隐患: 新建线程的开销。线程虽然比进程要轻量许多,但对于JVM来说,新建一个线程的代价还是挺大的,决不同于新建一个对象 资源消耗量。没有一个池来限制线程的数量,会导致线程的数量直接取决于应用的并发量,这样有潜在的线程数据巨大的可能,那么资源消耗量将是巨大的 稳定性。当线程数量超过系统资源所能承受的程度,稳定性就会成问题制定执行策略在每个需要多线程处理的地方,不管并发量有多大,需要考虑线程的执行策略 任务以什么顺序执行 可以有多少个任何并发执行 可以有多少个任务进入等待执行队列 系统过载的时候,应该放弃哪些任务?如何通知到应用程序? 一个任务的执行前后应该做什么处理线程池的类型不管是通过Executors创建线程池,还是通过Spring来管理,都得清楚知道有哪几种线程池: FixedThreadPool:定长线程池,提交任务时创建线程,直到池的最大容量,如果有线程非预期结束,会补充新线程 CachedThreadPool:可变线程池,它犹如一个弹簧,如果没有任务需求时,它回收空闲线程,如果需求增加,则按需增加线程,不对池的大小做限制 SingleThreadExecutor:单线程。处理不过来的任务会进入FIFO队列等待执行 SecheduledThreadPool:周期性线程池。支持执行周期性线程任务其实,这些不同类型的线程池都是通过构建一个ThreadPoolExecutor来完成的,所不同的是corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory这么几个参数。具体可以参见JDK DOC。线程池饱和策略由以上线程池类型可知,除了CachedThreadPool其他线程池都有饱和的可能,当饱和以后就需要相应的策略处理请求线程的任务,ThreadPoolExecutor采取的方式通过队列来存储这些任务,当然会根据池类型不同选择不同的队列,比如FixedThreadPool和SingleThreadExecutor默认采用的是无限长度的LinkedBlockingQueue。但从系统可控性讲,最好的做法是使用定长的ArrayBlockingQueue或有限的LinkedBlockingQueue,并且当达到上限时通过ThreadPoolExecutor.setRejectedExecutionHandler方法设置一个拒绝任务的策略,JDK提供了AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy几种策略,具体差异可见JDK DOC线程无依赖性多线程任务设计上尽量使得各任务是独立无依赖的,所谓依赖性可两个方面: 线程之间的依赖性。如果线程有依赖可能会造成死锁或饥饿 调用者与线程的依赖性。调用者得监视线程的完成情况,影响可并发量当然,在有些业务里确实需要一定的依赖性,比如调用者需要得到线程完成后结果,传统的Thread是不便完成的,因为run方法无返回值,只能通过一些共享的变量来传递结果,但在Executor框架里可以通过Future和Callable实现需要有返回值的任务,当然线程的异步性导致需要有相应机制来保证调用者能等待任务完成,关于Future和Callable的用法见下面的实例就一目了然了:1. publicclassFutureRenderer2. privatefinalExecutorServiceexecutor=.;3. voidrenderPage(CharSequencesource)4. finalListimageInfos=scanForImageInfo(source);5. CallableListtask=6. newCallableList()7. publicListcall()8. Listresult9. =newArrayList();10. for(ImageInfoimageInfo:imageInfos)11. result.add(imageInfo.downloadImage();12. returnresult;13. 14. ;15. FutureListfuture=executor.submit(task);16. renderText(source);17. try18. ListimageData=future.get();19. for(ImageDatadata:imageData)20. renderImage(data);21. catch(InterruptedExceptione)22. /Re-assertthethreadsinterruptedstatus23. Thread.currentThread().interrupt();24. /Wedontneedtheresult,socancelthetasktoo25. future.cancel(true);26. catch(ExecutionExceptione)27. throwlaunderThrowable(e.getCause();28. 29. 30. 以上代码关键在于List imageData =future.get();如果Callable类型的任务没有执行完时,调用者会阻塞等待。不过这样的方式还是得谨慎使用,很容易造成不良设计。另外对于这种需要等待的场景,就需要设置一个最大容忍时间timeout,设置方法可以在future.get()加上timeout参数,或是再调用ExecutorService.invokeAll加上timeout参数线程的取消与关闭一般的情况下是让线程运行完成后自行关闭,但有些时候也会中途取消或关闭线程,比如以下情况: 调用者强制取消。比如一个长时间运行的任务,用户点击cancel按钮强行取消 限时任务 发生不可处理的任务 整个

温馨提示

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

评论

0/150

提交评论