分工原则

从顺序到并发

如何估计需要创建多少个线程、如何分解问题以及如何估算性能提升的程度。

确定线程数

阻塞系数 : 任务有处于阻塞状态占总处理时间的百分比

计算密集型任务的阻塞系数为0 , 而IO密集型任务的阻塞系数则接近1

可以采用一些性能分析工具或java.lang.management API来确定线程花在系统IO操作上的时间与CPU密集任务所耗时间的比值

线程数 = CPU可用核心数 / (1-阻塞系数) , 其中阻塞系数的取值在0和1之间

确定任务的数量

在解决问题的过程中使处理器一直保持忙碌状态比将负载均摊到每个子任务要实惠得多。从处理问题的角度来讲,我们需要保证:只要还有待完成的任务,就不可能有空闲的处理器核心。所以,斤斤计较如何将负载平摊到每个子任务上,不如将任务拆得比线程数多,以使处理器一直不停工作来得更有效。我们应该尽可能地对问题进行拆分,以产生足够多的工作供所有处理器可用核心来执行。

在IO密集型应用程序中使用并发技术

IO密集型应用程序的阻塞系数一般都很大,所以程序开的线程数通常需要超过处理器可用核心数。

由于对Web服务的请求大部分时间都花在等待服务器响应上了,所以阻塞系数会相当高,因此程序需要开的线程数可能是处理器核心数的若干倍。假设阻塞系数是0.9,即每个任务90%的时间处于阻塞状态而只有10%的时间在干活,则在双核处理器上我们就需要开20个线程。如果有很多个任务要处理的话,我们可以在8核处理器上开到80个线程来处理该任务。

在计算密集型应用程序中使用并发技术

相对于IO密集型应用程序,处理器核心数对计算密集型应用程序的加速效果影响更大。

需要确保每个处理器核心都能分摊到均匀的工作负载才会对CPU有更高的利用率。

总结:

  1. 子任务的划分数应不少于处理器核心数,在子任务划分数超过一定数量之后,再增加子问题划分数对于性能的提升将十分有限。

  2. 线程数多于处理器核心数对性能提升毫无帮助。

  3. 设计一个问题拆分的简单方法并观察该方法是否能够均衡利用所有处理器核心。

Java的旧线程API现在还有用武之地吗?

问题:

  1. 由于线程不允许重新启动,所以一旦线程执行完了我们就必须把Thread类的实例丢掉。存在复用问题。

  2. synchronized关键字的粒度太粗。该关键字没有提供当线程没获取到锁的情况下的超时逻辑,而且也不允许对互斥区域的并发读。关于线程安全方面的单元测试也很难进行。

  3. 像wait()和notify()这样的函数都需要进行线程间同步,我们很难判断何时才是使用它们进行线程间通信的正确时机。而join()函数则使我们的注意力都集中在处理线程消亡的逻辑上,从而忽视了对即将结束的任务的处理(任务完成并不代表线程消亡,反之亦然。)。

解决方案:

  1. 以前代码中使用Thread类及其方法的地方,现在都可以考虑用ExecutorService类及其相关类来替换。

在现代的并发API中,Executors类被用作创建不同类型线程池的工厂类,其创建的线程池都可以用ExecutorService接口进行管理。我们甚至可以用它来创建所有任务都在一个线程内逐个调度执行的单线程的线程池。而固定大小的线程池则允许我们配置线程池的大小,并能调度可用线程并发执行我们丢给它的任务。如果任务数大于线程数,则所有任务将排队执行,且只要有可用线程则等待队列中的任务就可以立即被执行。此外,带缓存的线程池会按需创建线程,并尽可能地复用已经创建好的线程。如果某个线程空闲超过1分钟,则该闲置线程将被关停。

  1. 如果想要更好地控制加锁的过程,则最好使用Lock接口及其方法。

  2. 以前代码中使用wait/notify方法的地方,现在都可以使用像CyclicBarrier和CountdownLatch这样的同步工具来替换。

转载请注明:转载自srzyhead的博客(https://srzyhead.github.io)

本文链接地址: java虚拟机并发编程 (2-分工原则)