选择不同的GC收集器,会有不同的效果,CMS GC多数动作是和应用并发进行的,确实可以减少GC动作给应用造成的暂停。
对于web应用而言,在G1还不够成熟的情况下,CMS GC是不错的选择。
CPU us高的原因主要是执行程序无任何挂起动作,且一直执行,导致CPU没有机会去调度执行其他的线程,导致线程饿死的现象。
以上图片增加sleep,这种修改方式是以损失单次执行性能为代价,但由于降低CPU的消耗,对多线程的应用而言,反而提高总体的平均性能。
对GC频繁造成的CPU us高的现象,则要通过JVM调优或程序调优,降低GC的执行次数。
CPU sy高的原因主要是线程的运行状态经常切换,这种情况,最简单的优化方法是减小线程数。
造成CPU sy高的原因除了启动的线程过多以外,还有一个重要的原因是线程之间锁竞争激烈,造成线程状态经常要切换,因此尽可能降低线程间的锁竞争也是常见的优化方法。
造成文件IO消耗严重的原因主要是多线程在写大量的数据到同一个文件,导致文件很快变得很大,从而写入速度越来越慢,并造成各线程激烈争抢文件锁。通常采用几种方式调优:
1.异步写文件
将写文件的同步动作改成异步动作。
2.批量读写
频繁的读写操作对IO消耗很严重,批量操作将大幅度提升IO操作的性能。
3.限流
将文件IO消耗控制到一个能接受的范围。
4.限制文件大小
对于每个输出文件,都应做大小的限制。
常用的调优方法为进行限流,限流通常是闲置发送packet的频率,从而在网络IO消耗可接受的情况下来发送packet。
1.释放不必要的引用,例如使用ThreadLocal
2.使用对象缓存池
3.采用合理的缓存实效算法,例如FIFO和LRU策略的CachePool
4.合理使用softReference和WeakReference
锁竞争的状态会比较明显,这时线程很容易处于等待锁的状况,从而导致性能小将以及CPU sy上升。
给予CAS来做无需lock就可以实现资源一致性保证,主要的实现nonblocking的算法
Treiber算法主要用于实现stack,基于Treiber算法实现无阻塞Stack。
public class ConcurrentStack {
AtomicReference> head = new AtomicReference>();
public void push(E item) {
Node newHead = new Node(item);
Node oldHead;
do {
oldHead = head.get();
newHead.next = oldHead;
} while (!head.compareAndSet(oldHead, newHead));
}
public E pop() {
Node oldHead;
Node newHead;
do {
oldHead = head.get();
if (oldHead == null)
return null;
newHead = oldHead.next;
} while (!head.compareAndSet(oldHead,newHead));
return oldHead.item;
}
static class Node {
final E item;
Node next;
public Node(E item) { this.item = item; }
}
}
和Treiber算法类似,也是基于CAS以及AtomicReference来实现队列的非阻塞操作,ConcurrentLinkedQueue就是典型的基于Michael-Scott实现的非阻塞队列。
从上面两种算法来看,基于CAS和AtomicReference来实现无阻塞算法是不错的选择。但值得注意的是,由于CAS是基于不断的循环比较来保证资源一致性的,对于冲突较多的应用场景而言,CAS会带来更高的CPU消耗,因此不一定采用CAS实现无阻塞的就一定比采用Lock方式的性能好。业界还有一些无阻塞算法的改进,如MCAS、WSTM20等
尽可能让锁仅在需要的地方出现,通常没必要对整个方法加锁,而只对需要控制的资源做加锁操作。尽可能让锁最小化,例如一个操作中需要保护的资源只有HashMap,那么在加锁时则可只synchronized(map),而没必要synchronized(this).
把独占锁拆分为多把锁,常见的有读写锁拆分及类似ConcurrentHashMap中默认拆分为16把锁的方法。需要注意的是采用拆分锁后,全局性质的操作会变得比较复杂,例如ConcurrentHashMap的size操作。
在修改时加锁,并复制对象进行修改,修改完后切换对象的引用,而读取操作时则不加锁,这种方式称为CopyOnWrite。CopyOnWriteArrayList就是其中的典型实现。这种做法好处是可以明显提升读的性能,适用读多写少的场合,坏处是造成更多的内存消耗。
对于JAVA应用而言,通常原因就是在能并行处理的场景中未使用足够的线程。
另外,单线程的计算,也可以拆分为多线程来分别计算,最后合并结果,JDK7中的fork-join框架可以给以上场景提供一个好的支撑方法
如数据的缓存、耗时资源的缓存(如数据库连接,网络连接)、页面片段的缓存等。