Java高并发编程——为IO密集型应用设计线程数与划分任务

文章转自: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;
}

ConcurrentSolveIO:具体实现。
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()方法并在前后加上计时,用于检测性能优化的情况
	}
}

这个例子中,各个线程之间不需要访问临界区,因为在获得结果后的逻辑较简单,我们在所有任务停止之后一并处理结果。如果在收到外部接口的相应之后还要进行大量计算的话,则最好是在一有可用结果返回时就立即处理。这用JDK中自带的CompletionService可以实现这一功能。

你可能感兴趣的:(java,并发,线程,线程池,性能优化)