Java并发编程性能详解

1、介绍java

本文重点讨论多线程应用程序的性能问题。如用何种技术方法来减小锁竞争,以及如何用代码来实现。缓存

2、性能数据结构

咱们都知道,多线程能够提升线程的性能。性能提高的根本缘由在于咱们有多核的CPU或多个CPU。每一个CPU的内核均可以本身完成任务,所以把一个大的任务分解成一系列的可彼此独立运行的小任务就能够提升程序的总体性能了。能够举个例子,好比有个程序用来将硬盘上某个文件夹下的全部图片的尺寸进行修改,应用多线程技术就能够提升它的性能。使用单线程的方式只能依次遍历全部图片文件而且执行修改,若是咱们的CPU有多个核心的话,毫无疑问,它只能利用其中的一个核。使用多线程的方式的话,咱们可让一个生产者线程扫描文件系统把每一个图片都添加到一个队列中,而后用多个工做线程来执行这些任务。若是咱们的工做线程的数量和CPU总的核心数同样的话,咱们就能保证每一个CPU核心都有活可干,直到任务被所有执行完成。多线程

对于另一种须要较多IO等待的程序来讲,利用多线程技术也能提升总体性能。假设咱们要写这样一个程序,须要抓取某个网站的全部HTML文件,而且将它们存储到本地磁盘上。程序能够从某一个网页开始,而后解析这个网页中全部指向本网站的连接,而后依次抓取这些连接,这样周而复始。由于从咱们对远程网站发起请求到接收到全部的网页数据须要等待一段时间,因此咱们能够将此任务交给多个线程来执行。让一个或稍微更多一点的线程来解析已经收到的HTML网页以及将找到的连接放入队列中,让其余全部的线程负责请求获取页面。ide

高性能就是在短的时间窗口内作尽可能多的事情。这个固然是对性能一词的最经典解释了。可是同时,使用线程也能很好地提高咱们程序的响应速度。想象咱们有这样一个图形界面的应用程序,上方有一个输入框,输入框下面有一个名字叫“处理”的按钮。当用户按下这个按钮的时候,应用程序须要从新对按钮的状态进行渲染(按钮看起来被按下了,当松开鼠标左键时又恢复原状),而且开始对用户的输入进行处理。若是处理用户输入的这个任务比较耗时的话,单线程的程序就没法继续响应用户其余的输入动做了,函数

可扩展性(Scalability)的意思是程序具有这样的能力:经过添加计算资源就能够得到更高的性能。想象咱们须要调整不少图片的大小,由于咱们机器的CPU核心数是有限的,因此增长线程数量并不总能相应提升性能。相反,由于调度器须要负责更多线程的建立和关闭,也会占用CPU资源,反而有可能下降性能。性能

一、对性能的影响优化

写到这里,咱们已经取得这样一个观点:增长更多的线程能够提升程序的性能和响应速度。可是另外一方面,想要取得这些好处却并不是垂手可得,也须要付出一些代价。线程的使用对性能的提高也会有所影响。网站

首先,第一个影响来自线程建立的时候。线程的建立过程当中,JVM须要从底层操做系统申请相应的资源,而且在调度器中初始化数据结构,以便决定执行线程的顺序。操作系统

若是你的线程的数量和CPU的核心数量同样的话,每一个线程都会运行在一个核心上,这样或许他们就不会常常被打断了。可是事实上,在你的程序运行的时候,操做系统也会有些本身的运算须要CPU去处理。因此,即便这种情形下,你的线程也会被打断而且等待操做系统来从新恢复它的运行。当你的线程数量超过CPU的核心数量的时候,状况有可能变得更坏。在这种状况下,JVM的进程调度器会打断某些线程以便让其余线程执行,线程切换的时候,刚才正在运行的线程的当前状态须要被保存下来,以便等下次运行的时候能够恢复数据状态。不只如此,调度器也会对它本身内部的数据结构进行更新,而这也须要消耗CPU周期。全部这些都意味着,线程之间的上下文切换会消耗CPU计算资源,所以带来相比单线程状况下没有的性能开销。

多线程程序所带来的另一个开销来自对共享数据的同步访问保护。咱们可使用synchronized关键字来进行同步保护,也可使用Volatile关键字来在多个线程之间共享数据。若是多于一个线程想要去访问某一个共享数据结构的话,就发生了争用的情形,这时,JVM须要决定哪一个进程先,哪一个进程后。若是决定该要执行的线程不是当前正在运行的线程,那么就会发生线程切换。当前线程须要等待,直到它成功得到了锁对象。JVM能够本身决定如何来执行这种“等待”,假如JVM预计离成功得到锁对象的时间比较短,那JVM可使用激进等待方法,好比,不停地尝试得到锁对象,直到成功,在这种状况下这种方式可能会更高效,由于比较进程上下文切换来讲,仍是这种方式更快速一些。把一个等待状态的线程挪回到执行队列也会带来额外的开销。

所以,咱们要尽力避免因为锁竞争而带来的上下文切换。

下面将具体阐述两种下降这种竞争发生的方法。

二、锁竞争

两个或更多线程对锁的竞争访问会带来额外的运算开销,由于竞争的发生逼迫调度器来让一个线程进入激进等待状态,或者让它进行等待状态而引起两次上下文切换。有某些状况下,锁竞争的恶果能够经过如下方法来减轻:

1.少锁的做用域;

2.少须要获取锁的频率;

3.量使用由硬件支持的乐观锁操做,而不是synchronized;

4.量少用synchronized;

5.少使用对象缓存

  

2.1 缩减同步域

  若是代码持有锁超过必要的时间,那么能够应用这第一种方法。一般咱们能够将一行或多行代码移出同步区域来下降当前线程持有锁的时间。在同步区域里运行的代码数量越少,当前线程就会越早地释放锁,从而让其余线程更早地得到锁。这与Amdahl法则相一致的,由于这样作减小了须要同步执行的代码量。

2.2 分拆锁

另一种减小锁竞争的方法是将一块被锁定保护的代码分散到多个更小的保护块中。若是你的程序中使用了一个锁来保护多个不一样对象的话,这种方式会有用武之地。假设咱们想要经过程序来统计一些数据,而且实现了一个简单的计数类来持有多个不一样的统计指标,而且分别用一个基本计数变量来表示(long类型)。由于咱们的程序是多线程的,因此咱们须要对访问这些变量的操做进行同步保护,由于这些操做动做来自不一样的线程。要达到这个目的,最简单的方式就是对每一个访问了这些变量的函数添加synchronized关键字。

2.3 分离锁

上面一个例子展现了如何将一个单独的锁分开为多个单独的锁,这样使得各线程仅仅得到他们将要修改的对象的锁就能够了。可是另外一方面,这种方式也增长了程序的复杂度,若是实现不恰当的话也可能形成死锁。

分离锁是与分拆锁相似的一种方法,可是分拆锁是增长锁来保护不一样的代码片断或对象,而分离锁是使用不一样的锁来保护不一样范围的数值。JDK的java.util.concurrent包里的ConcurrentHashMap即便用了这种思想来提升那些严重依赖HashMap的程序的性能。在实现上,ConcurrentHashMap内部使用了16个不一样的锁,而不是封装一个同步保护的HashMap。16个锁每个负责保护其中16分之一的桶位(bucket)的同步访问。这样一来,不一样的线程想要向不一样的段插入键的时候,相应的操做会受到不一样的锁来保护。可是反过来也会带来一些很差的问题,好比,某些操做的完成如今须要获取多个锁而不是一个锁。若是你想要复制整个Map的话,这16个锁都须要得到才能完成。

2.4 原子操做

另一种减小锁竞争的方法是使用原子操做。java.util.concurrent包对一些经常使用基础数据类型提供了原子操做封装的类。原子操做类的实现基于处理器提供的“比较置换”功能(CAS),CAS操做只在当前寄存器的值跟操做提供的旧的值同样的时候才会执行更新操做。

这个原理能够用来以乐观的方式来增长一个变量的值。若是咱们的线程知道当前的值的话,就会尝试使用CAS操做来执行增长操做。若是期间别的线程已经修改了变量的值,那么线程提供的所谓的当前值已经跟真实的值不同了,这时JVM来尝试从新得到当前值,而且再尝试一次,反反复复直到成功为止。虽然循环操做会浪费一些CPU周期,可是这样作的好处是,咱们不须要任何形式的同步控制。

2.5 避免热点代码段

一个典型的LIST实现经过会在内容维护一个变量来记录LIST自身所包含的元素个数,每一次从列表里删除或增长元素的时候,这个变量的值都会改变。若是LIST在单线程应用中使用的话,这种方式无可厚非,每次调用size()时直接返回上一次计算以后的数值就好了。若是LIST内部不维护这个计数变量的话,每次调用size()操做都会引起LIST从新遍历计算元素个数。

这种不少数据结构都使用了的优化方式,到了多线程环境下时却会成为一个问题。假设咱们在多个线程之间共享一个LIST,多个线程同时地去向LIST里面增长或删除元素,同时去查询大的长度。这时,LIST内部的计数变量成为一个共享资源,所以全部对它的访问都必须进行同步处理。所以,计数变量成为整个LIST实现中的一个热点。

本文所讲述的这些优化方案再一次的代表,每一种优化方式在真正应用的时候必定须要多多仔细观测。不成熟的优化方案表面看起来好像颇有道理,可是事实上颇有可能会反过来成为性能的瓶颈。

相关文章
相关标签/搜索