第一章、The power and perils of concurrency(并发编程的威力和风险)
1.1 Threads: The Flow of Execution(线程:程序执行流程)
线程可以看成是进程中的一个执行流程。当运行一个程序时,进 程中至少有一个线程在执行。我们可以通过创建线程的方式来启动其他的执行流程以并发的处理任务。
1.2 The Power of Concurrency(并发的威力)
我们对并发感兴趣的原因有两点:并发编程可以提高应用的响应速度和提升用户体验。
需要使用并发的几种场景:Services(服务程序)、computationally intensive applications(计算密集型应用程序)、datacrunching applications(数据分析处理应用程序)。
总之并发可以帮助我们提升应用的响应(responsive),降低延迟(latency),提高系统的吞吐量(throughput
)。
1.3 The Perils of Concurrency(并发的风险)
并发的3大问题:饥饿(starvation),死锁(deadlock),竞争条件(race condition)。前两个问题比较容易检测甚至避免,但是竞争条件是一个很棘手的问题。
1.3.1 了解内存栅栏(memory barrier)
内存栅栏就是从本地或者功能内存到主存的拷贝动作,仅当写线程先跨域内存栅栏,读操作后跨域内存栅栏时,写操作的变更才会对其他线程可见,sychronized和volatile都强制规定了所有的变更必须全局所见。在程序运行中所有的变更会先在寄存器(registers)或者缓存中完成,然后在拷贝到主存中以跨域内存栅栏。
1.3.2 Avoid Shared Mutability(避免共享可变)
1.4 Recap(小结)
无论是在开发一个客户端桌面交互程序,还是一个高性能的后端服务器应用。并发编程将扮演着一个很重要的角色。并发编程可以帮助我们提高应用的响应,提升用户体验以及充分利用现代的多核处理器性能。
传统基于可变性处理的jvm并发编程模型带来了很多的问题。创建完线程后,我们需要努力的避免程序陷入饥饿,死锁和竞争条件下。通过消除共享可变性,我们可以从根本上去解决上述问题。而不可变性的编程将会是并发变得简单、安全、有趣。
第二章、Division of Labor (分工)
2.1 From sequential to concurrency(从顺序到并发)
Divide and Conquer(分而治之)
Determining the Number of Threads(确定线程数量)
获取系统的可用处理器核心数,通过下面代码很容易得到:
Runtime.getRuntime().availableProcessors();
因此线程的最小数量应该等于系统可用核心数的数量。对于计算密集型任务,创建和核心数相同的线程数就可以了,更多的线程数反而会导致性能下降,因为在多个任务处于就绪状态时,处理器核心需要在线程间不停的切换上下文。这种切换对程序性能损耗较大。但是如果任务都是IO密集型就需要开更多的线程。
当一个任务执行IO操作时,线程将会被阻塞,处理器立即会执行上下文切换以便处理其他就绪线程,如果我们只有核心数那么个线程,即使有待执行的任务也无法处理。因为拿不出更多的线程来供给处理器调度。
如果任务的50%的时间处于阻塞状态,则将线程数设置为处理器核心数的2倍,如果任务阻塞等待的少于50%,即这些任务属于计算密集型的,则程序所需线程数随之减少,但最少也不能低于处理器核心数。如果任务的阻塞时间大于执行时间,则该任务属于IO密集型。这种需要增加线程数。
因此可以计算出所需的线程数:
线程数 = 可用核心数/(1-阻塞系数)
计算密集型的阻塞系数为0,而IO密集型的阻塞系数接近1,
Determining the Number of Parts(确定任务数)
前面提到了如何计算所需线程数,现在需要确定如何分解问题。换句话说就是把一个大的任务分解成几个小的任务。
2.2 在IO密集型应用中使用并发
对于IO密集型应用阻塞都很大,所需的线程数一般都大于可用核心数。
数字例子主要是讲解一个从Yahoo获取前一天的股票收盘价,计算总n只股的总净值。
并发源码:
/**
* 获取先一个交易日的收盘价
*
*/
public class YahooFinance {
public static double getPrice(final String ticker) throws IOException {
final URL url =
new URL("http://ichart.finance.yahoo.com/table.csv?s=" + ticker);
final BufferedReader reader = new BufferedReader(
new InputStreamReader(url.openStream()));
//Date,Open,High,Low,Close,Volume,Adj Close
//2011-03-17,336.83,339.61,330.66,334.64,23519400,334.64
final String discardHeader = reader.readLine();
final String data = reader.readLine();
final String[] dataItems = data.split(",");
final double priceIsTheLastValue =
Double.valueOf(dataItems[dataItems.length - 1]);
return priceIsTheLastValue;
}
}
股票信息抽象处理类
public abstract class AbstractNAV {
//从文件中读取每只股的股票信息
public static Map<String, Integer> readTickers() throws IOException {
final InputStream io =Thread.currentThread().getContextClassLoader()
.getResourceAsStream("stocks.txt");
final BufferedReader reader =
new BufferedReader(new InputStreamReader(io));
final Map<String, Integer> stocks = new HashMap<>();
String stockInfo = null;
while ((stockInfo = reader.readLine()) != null) {
final String[] stockInfoData = stockInfo.split(",");
final String stockTicker = stockInfoData[0];
final Integer quantity = Integer.valueOf(stockInfoData[1]);
stocks.put(stockTicker, quantity);
}
return stocks;
}
//累加用户所持股票的价格
public void timeAndComputeValue()
throws ExecutionException, InterruptedException, IOException {
final long start = System.nanoTime();
final Map<String, Integer> stocks = readTickers();
final double nav = computeNetAssetValue(stocks);
final long end = System.nanoTime();
final String value = new DecimalFormat("$##,##0.00").format(nav);
System.out.println("Your net asset value is " + value);
System.out.println("Time (seconds) taken " + (end - start)/1.0e9);
}
public abstract double computeNetAssetValue(
final Map<String, Integer> stocks)
throws ExecutionException, InterruptedException, IOException;
}
并发处理
public class ConcurrentNAV extends AbstractNAV {
public double computeNetAssetValue(final Map<String, Integer> stocks)
throws InterruptedException, ExecutionException {
final int numberOfCores = Runtime.getRuntime().availableProcessors();
final double blockingCoefficient = 0.9;
final int poolSize = (int)(numberOfCores / (1 - blockingCoefficient));
System.out.println("Number of Cores available is " + numberOfCores);
System.out.println("Pool size is " + poolSize);
final List<Callable<Double>> partitions = new ArrayList<>();
for(final String ticker : stocks.keySet()) {
partitions.add(new Callable<Double>() {
public Double call() throws Exception {
return stocks.get(ticker) * YahooFinance.getPrice(ticker);
}
});
}
System.out.println("tasks "+ partitions.size());
final ExecutorService executorPool =
Executors.newFixedThreadPool(poolSize);
final List<Future<Double>> valueOfStocks =
executorPool.invokeAll(partitions, 10000, TimeUnit.SECONDS);
double netAssetValue = 0.0;
for(final Future<Double> valueOfAStock : valueOfStocks)
netAssetValue += valueOfAStock.get();
executorPool.shutdown();
return netAssetValue;
}
public static void main(final String[] args)
throws ExecutionException, InterruptedException, IOException {
new ConcurrentNAV().timeAndComputeValue();
}
}
2.3 在计算密集型应用中使用并发
书中例子主要讲述使用并发查找素数,例子代码略......
2.4 Strategies for Effective Concurrency(有效的并发策略)
我们应该尽可能的提供共享不可变性,否则就应该遵守隔离可变性原则,即保证总是只有一个线程可以访问可变变量。
第三章 Design Approaches(设计方法)
这一章主要探究无需改变内存中任何数据的设计方法。
3.1 Dealing with State(处理状态)
虽然处理状态是不可避免的,但是我们有三种方法用于处理状态:shared mutability(共享可变),isolated mutability(隔离可变性),pure immutability(完全不可变性)。
一个极端情况是共享可变性,我们创建的变量允许所有线程修改。这种在同步方面很容易出错。
在处理状态方面,隔离可变性是一个折中的选择。在该方法中变量是可变的,但是任何时候只有一个线程可以看到该变量。
另一个极端的方法是纯粹不可变性,该方法所有事物都是不可更改的。要使用这样的设计不好实现,部分原因是由问题的性质决定的。