其他一些需要考虑的因素
不要给内存系统太大的压力
如果线程执行过程中需要分配内存这在 Java 中通常不会造成问题现代的 JVM 是高度优化的它通常为每个线程保留一块 Buffer这样在分配内存时只要 buffer 没有用光那么就不需要和全局的堆打交道而本地 buffer 分配完毕之后 JVM 将不得不到全局堆中分配内存这样通常会带来严重的可扩展性的降低另外给 GC 带来的压力也会进一步降低程序的可扩展性尽管我们有并行的 GC但其可扩展性通常并不理想如果一个循环执行的程序在每次执行中都需要分配临时对象那么我们可以考虑利用 ThreadLocal 和 SoftReference 这样的技术来减少内存的分配
使用 ThreadLocal
ThreadLocal 类能够被用来保存线程私有的状态信息对于某些应用非常方便通常来讲它对可扩展性有正面的影响它能为各个线程提供一个线程私有的变量因而多个线程之间无须同步需要注意的是在 JDK 之前ThreadLocal 有着相当低效的实现如果需要在 JDK 或更老的版本上使用 ThreadLocal需要慎重评估其对性能的影响类似的目前 JDK 中的 ReentrantReadWriteLock 的实现也相当低效如果想利用读锁之间不互斥的特性来提高可扩展性同样需要进行 profile 来确认其适用程度
锁的粒度很重要
粗粒度的全局锁在保证线程安全的同时也会损害应用的性能仔细考虑锁的粒度在构建高可扩展 Java 应用时非常重要当 CPU 个数和线程数较少时全局锁并不会引起激烈的竞争因此获得一个锁的代价很小(JVM 对这种情况进行了优化)随着 CPU 个数和线程数增多对全局锁的竞争越来越激烈除了一个获得锁的 CPU 可以继续工作外其他试图获得该锁的 CPU 都只能闲置等待导致整个系统的 CPU 利用率过低系统性能不能得到充分利用当我们遇到一个竞争激烈的全局锁时可以尝试将锁划分为多个细粒度锁每一个细粒度锁保护一部分共享资源通过减小锁的粒度可以降低该锁的竞争程度 ncurrentConcurrentHashMap 就通过使用细粒度锁提高 HashMap 在多线程应用中的性能在 ConcurrentHashMap 中默认构造函数使用 个锁保护整个 Hash Map 用户可以通过参数设定使用上千个锁这样相当于将整个 Hash Map 划分为上千个碎片每个碎片使用一个锁进行保护
结论
通过选择一种合适的 profile 工具检查 profile 结果中的热点区域使用适合多线程访问的数据结构线程池细粒度锁减小热点区域并重复此过程不断提高应用的可扩展性
构建在多核上具有高可扩展性的 Java 应用并不是一件容易的事减少各个线程之间的沖突和同步是提高可扩展性的关键本文中介绍的一些通用工具和技巧可以给程序员提供一些帮助但更多的情况要依赖于具体的应用