并发数
并发数和2个因素有关,
一是系统可用的
处理器核数,这个值可以通过硬件查询得到,也可以通过如下代码得到:
class="java" name="code">Runtime.getRuntime().availableProcessors();
二是并发任务的类型,任务类型一般分为IO密集型和CPU密集型
- I/O 密集的任务通常行为是反复去读写磁盘文件,执行任务时,观察 CPU 占用的话多数时间都是出于 I/O wait 状态。这种情况下,当一个任务阻塞在IO操作上时,我们可以立即切换执行其他任务或启动其他IO操作请求,这样并发就可以帮助我们有效地提升程序执行效率。
- CPU 密集则是大量 CPU 时间都用于进行计算。这种情况下,可以将问题拆分为子任务、并发执行各子任务并最终将子任务的结果汇总合并以提升效率。
说是有这么分,但是90%以上的的任务都是既有I/O操作,又有CPU运算的,因此我们在设定并发任务的时候,更重要的是去关注
线程等待时间和线程CPU时间,这2个时间可以通过JMX从线程的ThreadInfo中获取,具体如何做的方式:http://stackoverflow.com/questions/1680865/how-
can-i-monitor-cpu-usage-per-
thread-of-a-java-application-in-a-linux-multipro
有了上面的2个数据,可以大致估算一个并发的线程数:
引用最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU核数
这只是一个大致估算,可以作为一个起点,在实际生产环境中,我们需要
结合系统真实情况(比如是IO密集型或者是CPU密集型或者是纯
内存操作)和硬件环境(CPU、内存、
硬盘读写速度、网络状况等)来不断调整,寻找到系统的一个最佳性能点。
并发的执行策略
要并发执行任务,首先要做的就是拆分任务,把一个大的过程或者任务拆分成很多小的工作单元,拆分后的每一个工作单元可能相关、也可能无关,但
他们在一定程度上可以充分利用CPU的特性并发的执行,从而提高并发性(性能、响应时间、吞吐量等)。
在拆分任务时,尽量把任务拆分得独立,不相互影响,但是在实际的业务场景中,工作单元之间或多或少会存在着一定的依赖关系,对于有依赖关系以及资源竞争的工作单元就涉及到任务的调度和负载均衡。另外,在并发量超过资源承载能力的情况下,也涉及各个工作单元的调度。这就涉及到并发任务的执行策略问题。
一个并发的执行策略主要包含下面几个部分:
- 任务在如何执行
- 任务以什么顺序执行(FIFO/LIFO/优先级等)
- 同时有多少个任务并发执行
- 允许有多少个个任务进入执行队列
- 系统过载时选择放弃哪一个任务,如何通知应用程序这个动作
- 任务执行的开始、结束应该做什么处理
有了思路,来看一下Java世界是怎么实现的?
任务如何执行?在Java的世界,所有的并发任务都必须放到线程Thread中执行,不管你是
Runnable/Callable/TimerTask中的哪个,最终执行任务的就是Thread类,因此拆分任务就是形成一个个的小的Thread执行的任务。使用Callable可以获取到任务执行的结果,另外还有Future类可以帮助阻塞当前线程直到
异步任务返回结果。
任务以何种顺序执行?在Java中控制并发任务的顺序有两部分,一是线程的优先级,但是这个不能保证一定高优先级的比低优先级的先,只是获得CPU时间片的概率会更高一点,二是,任务等待队列的顺序,即通过有序的分发任务来保证线程的执行顺序。
同时有多少个任务并发执行? 这个通过线程池大小来控制,开发者可以通过上一小节中的并发数估算值来设置线程池大小。而JDK1.5的并发包中提供了辅助类Executors,该类可以非常方便的设置线程池大小,同时解决了向线程池提交任务的入口问题。顺道说一句,其还提供了ScheduledExecutor
Service来解决重复调用任务的问题。
多少个个任务进入执行队列?这个值可以通过队列的长度来控制,JDK包中提供了各种BlockingQueue的实现,包括有容量
限制的ArrayBlockingQueue, 可
自定义顺序的PriorityBlockingQueue等等。
系统过载时选择放弃哪一个任务,如何通知应用程序这个动作?Java提供了RejectedExecutionHandler来解决此问题。JDK默认提供了四种方式处理。 DiscardPolicy:直接丢弃当前将要加入队列的任务本身; DiscardOldestPolicy:丢弃任务队列中最旧任务; AbortPolicy:抛出
异常RejectedExecutionException; CallerRunsPolicy:调用者线程执行
任务执行的开始、结束应该做什么处理? JDK中首先提供了CompletionService/ExecutorCompletionService类来整合有返回值的Callable任务的所有结果,另外,还可以定制ThreadPoolExecutor的beforeExecute方法和afterExecute()方法。
综上所述,要设置一个合适的并发执行策略是很复杂的,所幸的是Doug Lea大爷在JDK1.5中为大家带来的并发包,提供了大量的辅助类帮助开发者简化这些配置,感恩。
并发模型
对于并发模型, 主要是2派观点,一派是基于内存共享的Thread模型,一派是基于消息传递的Actor模型(Event-Driven模型)。这两者的主要特征如下:
EventsThreadsevent handlersmonitors events accepted by a handler
functions exported by a moduleSendMessage/AwaitReplyprocedure call,or fork/joinSendReplyreturn from procedurewaiting for messageswaiting on condition variables
这两派的观点争执有好几十年了,这边Standford大学1995年讲述了《为什么Events优于Thread?》(http://web.stanford.edu/~ouster/cgi-bin/papers/threads.pdf), 主要观点如下:
引用现在我们有点滥用Thread,Thread是基于共享内存做
同步的,同时线程池的执行顺序都是预先设定好的,这就会导致死锁,出了问题很难调试,编程模型被破坏,无法使用回调等问题,而Event-Driven的编程模型,因为没有共享状态,就不需要担心死锁,同步的问题, 而且因为每个Event Handler都有自己处理域的所有信息,因此bug的调试变得简单,另外,无锁也使得在单个CPU上Event模型的性能高于Thread模型。另外,作者也承认Thread模型也有优于Event模型的地方,比方说在多个CPU上的并行能力,另外,Event模型中当某个Event Handler耗时过长时可能导致系统假死等。最终作者的结论是我们应该首先考虑使用Event模型,在一些性能要求特别苛刻的场景下,才考虑使用高端的Thread技巧,去把
多核的性能发挥到
极致。
那边Berkeley大学在2003年发表了论文《
Why Events Are A Bad Idea (for high-concurrency servers)》:http://wenku.baidu.com/link?url=UmFNZN29i3XsMkUfzSUwflOH1Y_qPNbpz4H5zloshCJOC61RfKCgm1P_2iNZtdAKvTMCWsLcHnObfT-950nTlku7GZmuEURgQJXYvTXUELG 观点如下:
引用其实事件驱动并没有在功能上有比线程有什么优越之处,但编程要麻烦很多,而且特别容易出错。线程的问题,无非是目前的实现的原因。一个是线程占的资源太大,一创建就分配几个MB的stack,一般的机器能支持的线程大受限制。针对这点,可以用自动扩展的stack,创建的先少分点,然后动态增加。第二个是线程的切换负担太大,Linux中实际上process和thread是一回事,区别就在于是否共享地址空间。解决这个问题的办法是用轻量级的线程实现,通过合作式的办法来实现共享系统的线程。这样一个是切换的花费很少,另外一个可以维护比较小的stack。他们用coroutine和nonblocking I/O(用的是poll()+thread pool)实现了一个原型系统,证明了性能并不比事件驱动差。
接着Berkeley大学在2006年自己打脸又发表了论文《The Problem With Thread》(http://wenku.baidu.com/link?url=eCpQcSFqxxIqaRfsoeDcycIVJI0Ff5hqFtU3OWquldRg5ahQx-jQmh2aKj4-H91EP8dhCCErOiKMGTqsuYznVhNDevimi7e2WorofDBRVcm), 表述的观点如下:
引用目前,程序的模型基本上是基于顺序执行。顺序执行是确定性的,容易保证正确性。而人的思维方式也往往是单线程的。线程的模式是强行在单线程,顺序执行的基础上加入了并发和不确定性。这样程序的正确性就很难保证。线程之间的同步是通过共享内存来实现的,你很难来对并发线程和共享内存来建立数学模型,其中有很大的不确定性,而不确定性是编程的巨大敌人。作者以他们的一个项目中的经验来说明,保证多线程的程序的正确性,几乎是不可能的事情。首先,很多很简单的模式,在多线程的情况下,要保证正确性,需要注意很多非常微妙的细节,否则就会导致deadlock或者race condition。其次,由于人的思维的限制,即使你采取各种消除不确定的办法,比如monitor,transactional memory,还有promise/future,等等机制,还是很难保证面面俱到。以作者的项目为例,他们有计算机科学的专家,有最聪明的
研究生,采用了整套软件工程的流程:design review, code review, regression tests, automated code coverage metrics,认为已经消除了大多数问题,不过还是在系统运行4年以后,出现了一个deadlock。作者说,很多多线程的程序实际上存在并发
错误,只不过由于硬件的并行度不够,往往不显示出来。随着硬件的并行度越来越高,很多原来运行完好的程序,很可能会发生问题。
我自己的体会也是,程序NPE,core dump都不怕,
最怕的就是race condition和deadlock,因为这些都是不确定的(non-deterministic),往往很难重现。
对于这两种模型,个人觉得见仁见智了,条条大路通罗马,关键是你通过对自己的问题分析觉得它适合于哪一种模型。