搜索
写经验 领红包

多线程并发的性能问题和解决方法有哪些(多线程并发的性能问题和解决方法)

导语:多线程并发的性能问题和解决方法

多线程并发的性能问题和解决方法有哪些(多线程并发的性能问题和解决方法)

多线程与并发是为了提升性能,性能提升实际上就是使用更少的资源做更多的事情。

多线程并发出现的问题

多线程并发的目标是提升整体性能,但是使用多线程也会造成一些额外的开销,比如线程之间的协调、上下文切换、线程的创建和销毁、线程调度。如果多线程的性能比实现同功能的性能还差,那就是一个很糟糕的并发设计,

要想通过多线程并发来获得更好的性能,主要做好两个事情:更有效的利用现有处理资源、在出现新的处理资源时使程序尽可能地利用这些新资源,就是尽可能使CPU处于忙碌状态(并不是做无用功),最终的目标就是对资源充分的利用,充分有效的利用就能发挥资源的最大价值,就能最大的提高性能。比如程序是计算密集型可以通过增加处理器来提高性能,如果一个线程不能使CPU一直处于忙碌状态,那么就应该使用多线程使CPU处于忙碌状态。

可伸缩性

要获得更好的性能就需要有效的利用现有资源和利用新出现的资源,现有资源好充分利用,但是新出现的资源就不是那么好办了。所以当增加计算资源时(如CPU、内存、存储容量或者I/O带宽),程序的吞吐量或者处理能力能相应地增加是程序很最重要的功能也就是可伸缩性。

多线程的额外开销

上下文切换:如果可运行的线程数大于CPU的数量,那么操作系统最终会将某个正在运行的线程调度出来,从而使其他线程能够使用CPU,这将导致一次上下文切换,这个过程将保存当前运行线程的执行上下文,并将新调度进来的线程的执行上下文设置为当前上下文。

内存同步:多线程开发一般都会有synchronized和volatile等保证资源的可见性,这些关键字可能会使用一些特殊的指令内存栅栏(Menory Barrier),这些指令可以刷新缓存,内存栅栏可能对性能带来影响,因为他们会抑制一些编译器优化操作,比如不能指令重排。

线程阻塞:一个线程可能会阻塞,尤其在存在锁竞争的情况,竞争失败的线程肯定会阻塞。线程发生阻塞JVM可能会使线程自旋或者挂起,如果对线程进行挂起,那么就会多两个额外的上下文切换,挂起和恢复。

多线程最主要的额外开销:锁竞争

在Java中多线程额外开销最主要的原因就是锁竞争,因为获取锁会导致串行操作同时获取锁失败会导致线程挂起出现上下文竞争。所以减少锁竞争是降低多线程开销最主要的手段。

而在并发程序中,对可伸缩性的最主要威胁就是独占方式的资源锁。而影响锁竞争的因素:锁请求频率,每次持有锁的时间,如果两个的乘积很小,说明获取锁和持有锁的总时间就很少,那么大多数获取锁的操作都不会发生竞争。

所以减少锁竞争的主要方法是:减少锁的只有时间、降低锁的请求频次、或者使用带有协调机制的独占锁替代,因为这些机制允许更高的并发性。

减少锁竞争手段

第一个是缩小锁的范围:将与锁无关代码移除同步代码块,尤其是那些可能发生阻塞的操作比如I/O;

第二个是减少锁的粒度:使用多个相互独立锁管理独立的状态变量,改变某个变量只用获取对应变量锁,而不用获取整体锁,其他线程仍然能使用其他变量。但是使用锁越多,那么发生死锁的风险也就越高。

第三个是锁分段:比如ConcurrentHashMap底层的链表数组,对数组中每一个数组元素进行加锁,数组长度是多少就有多少个锁,也就最大支持多少并发。不过在对数组扩张的时候就会更加复杂;

第四个是避免热点域:当一个操作要访问多个变量时,锁的粒度就很难减少了,一种解决方法是将这多个变量的计数结果缓存起来,都会引入一些,这些热点域往往会限制可伸缩性。

比如Map的size方法,每次可以遍历链表数组每个元素来统计,不过可以优化成记录一个计数器,每次调用put和remove时更新下,只是稍微增加了点两个方法的开销却大幅提高了size方法。

但是在多线程的情况下,put和remove方法都要更新这个值,如果对计数器同步,那么实际上这个伸缩性就降低了。ConcurrentHashMap中的size将统计每段的数量,而每段的数量由各段进行记录,这样既不影响原有并发,还能很好的支持size方法,ConcurrentHashMap的size方法最终是sunCount方法,如下图:

其他方法:替换锁

可以使用一些其他方法来替代独占锁方法:

1、ReadWriteLock(读写锁)多个读一个写的加锁规则,因为读是不会修改资源的。

2、 原子变量:主要解决热点域问题,例如AtomicLong可以用来记录Map的长度,在JVM底层对于这类原子变量支持的比较好,性能影响不大。

总结

单线程性能不高,不能充分利用CPU,可伸缩性不高,使用多线程能够充分的利用CPU,提高系统的可伸缩性,但是多线程会有一些额外的开销,而额外开销最主要在于由于独占锁导致的锁竞争产生的串行执行和上下文切换,解决独占锁方法主要是降低锁的使用时间和锁的粒度,或者采用其他方式替换独占锁。

Java程序员日常学习笔记,如理解有误欢迎各位交流讨论!

本文内容由小美整理编辑!