文章转自:http://www.tuicool.com/articles/fA7rMn
实际工作中的三类程序适用于以并发的形式来提速:
1. 服务程序:同时响应多个用户请求
2. 计算密集型程序:并发计算,将问题拆分为子任务、并发执行各子任务并最终将子任务的结果汇总合并。
3. IO密集型程序(阻塞型):常需要阻塞等待的程序,比如说因为网络环境阻塞等待,因为IO读取阻塞等待。当一个任务阻塞在IO操作上时,我们可以立即切换执行其他任务或启动其他IO操作请求,这样并发就可以帮助我们有效地提升程序执行效率。
对于IO密集型程序,我们要用并发来获得执行效率的大幅提升时,首先要思考两个问题:如何估计需要创建多少个线程以及如何分解问题。这里也涉及到如何估算并发带来的性能提升的程度。
1. 确定线程数
确定线程数首先需要考虑到系统可用的处理器核心数:
Runtime.getRuntime().availableProcessors();
应用程序最小线程数应该等于可用的处理器核数。如果所有的任务都是计算密集型的,则创建处理器可用核心数这么多个线程就可以了,这样已经充分利用了处理器,也就是让它以最大火力不停进行计算。创建更多的线程对于程序性能反而是不利的,因为多个线程间频繁进行上下文切换对于程序性能损耗较大。
但如果任务都是IO密集型的,那我们就需要创建比处理器核心数大几倍数量的线程。为何?当一个任务执行IO操作时,线程将被阻塞,于是处理器可以立即进行上下文切换以便处理其他就绪线程。如果我们只有处理器核心数那么多个线程的话,即使有待执行的任务也无法调度处理了。
因此,线程数与我们每个任务处于阻塞状态的时间比例相关。加入任务有50%时间处于阻塞状态,那程序所需线程数是处理器核心数的两倍。我们可以计算出程序所需的线程数,公式如下:
线程数=CPU可用核心数/(1 - 阻塞系数),其中阻塞系数在在0到1范围内。
计算密集型程序的阻塞系数为0,IO密集型程序的阻塞系数接近1。
确定阻塞系数,我们可以先试着猜测,或者采用一些性能分析工具或java.lang.management API 来确定线程花在系统IO上的时间与CPU密集任务所耗的时间比值。
2. 确定任务的数量
我们常常希望各个子任务的工作量是均匀分布的,这样每个线程的负载都差不多。但这通常会花大量的精力去做问题分解。事实证明,把任务尽可能拆解成细粒度,让它远比线程数多,让处理器一直不停地工作,是最实惠的方法。
接下来看一看IO密集型应用程序使用并发的一个实用例子:
这是一个求用户当前股票市值的例子,我们假设在获取用户持有股票信息之后,需向雅虎请求获得该股票的当前市值。
父类,定义了读取输入数据的方式与计时方法,计时方法中调用了solve方法进行处理,并获取执行时间。solve方法由子类实现。
public abstract class AbstractSolver { public static Map<String,Integer> readTickers() throws IOException { //从文件或数据库读取某用户持有的股票ID与持股数 并以Map<股票ID,持股数>的形式返回 } public void timeAndCompute() { final long start = System.nanoTime(); final Map<String,Integer> stocks = readTrickers(); final double result = solve(stocks); final long end = System.nanoTime(); System.out.printf("Number of primes under %d is %d\n", number, numberOfPrimes); System.out.println("Time (seconds) taken is " + (end - start) / 1.0e9); } public abstract int solve(final Map<String, Integer> stocks) throws InterruptedException,ExcecutionException,IOException; }
public class ConcurrentSolveIO extend AbstractSolver { public double solve(final Map<String,Integer> stocks)throws InterruptedException,ExcecutionException { final int numberOfCores = Runtime.getRuntime().availableProcessors(); //获得核心数 final double blockingCoefficient = 0.9;//阻塞系数 final int poolSize = (int)(numberOfCores / (1 - blockingCoefficient)); //求得线程数大小 System.out.println("Number of cores is " + numberOfCores); System.out.println("PoolSize is " + poolSize); final List<Callable<Double>> partitions = new ArrayList<Callable<Double>>(); //一系列的任务集合 for(final String ticker : stocks.keySet()){ partitions.add(new Callable<Double>(){ public Double call() throws Exception{ return stocks.get(triker) * YahooFinance.getPrice(tiker);//YahooFinance.getPrice(tiker)获得股票市值 } }); } final ExecutorService executorPool = Executors.newFixedThreadPool(poolSize);//创建线程池 Java自带的newFixedThreadPool可以满足我们生成指定线程数的线程池的需要 如果任务数大于线程数,则所有任务将排队等待被执行 final List<Future<Double>> valuesOfStocks = ExecutorPool.invokeAll(partitions, 10000, TimeUnit.SECONDS);//设置任务 double netAssetValue = 0.0; for(final Future<Double> valuesOfStocks : valueOfStocks){ //在任务全部执行完成之后,合计结果 netAssetValue += valuesOfAStock.get(); } executorPool.shutdown(); //关闭线程池 return netAssetValue; } public static void main(final String[] aargs) throws ExecutionException, InterruptedException, IOException{ new ConcurrentSolveIO.timeAndComputeValue(); //timeAndComputeValue方法继承自AbstractIOSolver,其中调用了solve()方法并在前后加上计时,用于检测性能优化的情况 } }