工作十余年,见证了信息系统的业务激增,也见证了业务数据量的爆炸式增长。越来越多的系统性能问题困扰着软件从业者。
谈到系统性能,很多人首先想到的就是数据库索引,甚至有的人什么都想不出来。我问过很多人StringBuffer跟StringBuilder有什么区别,相当一部分人不知道这两个类的区别,即便有人知道一个是线程安全的,一个是线程不安全的,当我继续追问哪个是线程安全时,很多人又会答错,其实StringBuffer跟StringBuilder的合理使用,也从侧面能够知道一个人如何看待系统性能问题,局部变量完全不存在并发场景,但是很多人还是使用StringBuffer这个线程安全的类,牺牲了系统性能。
有些人觉得语句执行的频率低就可以忽视系统性能,有些人觉得只要把SQL语句写好了,就解决了系统性能问题,有些人觉得使用了缓存就解决了系统性能问题。
性能作为软件系统的重要指标,值得每位软件从业者去重视它,值得从各个方面进行考虑。大师之所以成为大师,是因为他能够写出最优的代码,在影响系统性能的方面,他能考虑到方方面面,他甚至因为一行代码从CPU方面考虑系统性能。
谈到单例模式,很多人都耳熟能详。很多人都对双重检查锁了如指掌。双重检查锁,在保证单例模式的同时尽最大努力提升了系统性能。
对于单例模式中,实例变量要加上volatile修饰符,肯定很多人都知道原因,volatile避免了指令的重排序,保证了实例变量的多线程可见性。而指令重排序是为了提升性能,CPU缓存也是为了提升性能。volatile避免了指令重排序肯定会牺牲一定的系统性能。多线程可见性,必须要保证CPU缓存与内存数据的实时同步,这个也是以牺牲系统性能为代价的。
今天我们来看一段Spring大师Rob Harrop的代码。这段代码是DefaultNamespaceHandlerResolver类中对实例变量handlerMappings采用双重检查锁实现单例模式。核心代码如下:
private Map<String, Object> getHandlerMappings() {
Map<String, Object> handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
synchronized (this) {
handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
if (logger.isTraceEnabled()) {
logger.trace("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
}
try {
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
if (logger.isTraceEnabled()) {
logger.trace("Loaded NamespaceHandler mappings: " + mappings);
}
handlerMappings = new ConcurrentHashMap<>(mappings.size());
CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
this.handlerMappings = handlerMappings;
}
catch (IOException ex) {
throw new IllegalStateException(
"Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
}
}
}
}
return handlerMappings;
}
我们注意到一个细节,Rob Harrop的代码跟大部分人的代码稍微有点区别,双重检查锁的判断过程中,Rob Harrop并未直接对实例变量进行判断,而是首先将实例变量赋值给了局部变量,然后对局部变量进行判断,第一次调用的时候对象创建完成后也是先赋值给了局部变量,最后才让实例变量指向了局部变量所指向的对象。
对于这样的写法,我曾经问过一些高手,很多人觉得这样写的原因可能是作者本人的编码风格,认为作者这样写显得比较清爽。当然了,我相信大部分人对这个细节还是视而不见,如果让我们来实现这个功能,估计90%的人还是会直接使用实例变量。
原因还是出在volatile上面,前面我们也提到了volatile会牺牲一定的性能。而这里把实例变量赋值给局部变量去判断就是为了避免volatile造成的性能损失出现。此处Map的填充采用了循环填充,如果直接使用实例变量,因为volatile的存在,所以每填充一个键值对就要将CPU缓存中的当前Map对象所在的缓存行同步到内存中。如果使用局部变量的话就避免了CPU缓存跟内存针对此Map对象所在的缓存行的实时同步,同时也使用了指令重排序提升系统性能,所有的工作在局部变量上完成之后,再将实例变量指向局部变量,此时只需一次性将此Map对象所在的缓存行从CPU缓存同步到内存就可以了,完美的避开了volatile的性能损耗。
日常编程中,很多人会以场景出现的概率低为由忽视性能问题,觉得牺牲一点性能也无所谓了。此处我们举例的代码概率够不够低呢?只有当多个线程同时第一次调用到此段代码的时候,才会出现并发。概率已经非常的低了,但是Rob Harrop没有因为概率低而不去考虑性能问题,而是特意使用了局部变量这个一般人看似多余的操作,提升了系统性能。
我认为,这就是大师跟普通人的区别吧。我们每个人都应该跟大师学习,时刻记住性能是软件系统的一个重要指标。




