Java并发编程实战笔记_并发任务执行

来源:互联网 发布:cad mac中文破解版2016 编辑:程序博客网 时间:2024/04/30 07:35
        任务执行有2种处理机制,串行执行和并行执行。
        串行:同一时刻只能执行一个任务,程序简单,安全性高,不涉及同步等情况,缺点也显而易见,无法提高吞吐量和响应速度,适合任务数量很少并且执行时间很长时,或者只为单个用户使用,并且该用户每次只发出一个请求。
        并行:同一时刻能执行多个任务,能提高CPU的利用率(尤其多核CPU情况下),提高吞吐量和响应速度等很多好处,但是也有缺点,程序设计复杂(各种同步,各种资源访问,线程通信等),问题难以发现和排查,额外资源开销(线程管理,线程池,CPU竞争等)等等。
        当然,不能绝对的说哪个好哪个坏,程序只有适不适合,没有好坏。多线程并发执行更适合大部分场景,尤其在现在多核的时代的情况下,下面说说并行任务执行的设计。
        并行执行就要创建新线程,传统的方式中我们显式的创建线程:
        
        创建线程开销:线程的创建和销毁都是需要代价的,尤其处理大量非常简单的请求情况下(比如每秒上百次请求,但是每个请求只处理0.1s就结束了,大部分web服务都是这样的),创建和销毁线程花费时间比执行任务时间还大,这样“为每个任务分配一个线程”方法就有点傻B了。
        这种“为每个任务分配一个线程”的方法用在测试程序还可以,只要请求的增长速度不会超过服务器处理请求速度就行,但是生产环境这样可能会带来一些问题,我们说主要的3点:
        资源消耗:线程太多会占用更多内存,也会竞争CPU降低性能,垃圾回收也是个麻烦事,只要线程数量能保证CPU始终忙碌(比如始终使用率90%左右)即可。
        稳定性:可创建线程的最大数量受很多方面制约,比如操作系统,JVM参数等,超了容易OutOfMemoryError,线程太多也容易导致某些操作执行失败,莫名其妙的Error让你崩溃。
        也就是说在一定范围内,增加线程有助于提高吞吐量,但是再多就可能导致性能下降。其实这就是一个执行策略问题,并行任务要考虑很多东西,比如:
                1、在什么线程中执行?
                2、任务按照什么顺序执行(FIFO、LIFO、优先级等)?
                3、同时并发执行任务最大有多少?
                4、等待队列最多能等待多少个?
                5、如果系统由于过载而拒绝任务时,选择哪个任务?如何通知被拒绝的任务?
                6、执行一个任务前后,应该进行哪些操作?
        其实,每当看到new Thread()这种形式,就应该想到使用线程池(仅仅只是想到,具体用不用要看实际环境),线程池的概念不说了,好坏优劣也不说了,至少上面提到的执行策略都可以进行配置。
        Java中创建线程池有很多办法,比如使用ThreadPool,或者自己实现一个等等,其实JDK中是有现成的,更方便,就是Executors。Executors提供了10个方法创建线程池,其实就是5类方法,每类2个方法,一个提供自定义ThreadFactory来创建线程,一个不提供,下面说说这5类方法:
        
        说完创建就要继续说回收,也就是线程池的生命周期。你会发现所有的newXXXThreadPool都是返回的ExecutorService,没错,线程池的生命周期就由ExecutorService控制,ExecutorService接口继承了Executor接口,增加了控制生命周期的方法,线程池有3个状态:运行(running),关闭(shutdown),终止(terminate):
        
        下面这段代码,使用awaitTermination方法,每隔1s检查一下线程池是否结束,没结束继续等待:
        
        顺便说说ExecutorService扩展的其他方法,除了扩展生命周期方法外,还提供了新的调用执行任务的方法,原始的Executor只有execute方法来提交任务执行,ExecutorService新增了3类方法,每类中有2-3个方法,使用起来基本一致(就是有延迟没延迟,有的用Runnable,有的Callable等):
        
        再简单说说延迟任务和周期任务,JDK中有个Timer可以延迟、周期执行,Timer有个缺陷,就是执行任务时间长的时候会不精确,比如1分钟执行1次,1次执行10分钟,那么第2次执行时,会连续调用10次(固定速率),或者从执行结束后重新计算时间(固定延迟),就这2种策略,比较麻烦。你可能会想,我启用新线程执行,只是定时调用,不用等待执行任务的线程,尼玛这不就是ScheduledThreadPool线程池做的吗,所以涉及延迟、周期执行线程的问题,优先考虑ScheduledThreadPool。
        并行任务执行还涉及任务分配异构的问题,比如一组人负责洗碗,一组人负责烘干,如何分配任务,让双方的人都忙碌,两组人的比例如何分配,以后增加人时,如何保证相互不影响,这种情况没考虑清楚,并行任务不但复杂,而且并不一定有效率的提升。再比如两个任务分配给A、B两个人,但是A执行时间是B的10倍,那么并行比串行的速度仅仅提高了9%,而且在分解任务、分配任务时还需要一定的任务协调开销,而且这种开销不能超过节约出来的性能(比如上面的9%)。任务分配确实是一个问题,而且有非常多的情况,需要好好思考,下面介绍一个CompletionService类,是解决并发执行,顺序获取结果的一个接口,其实就是相当于Executor加上BlockingQueue,当子线程并发了一系列的任务以后,主线程需要实时地取回子线程任务的返回值并同时顺序地处理这些返回值,谁先返回就先处理谁,这时使用CompletionService就非常方便了(其实可以自己用Executor加上BlockingQueue实现,也比较简单,不过有JDK提供的方法,优雅方便)。
        在说一个实用的功能和场景,前面我们使用Future的get方法获取结果,可以阻塞线程,一有结果马上返回,非常方便,其实get方法还可以设置超时时间,如果超时则往下执行,使用场景是某个任务在指定时间内获取不到结果,就不再需要它的结果了,放弃这个任务了。比如网站页面的广告,要从广告商的服务器获取广告,2s获取不到就不要了,这样也不影响性能。Future的get(long timeout,TimeUnit unit)方法如果超时没返回结果,会抛出TimeoutException异常,如果要取消执行,可以catch后执行Futrue的canel方法。比如:
        
        这个是单个的超时例子,如果是一组线程执行任务,超时后取消没执行完的,可以考虑使用上面提到的invokeAll方法,非常好用。
        本次主要针对任务执行简单讨论,并行执行使程序复杂,Executor框架将任务提交和任务执行策略分离,解耦开来,同时支持多种不通类型的策略。要想将应用程序分解为不同的任务时获得最大好处,就必须定义清晰的任务边界,某些程序任务边界比较明显,有些就要根据实际情况自行分析了。
0 0
原创粉丝点击