使用并发的目标是为了提高性能。引入多线程后,其实会引入额外的开销。如果线程之间的协调,增加的山下文切换,线程的创建和销毁,线程的调度等等。如果是CPU密集型任务却开了很多线程去执行,就会导致多线程程序比单线程还低。
衡量应用的程序的性能:服务时间,延迟时间,吞吐量,节伸缩性等等。其中服务时间,延迟时间(多快),吞吐量(干活的时间占时间的比例)。此二者相互独立甚至互相矛盾。
对服务器来说,吞吐量往往比执行速度更重要。因此
1. 保证程序正确,确实达不到要求再提升速度。
2. 一定要以测试为基准。(压测)
线程引入的开销
上下文切换
即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现 这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态(程序计数器),以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。
这就像我们同时读两本书,当我们在读一本英文的技术书时,发现某个单词不认识,于是 便打开中英文字典,但是在放下英文技术书之前,大脑必须先记住这本书读到了多少页的第多少行,等查完单词之后,能够继续读这本书。这样的切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度。
当线程由于等待某个发生竞争的锁而被阻塞时,JVM通常会将这个线程挂起,并允许它被交换出去。如果线程频繁地发生阻塞,那么它们将无法使用完整的调度时间片。阻塞越多,CPU密集型的程序就会发生越多的上下文切换,从而增加调度开销,并因此降低吞吐量。
上下文切换是计算机密集型操作。也就是说,他需要相当客观的处理器时间。所以,上下文切换对系统来说意味着消耗大量的CPU时间。事实上,可能是操作系统中时间消耗最大的操作。上下文切换的实际开销会随着操作系统而变。多数处理器相当于50~10000个时钟周期,也就是几微秒。
UNIX系统的vmstat命令能报告从上下文切换次数以及在内核中执行时间所占比例等信息。如果内核占用率较高(超过10%),那么通常表示活动发生的很频繁,这很可能是由于IO或竞争锁导致的阻塞引起的。
同步内存(有序性)
同步操作的性能开销包括多个方面。在 synchronized 和 volatile 提供的可见性保证中可能会使用一些特殊指令,即内存栅栏( Memory Barrier)。
内存栅栏可以刷新缓存,使缓存无效刷新硬件的写缓冲,以及停止执行管道。
内存栅栏可能同样会对性能带来间接的影响,因为它们将抑制一些编译器优化操作。在内存栅栏中,大多数操作都是不能被重排序的。
阻塞
引起阻塞的原因:包括阻塞IO,等待获取发生竞争的锁,或者在条件变量上等待(while循环)等等。
阻塞会导致线程挂起【挂起:挂起进程在操作系统中可以定义为暂时被淘汰出内存的进程。机器资源是有限的,在资源不足的情况下,操作系统对内存中的程序进行合理的安排,其中有的进程被暂时调离出内存,当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态即就系态。】很明显,这个操作至少包括两次额外的上下文切换,还有相关的操作系统级操作等等。
如何减少锁竞争
减少锁的粒度
使用锁的时候,锁能保护的对象是多个。当这些多个对象属于独立变化的时候,不如用多把锁来一一保护这些对象,但是如果有同时持有多个锁的业务方法,要注意避免发生死锁。(联想ConcurrentHashMap的演绎,以及MySQL行锁)。
缩小锁范围
对锁的持有实现快进快出,尽量缩短持有锁的时间。将一些与锁无关的代码移出锁的范围。特别是一些耗时,可能引起阻塞的操作。
避免多余的锁
在一个方法里,为了缩小锁范围,连续加锁了两次。而这两个锁之间的代码非常简单,远远小于加锁与释放锁的消耗,那么这时候就把中间这些不需要加锁的代码也包上即可。
替换独占锁
在业务运行的情况下:
1. 使用读写锁
2. 用CAS自旋
3. 使用系统的并发容器。