lper对象加锁,然后使用cacheHelper的flushCache方法,打开cacheHelper对象才发现它又是一个MemberCacheHelper对象,这时候问题来了,为什么父类和子类都保存了一个MemberCacheHelper对象呢?其实MemberCacheHelper这个对象就是一个缓存的结构体,父类有一些公有的缓存数据,子类有自己的缓存信息,这样也能说得过去,继续到MemberCacheHelper类的flushCache方法:
// Must sync here because we want the three maps to be modified together.
public synchronized void flushCache() {
mapMemberToChildren.clear();
mapKeyToMember.clear();
mapLevelToMembers.clear();
if (rolapHierarchy instanceof RolapCubeHierarchy){
((RolapCubeHierarchy)rolapHierarchy ).flushCacheSimple();
}
// We also need to clear the approxRowCount of each level.
for (Level level : rolapHierarchy.getLevels()) {
((RolapLevel)level ).setApproxRowCount(Integer. MIN_VALUE);
}
}
这里对缓存中的每一个map进行clear,然后又对这个hierarchy执行flushCacheSimple方法,我勒个擦,怎么又回来了,这个hierarchy对象不就是我们进出进入flushCache的那个hierarchy对象吗?过了一遍flushCacheSimple方法发现它最终又调用了reader的flushCacheSimple方法,这个函数执行的操作类似于flushCache:
public void flushCacheSimple(){
super.flushCacheSimple();
rolapCubeCacheHelper.flushCacheSimple();
}
好了,继续到MemberCacheHelper的flushCacheSimple方法:
public void flushCacheSimple(){
synchronized(cacheHelper){
cacheHelper.flushCacheSimple();
}
}
我勒个擦,这里又加锁,之前不是已经加过了吗?当然这个锁因该是可重入的,这里自然不会造成死锁,但是下面的rolapCubeCacheHelper对象也是MemberCacheHelper对象啊!最后进入flushCacheSimple方法,这彻底凌乱了:
public synchronized void flushCacheSimple() {
mapMemberToChildren.clear();
mapKeyToMember.clear();
mapLevelToMembers.clear();
// We also need to clear the approxRowCount of each level.
for (Level level : rolapHierarchy.getLevels()) {
((RolapLevel)level).setApproxRowCount(Integer.MIN_VALUE);
}
}
这里面执行的操作和flushCache方法不是一样的吗?!这到底是在做什么,当然理了这么多也发现了出现死锁的根源了,就在于reader执行的flushCache方法,这里面分别调用了父类和当前类的cacheHelper对象的flushCache,但是这个方法还会调用flushCacheSimple方法,这个方法再次调用reader的flushCacheSimple方法,这里再次调用父类和当前类的cacheHelper对象的flushCacheSimple方法,而且每次调用都需要加锁,这就导致了如下的死锁情况: A线程执行flushCache方法,它已经完成了super.flushCache方法,然后执行当前reader对象的flushCache方法,首先及时需要持有这个helper对象的锁,然后再执行到flushCacheSimple的时候申请父类的helper对象的锁。B线程可能在执行super.flushCache进入这个函数意味着需要持有父类的helper,但是当它执行flushCacheSimple的时候有需要申请当前类的helper对象的锁,于是就造成了死锁。
开始没有定位到这个问题之前不晓得死锁到底是怎么回事造成的,于是想着让所有的线程顺序执行flushCache方法就可以避免死锁了(不要并发了),但是尝试了一下发现不能这样,因为其他线程还是有可能调用这个flushCache方法,这个不是由我控制的,于是只能具体了解这个函数到底执行了什么,发现flushCache和flushCacheSimple方法其实是重复的,不晓得当初写这段代码的人是怎么想的,于是就把所有的flushCacheSimple方法的调用去掉,这样就不会再有持有A锁再去申请B锁的情况了。
问题算是解决了,最终hotfix版本也算是上线了,一颗悬着的心也算放下了,着这个过程中我也学到了不少知识: 1、学会并且善于使用java提供的分析工具,例如jstack、jstat、jmap、以及开源的MAT等等。 2、遇到问题不要害怕,不要一味的埋怨这个问题不是我造成的,我也不知道怎么回事之类的,静下心来思考整个流程,运用以前的理论知识和经验一定能够把问题解决的,没有什么问题是偶然的,如果出错一定是代码有问题。 3、测试很重要,尤其压力测试,我们项目目前人手紧缺,QA也没有专职的,所以基本上是开发在开发环境上测试一下功能,并没有做过性能测试之类的东西,我觉得测试应该尽可能覆盖线上可能出现的各种情况。 4、上线之前做好回滚,否则你会很狼狈,幸亏这点我每次操作之前都先备份。 5、在编码的时候,尤其一个操作会涉及到多个synchronized操作的时候尤其要注意,回忆一下当初避免死锁的几个方法,按顺序加锁往往是最好的解决办法。 6、搞清楚一个方法到底想要做什么?输入是什么,输出是什么,会造成什么影响,在写完一个方法之后在脑子中模拟一下整个函数的执行流程是否符合预想。 7、如果真的遇到这样的需求:父类和子类都持有一个类型的对象,让他们独立操作,父类对象的操作完成之后在执行子类对象的操作,而不要穿插着调用。
接下来的一段时间要开始搞mondrian了,希望能够从这个OLAP执行引擎中学到一些东西,不过自己的编译原理方面的知识几乎为0,这方面需要补强啊,我对于mondrian中重点要看的东西应该是:1、如何解析MDX(类似于如何解析SQL),2、如何将MDX动态的翻译成一串SQL(类似于如何生成执行计划),3、缓存如何实现,4、执行MDX或者SQL时如何使用缓存,5、如果使用聚合表进行优化。