并发编程的目的是为了让程序运行的更快,但是并不是启动更多的线程,就能让程序最大限度的并发执行。在进行并发编程时,如果希望通过多线程执行任务让程序运行的更快,会面临非常多的挑战,比如上下文切换的问题,死锁的问题,以及受限于硬件和软件的资源限制问题,本章会介绍几种并发编程的挑战,以及解决方案。
1.1上下文切换即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停的切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下个任务,但是在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务的保存到再加载的过程就是一次上下文切换。
就像我们同时在读两本书,比如当我们在读一本英文的技术书时,发现某个单词不认识,于是便打开中英文字典,但是在放下英文技术书之前,大脑必需首先记住这本书读到了多少页的第多少行,等查完单词之后,能够继续读这本书,这样的切换是会影响读书效率的,同样上下文切换也会影响到多线程的执行速度。
1.1.1多线程一定快吗?下面的代码演示串行和并发执行累加操作的时间,请思考下面的代码并发执行一定比串行执行快些吗?
packagechapter01;/***并发和单线程执行测试*
authortengfei.fangtf*version$Id:ConcurrencyTest.java,v0.-7-18下午10:03:31tengfei.fangtfExp$*/publicclassConcurrencyTest{/**执行次数*/privatestaticfinallongcount=l;publicstaticvoidmain(String[]args)throwsInterruptedException{//并发计算concurrency();//单线程计算serial();}privatestaticvoidconcurrency()throwsInterruptedException{longstart=System.currentTimeMillis();Threadthread=newThread(newRunnable(){Overridepublicvoidrun(){inta=0;for(longi=0;icount;i++){a+=5;}System.out.println(a);}});thread.start();intb=0;for(longi=0;icount;i++){b--;}longtime=System.currentTimeMillis()-start;thread.join();System.out.println(concurrency:+time+ms,b=+b);}privatestaticvoidserial(){longstart=System.currentTimeMillis();inta=0;for(longi=0;icount;i++){a+=5;}intb=0;for(longi=0;icount;i++){b--;}longtime=System.currentTimeMillis()-start;System.out.println(serial:+time+ms,b=+b+,a=+a);}}答案是不一定,测试结果如表1-1所示:
表1-1测试结果
循环次数串行执行耗时(单位ms)并发执行耗时并发比串行快多少1亿约1倍1千万约1倍1百万55差不多10万43慢1万01慢从表1-1可以发现当并发执行累加操作不超过百万次时,速度会比串行执行累加操作要慢。那么为什么并发执行的速度还比串行慢呢?因为线程有创建和上下文切换的开销。
1.1.2测试上下文切换次数和时长下面我们来看看有什么工具可以度量上下文切换带来的消耗。
使用Lmbench3[1]可以测量上下文切换的时长。
使用vmstat可以测量上下文切换的次数。
下面是利用vmstat测量上下文切换次数的示例。
$vmstat1procs-----------memory-------------swap-------io------system-------cpu-----rbswpdfreebuffcachesisobiboincsussyidwast0127868398008005901180180056711
CS(ContentSwitch)表示上下文切换的次数,从上面的测试结果中,我们可以看到其中上下文的每一秒钟切换多次。
1.1.3如何减少上下文切换减少上下文切换的方法有无锁并发编程、CAS算法、单线程编程和使用协程。
无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据用ID进行Hash算法后分段,不同的线程处理不同段的数据。
CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
1.1.4减少上下文切换实战本节描述通过减少线上大量WAITING的线程,来减少上下文切换次数。
第一步:用jstack命令dump线程信息,看看pid是进程里的线程都在做什么。
sudo-uadmin/opt/ifeve/java/bin/jstack7gt;/home/tengfei.fangtf/dump17
第二步:统计下所有线程分别处于什么状态,发现多个线程处于WAITING(onobjectmonitor)状态。
[tengfei.fangtf
ifeve~]$grepjava.lang.Thread.Statedump17awk{print$2$3$4$5}
sort
uniq-c39RUNNABLE21TIMED_WAITING(onobjectmonitor)6TIMED_WAITING(parking)51TIMED_WAITING(sleeping)WAITING(onobjectmonitor)3WAITING(parking)
第三步:打开dump文件查看处于WAITING(onobjectmonitor)的线程在做什么。发现这些线程基本全是JBOSS的工作线程在await。说明JBOSS线程池里线程接收到的任务太少,大量线程都闲着。