出差北京,趁回杭州之前去了趟海淀,顺手带了本之前一直想买的虚拟机并发编程,虽然这本书在amazon中国上的评价一般,但看看总还是有些收获吧,可能那些说书一般的都是大神吧。书应该是刚上新的,放在了最显眼的位置。一夜火车,看了前三章觉得不错,还是忍不住写点儿笔记。
书的前言部分推荐了两本书《Java Concurrency in Practice》和《Concurrent Programming in Java》,这两本书主要对JDK提供解决方案,并在内存模型和一致性上进行了比较全面的叙述。而《虚拟机并发编程》则更重于对实际问题的分析和一些经验性的结论。比如这本书会从IO密集型与计算密集型等方面对并行程序进行分析。
书主要描述了三种并发的解决方案,java JDK并发模型,软件事务内存(STM)和基于角色的并发模型(Actor-based)。说来惭愧,这几个中文名词我还真没有听过。书主要分五部分,第一部分还是对并发的概念、应用场合等基础方面进行讲解,这部分通俗易懂,而且选用的实例非常具有代表性。后面几部分即JDKK并发模型、STM、Actor-based和后记。今天这部分主要记录前三章的内容,关于并发策略。
在讲到Race Condition的时候,作者用到了一个由于JIT编译器优化而产生的并发问题,当然这也跟java内存模型相关,但是这样的例子在之前的书确实很少见。书上给出了一个简短的程序:
public class RaceCondition {
private static boolean done;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable(){
@Override
public void run() {
int i=0;
while(!done){
i++;
}
System.out.println("Done!");
}
}).start();
System.out.println("OS:"+System.getProperty("os.name"));
Thread.sleep(2000);
done=true;
System.out.println("flag done set to true");
}
}
这个程序在JVM server模式下和client模式下运行的结果会不同,在client模式下,程序可以执行到Done,而server模式则不行。作者用的是JDK1.6的环境,很可惜我在1.7版本的JDK上并没有成功的运行处相应的结果。当然我的机器是64位的,书中给出的是32位的机器。
我们暂且运用作者给出的结论,这是由于server JIT编译器优化所导致的。JIT编译器可能对新线程代码里的while循环进行优化,导致新线程在线程上下文中不能看到变量done的变化。还有可能会是新线程只会从寄存器或本地cache中读取done的值。这都可以归结为内存栅栏问题,具体可看wiki百科,有很详细的解释。说到这里,是不是发现这个问题和内存可见性有关了,如果能想到这一步,那解决方案其实就很简单了,在done前面加volatile修饰,让done不再付线程中做cache,内存都去主内存中读取。这可能是个挺巧的问题,由于编译器的优化造成了内存可见性的问题。关于内存可见性,在《Java Concurrency in Practice》中有很详细的描述。
接下去的第二章叫分工原则,这里其实将分工分为IO密集型和计算密集型两种。针对这两种分工作者提出了不同的多线程解决方案。这里讲的多线程解决方案是为了提供一种方法来提高程序运行的效率(和单线程相比),这一章没有讨论多线程可能产生的问题。这一章提供了这么两个案例:第一个是计算某富商的资产净值,第二个则是统计某个区间内素数的个数,容易理解第一个用于讨论IO密集型,剩下的那个用于讨论计算密集型。
这一章会有很多结论性的成果,或者说是作者的经验,这些经验在其他任何一本并发编程的书里都会有,而且每个人的结论还都不一样,所以实际问题实际解决,这里只做参考。在解决上述两个问题的时候,我们应该有以下两个关键的步骤,第一,确定线程数,第二,确定任务数量。关于线程数的确定,这个说法就很多了,书上给出的结论是:
线程数=CPU可用核心数/(1-阻塞系数),其中阻塞系数取0和1之间。其中CPU的可用核心数很容易拿到:
Runtime.getRuntime().availableProcessors();
计算密集型任务的阻塞系数为0,而IO密集型任务的阻塞系数接近1。在实际问题中如果需要比较精确的评估阻塞系数,可以通过性能分析工具对任务进行跟踪。在第二步,确定任务数量上,这个也没有一个明确的指示,有时候并不一定是任务书等于线程数,针对IO密集型任务,由于每个任务的代价基本相同,所以每个任务分配一个线程还是比较合理的,但是在计算素数的个数任务中,由于数的特性,判断素数所用的时间不同的数会有不同的效率,所以不能很简单的将数分几段,然后分配几个线程去执行。书上还给出了从一个顺序执行的程序改写成并发程序的代码,采用的是线程池进行处理。当然大家也可以试着去写写用多线程的方式计算一个区间内的素数。很简单,都是入门级的代码,尽量采用concurrent包里的工具,这些会更简单。
第三章主要讲述了三种可以处理状态的方法,shared mutability、isolated mutability、pure immutability。这里的状态值得是并发编程中经常提的是否具有可变性的状态。我们知道,一个类要是状态是不可变的,那自然就不存在同步的问题。但是很少我们可以写出一个不改变状态的类。所以这一章就是通过这三种方法的介绍来给出解决这一问题的方案。我只提供术语,有兴趣的都可以去google里搜索,都有相应的结论。