Java并发之性能和可伸缩性 二
1.检测CPU的利用率
当我们测试可伸缩性时,通常要确保处理器得到充分利用。 那我们如何检测CPU的利用率呢?
- 在windows下我们可以通过下面方式来检测,打开cmd:
回车。
2.在Linux下,我们可以:
如果所有的CPU的利用率并不均匀(有些CPU很忙碌,而有些很闲),那么我们的首要目标就是进一步找到程序中的并行性。
不均匀的利用率表明大多数计算都是由一小组线程完成的,并且应用程序没有利用其他的处理器。
所以说,如果CPU没有得到充分利用,我们就需要找出其中的原因,通常有下面几种原因:
1)负载不充足。 发现这种原因的源头是测试程序中可能没有足够多的负载(这也表明一个好的测试程序的重要性),如果发生这种原因,我们可以增加负载后测试,并检查CPU的利用率,响应时间,服务时间等指标的变化。 如果产生足够多的负载使应用程序达到饱和,那么可能需要大量的计算机能耗,并且问题可能在于客户端系统是否具有足够的能力。
2)I/O密集。 可以通过iostat或者perfmon来判断某个应用程序是否是磁盘I/O密集型的,或者通过检测网络的通信流量级别来判断它是否需要高带宽。
3)外部限制。 如果应用程序依赖于外部服务,例如数据库或Web服务,那么性能瓶颈可能并不在我们自己的代码中。 可以使用某个分析工具或数据库管理工具来判断在等待外部服务的结果时需要多少时间。
4)锁竞争。 使用分析工具可以知道在程序中存在何种程度的锁竞争,以及在哪些锁上存在 “激烈的竞争”。然而我们也可以通过其他一些方式来获得相同的信息,例如随机取样触发一些线程转储并在其中查找在锁上发生竞争的线程。 如果线程由于等待某个锁而被阻塞,那么在线程转储信息中将存在相应的栈帧,其中包含 “waiting to lock monitor…”。非竞争的锁很少会出现在线程转储中,而对于竞争激烈的锁,通常至少会有一个线程来等待获取它,因此将在线程转储中频繁出现。
如果应用程序正在使CPU保持忙碌状态,那么可以使用监视工具来判断是否能通过增加额外的CPU来提升程序的性能。如果一个程序只有4个线程,那么可以充分利用一个4路系统的计算能力,但当移植到8路系统上时,却未必能获得性能提升,因为可能需要更多的线程才会有效利用剩余的处理器。
在vmstat命令的输出中,有一栏信息是当前处于可运行状态但没有运行的线程数量。如果CPU的利用率很高,并且总会有可运行的线程在等待CPU,那么当增加更多的处理器时,程序的性能可能会得到提升。
2.向对象池说 “不”
在JVM虚拟机的早期版本中,对象分配和垃圾回收等操作的执行速度非常慢,但在后续的版本中这些操作的性能得到了极大的提高。事实上现在的Java分配操作已经比C语言的malloc调用分配更快(在HotSpot 1.4.x和5.0中,“new Object” 的代码大约只包含10条机器指令)。
但是还是存在 “缓慢的” 对象生命周期问题,许多开发人员都选择使用对象池技术,在对象池中,对象能被循环使用,而不是由垃圾回收器回收并在需要时重新分配,这样的话就会像线程池一样,可以减少一定的开销和时间。
确实,在单线程程序中,尽管对象池技术能降低垃圾收集操作的开销,但对于高开销对象以外的其他对象来说,它仍然存在性能缺失(除了损失CPU指令周期外,在对象池技术中还存在一些其他问题,其中最大的问题就是如何正确地设定对象池的大小,如果对象池太小,那么将没有作用,如果太大,则会对垃圾回收器带来压力,因为过大的对象池将占用其他程序需要的内存资源。 如果在重新使用某个对象时没有将其恢复到正确的状态,那么可能会产生一些 “微妙的” 错误。此外,还可能出现一个线程在将对象归还给线程池后仍然使用该对象的问题,这会产生一种 “从旧对象到新对象” 的引用从而导致基于代的垃圾回收器需要执行更多的工作)。
类似的在并发程序中,对象池的表现更加的糟糕。如果一些线程池从对象池中请求一个对象,那么就需要通过某种同步来协调对象池数据结构的访问从而可能使某个线程被阻塞。如果某个线程由于锁竞争而被阻塞,那么这种阻塞的开销将是内存分配操作开销的数百倍,因此即使对象池带来的竞争很小,但是也会造成一个可伸缩的瓶颈。
所以:通常来说,对象分配操作的开销比同步的开销更低。因此,在并发程序中不要使用对象池!!
3.减少上下文切换的开销
在许多任务中都包含一些可能被阻塞的操作。 当任务在运行和阻塞这两个状态之间转换时,就相当于一次上下文切换。
如果大多数的锁获取操作不存在竞争,那么并发系统就能执行的更好,因为在锁获取操作上发生竞争时将导致更多的上下文切换。 在代码中造成的上下文切换次数越多,吞吐量就越低。
所以在设计并发程序时我们一定要考虑锁竞争问题。