Java性能权威指南-总结17

news/2024/11/16 21:27:54/

Java性能权威指南-总结17

  • 线程与同步的性能
    • 线程池与ThreadPoolExecutor
      • 线程池任务大小
      • 设置ThreadPoolExecutor的大小
    • 线程同步
      • 同步的代价

线程与同步的性能

线程池与ThreadPoolExecutor

线程池任务大小

等待线程池来执行的任务会被保存到某类队列或列表中;当池中有线程可以执行任务时,就从队列中拉出一个。这会导致不均衡:队列中任务的数量有可能变得非常大。如果队列太大,其中的任务就必须等待很长时间,直到前面的任务执行完毕。例如一个超负荷的Web服务器:如果有个任务被添加到队列中,但是没有在3秒钟内执行,那用户很可能就去看另一个页面了。

因此,对于容纳等待执行任务的队列,线程池通常会限制其大小。根据用于容纳等待执行任务的数据结构的不同,ThreadPoolExecutor会有不同的处理方式;应用服务器通常有一些调优参数,可以调整这个值。

就像线程池的最大线程数,这个值应该如何调优,并没有一个通用的规则。举例而言,假设某个应用服务器的任务队列中有30000个任务,有4个CPU可用,如果执行一个任务只需要50毫秒,同时假设这段时间不会到达新任务,则清空任务队列需要6分钟。这可能是可以接受的,但如果每个任务需要1秒钟,则清空任务队列需要2小时。因此,若要确定使用哪个值能带来需要的性能,测量真实应用是唯一的途径。

不管是哪种情况,如果达到了队列数限制,再添加任务就会失败。 ThreadPoolExecutor有一个rejectedExecution方法,用于处理这种情况(默认会抛出RejectedExecutionException)。应用服务器会向用户返回某个错误:或者是HTTP状态码500(内部错误),或者是Web服务器捕获错误,并向用户给出合理的解释消息——其中后者是最理想的。

设置ThreadPoolExecutor的大小

线程池的一般行为是这样的:创建时准备好最小数目的线程,如果来了一个任务,而此时所有的线程都在忙碌,则启动一个新线程(一直到达到最大线程数),任务就可以立即执行了。否则,任务被加入等待队列,如果任务队列中已经无法加入新任务,则拒绝。 不过,ThreadPoolExecutor的表现可能和这种标准行为有点不同。

根据所选任务队列的类型,ThreadPoolExecutor会决定何时启动一个新线程。 有以下3种可能:

SynchronousQueue
如果ThreadPoolExecutor搭配的是SynchronousQueue,则线程池的行为会和我们预计的一样,它会考虑线程数:如果所有的线程都在忙碌,而且池中的线程数尚未达到最大,则新任务会启动一个新线程。然而,这个队列没办法保存等待的任务:如果来了一个任务,创建的线程数已经达到最大值,而且所有线程都在忙碌,则新的任务总是会被拒绝。所以如果只是管理少量的任务,这是个不错的选择;但是对于其他情况,就不合适了。该类文档建议将最大线程数指定为一个非常大的值,如果任务完全是CPU密集型的,这可能行得通,但是我们会看到,其他情况下可能会适得其反。另一方面,如果需要一个容易调整线程数的线程池,这种选择会更好。

无界队列
如果ThreadPoolExecutor搭配的是无界队列(比如LinkedBlockedingQueue),则不会拒绝任何任务(因为队列大小没有限制)。这种情况下,ThreadPoolExecutor最多仅会按最小线程数创建线程,也就是说,最大线程池大小被忽略了。如果最大线程数和最小线程数相同,则这种选择和配置了固定线程数的传统线程池运行机制最为接近。

有界队列
在决定何时启动一个新线程时,使用了有界队列(如ArrayBlockingQueue)的ThreadPoolExecutor会采用一个非常复杂的算法。比如,假设池的核心大小为4,最大为8,所用的ArrayBlockingQueue最大为10。随着任务到达并被放到队列中,线程池中最多会运行4个线程(也就是核心大小)。即使队列完全填满,也就是说有10个处于等待状态的任务,ThreadPoolExecutor也是只利用4个线程。

如果队列已满,而又有新任务加进来,此时才会启动一个新线程。 这里不会因为队列已满而拒绝该任务,相反,会启动一个新线程。新线程会运行队列中的第一个任务,为新来的任务腾出空间。在这个例子中,池中会有8个线程(最大线程数)的唯一一种情形是,有7个任务正在处理,队列中有10个任务,这时又来了一个新任务。

这个算法背后的理念是,该池大部分时间仅使用核心线程(4个),即使有适量的任务在队列中等待运行。这时线程池就可以用作节流阀(这是很有好处的)。如果积压的请求变得非常多,该池就会尝试运行更多线程来清理;这时第二个节流阀——最大线程数——就起作用了。

如果系统没有外部瓶颈,CPU周期也足够,那一切就都解决了:加入新的线程可以更快地处理任务队列,并很可能使其回到预期大小。该算法所适合的用例当然也很容易构造。

另一方面,该算法并不知道队列为何会突然增大。如果是因为外部的任务积压,那么加入更多线程并非明智之举。如果该线程所运行的机器已经是CPU密集型的,加入更多线程也是错误的。只有当任务积压是由额外的负载进入系统(比如有更多客户端发起HTTP请求)引发时,增加线程才是有意义的。(如果是这种情况,为什么要等到队列已经接近某个边界时才增加呢?如果有额外的资源供更多线程使用,则尽早增加线程将改善系统的整体性能。)

对于上面提到的每一种选择,都能找到很多支持或反对的论据,但是在尝试获得最好的性能时,可以应用KISS原则"Keep it simple,stupid"。可以将ThreadPoolExecutor的核心线程数和最大线程数设为相同,在保存等待任务方面,如果适合使用无界任务列表,则选择LinkedBlockingQueue;如果适合使用有界任务列表,则选择ArrayBlockingQueue

快速小结

  1. 有时对象池也是不错的选择,线程池就是情形之一:线程初始化的成本很高,线程池使得系统上的线程数容易控制。
  2. 线程池必须仔细调优。盲目向池中添加新线程,在某些情况下对性能会有不利影响。
  3. 在使用ThreadPooLExecutor时,选择更简单的选项通常会带来最好的、最能预见的性能。

线程同步

同步与Java并发设施在此部分,当用到“同步”(synchronization)这个术语时,它指的是这样的代码:这段代码在一个代码块内,它们对一组变量的访问看上去是串行的,每次只有一个线程能访问内存。具体而言,
既包括用synchronized关键字保护的代码块,也包括用java.util.concurrent.lock.Lock实例保护的代码,再就是java.util.concurrent和java.util.concurrent.atomic包内的代码。
严格来讲,atomic下的类并没有使用同步,至少从CPU编程术语来看是这样的。它们利用了“比较与交换”(Compare and Swap,CAS)CPU指令,而同步需要互斥访问某个资源。
在同步访问同一资源时,利用了CAS指令的线程不会阻塞,而对于需要同步锁的线程而言,如果另一个线程占据了该资源,则这个线程会阻塞。这两种方式之间存在性能的权衡。然而,即使CAS指令是无锁、非阻塞的,它们仍然会表现出阻塞方式所具有的大部分行为:在开发者看来,最终结果看上去还是线程只能串行地访问被保护内存。

同步的代价

同步代码对性能有两个方面的影响。其一,应用在同步块上所花的时间会影响该应用的可伸缩性。其二,获取同步锁需要一些CPU周期,所以也会影响性能。

  1. 同步与可伸缩性

当某个应用被分割到多个线程上运行时,加速比(Speedup)可以用如下等式定义(即Amdahl定律):

Speedup = 1 / ((1 - P) + P / N)

P是程序并行运行部分所花的时间,N是所用到的线程数(假定每个线程总有CPU可用)。所以,如果20%的代码是串行执行的(这意味着P是80%),有8个CPU可用,则可以预计存在并发的情况下加速比为3.33。

从这个等式可以看出一个关键事实,即随着P值的降低(也就是说,有更多代码是串行执行的),引入多个线程所带来的性能优势也会随之下降。限制串行块中的代码量之所以如此重要,原因就在于此。在这个例子中,有8个CPU可用,我们可能会希望速度提升8倍。但是在只有20%的代码串行执行时,引入多个线程的好处就少了一半多(只增加了3.33倍)。

  1. 锁定对象的开销

除了对可伸缩性的影响,同步操作本身还有两个基本的开销。首先是获取同步锁的成本。如果某个锁没有被争用(即两个线程没有同时尝试访问这个锁),那这方面的开销会相当小。synchronized关键字和CAS指令之间有轻微的差别。非竞争的synchronized锁被称为非膨胀(uninflated)锁,获取非膨胀锁的开销在几百纳秒的数量级。非竞争的CAS代码损失会更小。

在存在竞争的条件下,开销会更高。当第2个线程尝试访问某个同步锁时,可以预见这个锁会变成膨胀的(inflated)。这个成本是固定的,不管是2个还是20个线程要访问同一个锁,执行的代码量是一样的。(20个线程都必须执行加锁代码,当然,成本会随线程数增加,但每个线程所花的时间是固定的,这是重点。)

对于使用CAS指令的代码,当存在竞争时,开销是无法预测的。CAS原语基于一种乐观的策略:线程设置某个值,执行一些代码,然后确保初始值没有被修改。如果值被修改了,那么基于CAS的代码必须再次执行这些代码。在最坏的情况下,如果有两个线程,它们都在修改CAS所保护的值,那么相互就会看到另一个线程同时也在修改这个值,就有可能会陷入无限循环。不过在实践中,两个线程不会进入这样的无限循环,但是随着竞争CAS所保护值的线程数的增加,重试次数也会增加。(如果此处的操作是只读的,那基于CAS的保护不会受竞争访问的影响。比如,不管有多少线程,它们都可以同时在同一个对象上调用AtomicLong.get()方法,而不用因竞争付出任何代价。这是使用基于CAS的设施的另一个重要优势。)

同步的目的是保护对内存中值(也就是变量)的访问。 变量可能会临时保存在寄存器中,这要比直接在主内存中访问更高效。寄存器值对其他线程是不可见的;当前线程修改了寄存器中的某个值,必须在某个时机把寄存器中的值刷新到主内存中,以便其他线程可以看到这个值。而寄存器值必须刷新的时机,就是由线程同步控制的。

实际的语言会非常复杂,最简单的理解是,当一个线程离开某个同步块时,必须将任何修改过的值刷新到主内存中。这意味着进入该同步块的其他线程将能看到最新修改的值。类似地,基于CAS的保护确保操作期间修改的变量被刷新到主内存中,标记为volatile的变量,无论什么时候被修改了,总会在主内存中更新。

应该学习避免使用Java中性能不太高的构造,即使这看上去像“过早地优化”自己的代码(事实并非如此)。下面循环中有个有趣的案例,而且是一个现实中的例子:

	Vector v;for (inti = 0; i < v.size(); i++) {process(v.get(i));}

在生产中,这个循环消耗的时间惊人。比较合乎逻辑的假设是,process()方法是罪魁祸首。但事实并非如此,问题也不在于size()get()方法调用本身(调用已经被编译器内联了)。Vector类的size()get()方法是同步的,所有这些调用所需要的寄存器刷新是很大的性能问题。

这段代码之所以不理想,还有其他一些原因。特别是,在某个线程调用size()和调用get()的中间时间内,Vector对象的状态有可能会发生变化。如果在此之间,另一个线程移除了这个对象的最后一个元素,则get()方法将抛出ArrayIndexOutofBoundsException。除了代码中的语义问题,细粒度的同步也是较差的选择。

一种方案是将大量连续的、细粒度的同步调用包含在一个同步块内:

	synchronized(v) {for (int i = 0; i < v.size(); i++){process(v.get(i));}}

如果process()方法执行时间很长,并不能很好地解决问题,因为这个Vector对象没法并行处理。另一个备选方案,复制并分割Vector对象可能是必要的,这样就可以借助副本来并行处理其中的元素,不过其他线程仍然可能会修改原始的Vector对象。

寄存器刷新的影响也和程序运行所在的处理器种类有关;有大量供线程使用的寄存器的处理器与较简单的处理器相比,将需要更多刷新。实际上,这段代码在许多环境上执行了很长时间,而没有出现问题。只是在尝试每个线程有大量寄存器可用、基于SPARC的大型机时,才出现了问题。

快速小结

  1. 线程同步有两个性能方面的代价:限制了应用的可伸缩性,以及获取锁是有开销的。
  2. 同步的内存语义、基于CAS的设施和volatile关键字对性能可能会有很大的影响,特别是在有很多寄存器的大型机上。

http://www.ppmy.cn/news/561095.html

相关文章

k8s kubectl常用命令

kubectl 是 Kubernetes 的一个命令行管理工具&#xff0c;可用于 Kubernetes 上的应用部署和日常管理。本文列举了 9 个常见的 kubectl 命令&#xff0c;并总结了一些使用技巧&#xff0c;希望可以帮助系统管理员简化管理工作。 一、使用 Kubectl 查询、创建、编辑和删除资源 …

解决edge浏览器主页被联想网址篡改问题

新买回来的电脑&#xff0c;刚打开浏览器&#xff0c;主页是这个样子&#xff01;&#xff01;&#xff01;&#xff01;还取消不了&#xff0c;是不是很无语。。。 1.打开&#xff1a;联想电脑管家 取消上网主页 锁定锁 2.打开浏览器 关闭浏览器&#xff0c;重新打开 上网…

HTML+CSS实现拼多多官网首页

写得一般&#xff0c;愿与大家共同进步&#xff08;不会制作GIF图片&#xff09; 一、 以下为部分实现图片 二、HTML代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatib…

联想摄像头无法一直运行

问题&#xff1a; 摄像头有间隔地运行&#xff0c;指示灯一会亮&#xff0c;一会不亮&#xff1b;在浏览器进行面试的时候&#xff0c;摄像头只照一次就不运行了。 解决&#xff1a; 找了无数方法&#xff0c;最终还是驱动的问题。 1&#xff09; 去联想官网下载驱动&#…

联想电脑管家不显示开机时间_联想电脑怎么设置显示开机时间

电脑管家每次在开机的时候会提示开机用时&#xff0c;这个功能怎么设置呢?下面由小编为你整理了的相关方法&#xff0c;希望对你有帮助!电脑管家设置提示开机时间步骤如下1首先&#xff0c;没有此软件的朋友&#xff0c;先从百度官网下载该软件。百度搜索电脑管家&#xff0c;…

搜索下拉框推广优化如何做?下拉联想词有什么优势?

下拉词也叫推荐词&#xff0c;就是让网友输入更少的词&#xff0c;看到更多与搜索词相关的推荐词。例如百度下拉词是百度搜索为方便用户搜索而提供的关键字关联服务&#xff0c;提高了用户的搜索效率。大多数人在搜索关键字时不知道如何组织语言以达到更准确的搜索目的&#xf…

无线打印便捷不止是说说 联想小新打印机初体验

说不清什么时候开始&#xff0c;“打印机”就快成为家庭用户的标配了&#xff0c;尤其是那些有孩子的家庭&#xff0c;选购打印机成为了早晚的事。就在我们全面体验联想全新小新打印机的时候&#xff0c;我的一个同事咨询了我购买打印机的事情&#xff0c;而她对于产品方面的一…

青龙羊毛——美团联想商城喜爱帮(搬运)

前几天有人评论区要新的美团脚本&#xff0c;更新一下&#xff0c;顺便更新一下萝卜大佬写的小毛&#xff01; 1.喜爱帮 &#xff08;1&#xff09;入口 羊毛来源——喜爱帮 &#xff08;2&#xff09;脚本 不会拉库就别撸毛了&#xff0c;撸不明白&#xff01; ql raw http…