首页 > 代码库 > Java 并发编程之性能和可伸缩性

Java 并发编程之性能和可伸缩性

对性能的思考

对于一个给定的操作,通常会缺乏某一种特定的资源限制它的性能,就像常说的短板理论,比如CPU时钟周期、内存、网络带宽、I/O带宽、数据库请求(这个应该是现在高并发的一个瓶颈)、磁盘空间等等。当操作因为某种资源而受到限制时,我们就以资源+“密集型”命名这个操作,如CPU密集型,数据库密集型、。

相对于单线程来说,多线程在多个CPU存在时,能更好的发挥它的优势,不过如果是单核的情况下会花费更多的开销,比如。线程之间的协调,增加的上下文切换、线程的创建和销毁、以及线程的调度。如果过度的使用多线程,那么这些开销甚至影响这个程序的性能比单线程的性能还差。

所以我们在编写多线程的程序时,应该先考虑的程序的运行环境是否是多线程程序所适应的。

性能和可伸缩性

可伸缩性是指当增加计算资源时,程序的吞吐量或者处理能力相应的增加

在进行性能调优时,我们应尽可能地对计算实现并行化。

三层程序模型:表现层、业务逻辑层和持久化层是彼此独立,如果我们把这三层融合到同一个应用程序中那么性能肯定高于应用程序分为多层次分布到多个系统的性能。但是同时它们的可伸缩性就降低了。

当这种单一的系统 到达自身处理能力的极限时,要进一步提升它的处理能力将非常困难 。因此我们通常倾向于更好的可伸缩性。

评估各种性能权衡因素

避免不成熟的优化,首先使程序正确,然后再提高运行速度-如果它还运行的不够快,就是常说的。如果程序没有出现什么 问题,那么 就不要动它。


常用的优化包括:增加内存使用量以降低延迟、增加开销换取安全性。

换句话说,就是这样做值不值得。有下面几个问题:

更快的含义是什么 ?

该方法在什么 条件下运行得更快?低负载还是高负载?大数据集还是小数据集?能否通过测试结果来验证你的答案?

这些条件在运行环境中发生频率?能否通过测试来验证你的答案?

在其他不同条件的环境中能否使用这里的代码?

最后就是为优化牺牲的一些计算资源值不值得?



不要猜测,以测试为基准。



Amdahl定律

在增加计算资源的情况下,程序在理论上能够实现最高加速比,这个值取决于程序中可并行组件与串行组件所占的比重。假定F是必须被串行执行的部分

Speedup<=1/F+((1-F)/N)

所以需要串行执行的部分越少,可实现的最高加速比越高。


不存在完全并行化的程序,就算最简单的并发程序也有从队列里获取Runnable这个串行部分

比如:在线程数量不断增加时,ConcurrentLinkedQueue能比synchronizedLinkedLIst有两个倍的吞吐率。


因为在第一个队列中,只有对指针的更新操作需要串行执行,第二个则是整个的插入或者删除操作都将串行执行。


上下文切换

即线程的调度。当可运行的线程数量大于CPU的数量的时候发生。将新调度进来的线程执行上下文设置为当前上下文。学过安卓开发的知道安卓有一个非常重要的域就是Context(上下文)

当线程由于等待某个线程发生竞争的锁而被阻塞时,JVM通过会将这个线程挂起,并允许它交换出去。如果线程频繁地发生阻塞,那么 它们将无法使用完整的调度时间片,那么就发生越多的上下文切换,增加调度开销,并因此降低吞吐量。

大多数通用的处理器中,上下文的切换相当于5k到10k的时钟周期,也就是几微秒。

Unix系统中的vmstat命令和Windows系统的perfmon工具都能报告上下文切换次数以及在内核 中执行时间所占比例等 信息。如果内核占用率超过10%,那么通常表示 调度活动发生很频繁。很可能是由于 I/O或者竞争锁的阻塞引起的。

内存同步

在synchronized和volatile提供的可见性保证 中可能会使用一些特殊指令,即内存栅栏。它将抵制一些编译器优化操作,在内存栅栏中,大多数操作都是不能被重排序的。

非竞争同步是被鼓励采用的。它对应用程序整体性能的影响微乎其微。而有竞争的同步会在破坏安全性的同时,经历一个非常痛苦的除错过程。

现在的JVM能通过优化来去掉一些不会发生竞争的锁,从而减少不必要的同步开销。

比如

synchronized (new Object()) {
			
		}
上面这个同步通常都会被优化掉。

还有一些优化操作如:

	public String getStoogeNames(){
		List<String> stooges=new ArrayList<String>();
		stooges.add("adf");
		stooges.add("adf");
		stooges.add("adf");
		return stooges.toString();
	}
在上面的这段代码中,至少会将stooges的锁获取释放4次(最后的toString()也是一次,一个智能的运行时编译器会分析这些调用,从而合并锁的获取等操作变成一次的锁获取和释放。并且在第一次执行后把getStoogeNames重新编译为仅返回第一次执行的结果。

非竞争同步带来的开销已经非常小了,所以我们应该将优化重点放在那些发生竞争的地方。













Java 并发编程之性能和可伸缩性