答疑课堂:模块三热点问题解答 - 图1答疑课堂:模块三热点问题解答

你好,我是刘超。

不知不觉“多线程性能优化“已经讲完了,今天这讲我来解答下各位同学在这个模块集中提出的两⼤问题,第⼀个是有关监测上

答疑课堂:模块三热点问题解答 - 图2答疑课堂:模块三热点问题解答 - 图3
下⽂切换异常的命令排查⼯具,第⼆个是有关blockingQueue的内容。

也欢迎你积极留⾔给我,让我知晓你想了解的内容,或者说出你的困惑,我们共同探讨。下⾯我就直接切⼊今天的主题了。

使⽤系统命令查看上下⽂切换
在第15讲中我提到了上下⽂切换,其中有⽤到⼀些⼯具进⾏监测,由于篇幅关系就没有详细介绍,今天我就补充总结⼏个常
⽤的⼯具给你。

1. Linux命令⾏⼯具之vmstat命令
vmstat是⼀款指定采样周期和次数的功能性监测⼯具,我们可以使⽤它监控进程上下⽂切换的情况。
vmstat 1 3 命令⾏代表每秒收集⼀次性能指标,总共获取3次。以下为上图中各个性能指标的注释:

procs
r:等待运⾏的进程数
b:处于⾮中断睡眠状态的进程数

memory

swpd:虚拟内存使⽤情况

free:空闲的内存
buff:⽤来作为缓冲的内存数
cache:缓存⼤⼩

swap

si:从磁盘交换到内存的交换⻚数量
so:从内存交换到磁盘的交换⻚数量

io

bi:发送到块设备的块数
bo:从块设备接收到的块数

system

in:每秒中断数
cs:每秒上下⽂切换次数

cpu

us:⽤户CPU使⽤时间
sy:内核CPU系统使⽤时间
id:空闲时间
wa:等待I/O时间
st:运⾏虚拟机窃取的时间

Linux命令⾏⼯具之pidstat命令

我们通过上述的vmstat命令只能观察到哪个进程的上下⽂切换出现了异常,那如果是要查看哪个线程的上下⽂出现了异常呢?

pidstat命令就可以帮助我们监测到具体线程的上下⽂切换。pidstat是Sysstat中⼀个组件,也是⼀款功能强⼤的性能监测⼯具。我们可以通过命令 yum install sysstat 安装该监控组件。

通过pidstat -help命令,我们可以查看到有以下⼏个常⽤参数可以监测线程的性能:
答疑课堂:模块三热点问题解答 - 图4
常⽤参数:

-u:默认参数,显示各个进程的cpu使⽤情况;
-r:显示各个进程的内存使⽤情况;
-d:显示各个进程的I/O使⽤情况;
-w:显示每个进程的上下⽂切换情况;
-p:指定进程号;
-t:显示进程中线程的统计信息

⾸先,通过pidstat -w -p pid 命令⾏,我们可以查看到进程的上下⽂切换:
答疑课堂:模块三热点问题解答 - 图5
cswch/s:每秒主动任务上下⽂切换数量

nvcswch/s:每秒被动任务上下⽂切换数量

之后,通过pidstat -w -p pid -t 命令⾏,我们可以查看到具体线程的上下⽂切换:
答疑课堂:模块三热点问题解答 - 图6

JDK⼯具之jstack命令

查看具体线程的上下⽂切换异常,我们还可以使⽤jstack命令查看线程堆栈的运⾏情况。jstack是JDK⾃带的线程堆栈分析⼯具,使⽤该命令可以查看或导出 Java 应⽤程序中的线程堆栈信息。

jstack最常⽤的功能就是使⽤ jstack pid 命令查看线程堆栈信息,通常是结合pidstat -p pid -t⼀起查看具体线程的状态,也经常
⽤来排查⼀些死锁的异常。
答疑课堂:模块三热点问题解答 - 图7
每个线程堆栈的信息中,都可以查看到线程ID、线程状态(wait、sleep、running等状态)以及是否持有锁等。

我们可以通过jstack 16079 > /usr/dump将线程堆栈信息⽇志dump下来,之后打开dump⽂件,通过查看线程的状态变化,就可以找出导致上下⽂切换异常的具体原因。例如,系统出现了⼤量处于BLOCKED状态的线程,我们就需要⽴刻分析代码找出原因。

多线程队列
针对这讲的第⼀个问题,⼀份上下⽂切换的命令排查⼯具就总结完了。下⾯我来解答第⼆个问题,是在17讲中呼声⽐较⾼的有关blockingQueue的内容。

在Java多线程应⽤中,特别是在线程池中,队列的使⽤率⾮常⾼。Java提供的线程安全队列⼜分为了阻塞队列和⾮阻塞队
列。

1.阻塞队列

我们先来看下阻塞队列。阻塞队列可以很好地⽀持⽣产者和消费者模式的相互等待,当队列为空的时候,消费线程会阻塞等待队列不为空;当队列满了的时候,⽣产线程会阻塞直到队列不满。

在Java线程池中,也⽤到了阻塞队列。当创建的线程数量超过核⼼线程数时,新建的任务将会被放到阻塞队列中。我们可以根据⾃⼰的业务需求来选择使⽤哪⼀种阻塞队列,阻塞队列通常包括以下⼏种:

ArrayBlockingQueue:⼀个基于数组结构实现的有界阻塞队列,按 FIFO(先进先出)原则对元素进⾏排序,使⽤
ReentrantLock、Condition来实现线程安全;
LinkedBlockingQueue:⼀个基于链表结构实现的阻塞队列,同样按FIFO (先进先出) 原则对元素进⾏排序,使⽤
ReentrantLock、Condition来实现线程安全,吞吐量通常要⾼于ArrayBlockingQueue;
PriorityBlockingQueue:⼀个具有优先级的⽆限阻塞队列,基于⼆叉堆结构实现的⽆界限(最⼤值Integer.MAX_VALUE -
8)阻塞队列,队列没有实现排序,但每当有数据变更时,都会将最⼩或最⼤的数据放在堆最上⾯的节点上,该队列也是使
⽤了ReentrantLock、Condition实现的线程安全;
DelayQueue:⼀个⽀持延时获取元素的⽆界阻塞队列,基于PriorityBlockingQueue扩展实现,与其不同的是实现了Delay
延时接⼝;
SynchronousQueue:⼀个不存储多个元素的阻塞队列,每次进⾏放⼊数据时, 必须等待相应的消费者取⾛数据后,才可以再次放⼊数据,该队列使⽤了两种模式来管理元素,⼀种是使⽤先进先出的队列,⼀种是使⽤后进先出的栈,使⽤哪种模式可以通过构造函数来指定。

Java线程池Executors还实现了以下四种类型的ThreadPoolExecutor,分别对应以上队列,详情如下:
答疑课堂:模块三热点问题解答 - 图8

2.⾮阻塞队列

我们常⽤的线程安全的⾮阻塞队列是ConcurrentLinkedQueue,它是⼀种⽆界线程安全队列(FIFO),基于链表结构实现,利⽤
CAS乐观锁来保证线程安全。

下⾯我们通过源码来分析下该队列的构造、⼊列以及出列的具体实现。

构造函数:ConcurrentLinkedQueue由head 、tair节点组成,每个节点(Node)由节点元素(item)和指向下⼀个节点的引⽤
(next) 组成,节点与节点之间通过 next 关联,从⽽组成⼀张链表结构的队列。在队列初始化时, head 节点存储的元素为空,tair 节点等于 head 节点。


public ConcurrentLinkedQueue() { head = tail = new Node(null);
}

private static class Node { volatile E item; volatile Node next;
.
.
}

⼊列:当⼀个线程⼊列⼀个数据时,会将该数据封装成⼀个Node节点,并先获取到队列的队尾节点,当确定此时队尾节点的
next值为null之后,再通过CAS将新队尾节点的next值设为新节点。此时p != t,也就是设置next值成功,然后再通过CAS将队尾节点设置为当前节点即可。

public boolean offer(E e) { checkNotNull(e);
//创建⼊队节点
final Node newNode = new Node(e);
//t,p为尾节点,默认相等,采⽤失败即重试的⽅式,直到⼊队成功for (Node t = tail, p = t;;) {
//获取队尾节点的下⼀个节点
Node q = p.next;
//如果q为null,则代表p就是队尾节点if (q == null) {
//将⼊列节点设置为当前队尾节点的next节点
if (p.casNext(null, newNode)) {
//判断tail节点和p节点距离达到两个节点
if (p != t) // hop two nodes at a time
//如果tail不是尾节点则将⼊队节点设置为tail。
// 如果失败了,那么说明有其他线程已经把tail移动过
casTail(t, newNode); // Failure is OK. return true;
}
}
// 如果p节点等于p的next节点,则说明p节点和q节点都为空,表示队列刚初始化,所以返回
else if (p == q)
p = (t != (t = tail)) ? t : head; else
// Check for tail updates after two hops. p = (p != t && t != (t = tail)) ? t : q;
}
}

出列:⾸先获取head节点,并判断item是否为null,如果为空,则表示已经有⼀个线程刚刚进⾏了出列操作,然后更新head节
点;如果不为空,则使⽤CAS操作将head节点设置为null,CAS就会成功地直接返回节点元素,否则还是更新head节点。

public E poll() {
// 设置起始点restartFromHead: for (;;) {
//p获取head节点
for (Node h = head, p = h, q;;) {
//获取头节点元素
E item = p.item;
//如果头节点元素不为null,通过cas设置p节点引⽤的元素为null if (item != null && p.casItem(item, null)) {
// Successful CAS is the linearization point
// for item to be removed from this queue. if (p != h) // hop two nodes at a time
updateHead(h, ((q = p.next) != null) ? q : p); return item;
}
//如果p节点的下⼀个节点为null,则说明这个队列为空,更新head结点else if ((q = p.next) == null) {
updateHead(h, p); return null;
}
//节点出队失败,重新跳到restartFromHead来进⾏出队else if (p == q)
continue restartFromHead; else
p = q;
}
}
}

ConcurrentLinkedQueue是基于CAS乐观锁实现的,在并发时的性能要好于其它阻塞队列,因此很适合作为⾼并发场景下的排
队队列。

今天的答疑就到这⾥,如果你还有其它问题,请在留⾔区中提出,我会⼀⼀解答。最后欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他加⼊讨论。

答疑课堂:模块三热点问题解答 - 图9

  1. 精选留⾔

答疑课堂:模块三热点问题解答 - 图10-W.LI-
⽼师好!FGC正常情况多久⼀次⽐较合适啊?我们项⽬1.2天⼀次FGC⽼年代给了3G年轻代1G想吧年轻代给多点。有个定时任务
,2⼩时⼀次⽤的线程池。给了40个线程并发请求4K次。设置了空闲回收策略回收核⼼线程。现在就是定时任务,每次都新建4
0个线程⼀张吃⽼年代内存。不设置回收这些线程不实⽤的那个吧⼩时就⼀直阻塞。怎么处理⽐较合适
2019-07-04 16:12
作者回复
GC在核⼼业务应⽤服务中越久发⽣越合适,且GC的时间不要太⻓。⼀般⽣产环境的FGC⼏天⼀次是⽐较正常的。40个线程是 不是设置太⼤了,建议调⼩⼀些,当然需要你们具体压测验证下调⼩后的性能情况。

年轻代可以调⼤⼀些,如果年轻代太⼩,当MinorGC时,发现年轻代依然存活满对象,新的对象可能将⽆法放⼊到年轻代,则会通过分配担保机制提前转移年轻代的存活对象到⽼年代中,这样反⽽会增加⽼年代的负担。默认情况下⽼年代和新⽣代是2:1
。建议没有特殊情况,不要固定设置⽼年代和新⽣代。
2019-07-05 09:36

答疑课堂:模块三热点问题解答 - 图11张德
⽼师 Disruptor是不是⽐ConcurrentLinkedQueue性能更强呢???
2019-07-15 11:10
答疑课堂:模块三热点问题解答 - 图12咬你
⽼师,通过vmstat参数获取的参数,可否结合⼀些真实场景,分析下什么样的数据范围属于正常范围,出现什么样的参数,我 们就需要重点关注
2019-07-04 09:41
作者回复
⼀般系统出现性能瓶颈,可以结果上下⽂切换指标进⾏分析。在之前15讲中,我已经通过⼀个真实案例讲解了,可以参考下,有什么问题欢迎沟通。
2019-07-07 10:20

答疑课堂:模块三热点问题解答 - 图13nightmare
性能好是⼀⽅⾯,如果是抢购应⽤在需要⽤有界队列
2019-07-04 08:14
答疑课堂:模块三热点问题解答 - 图14Liam
我有2个问题想请教⽼师:

  1. 系统出现问题时我们⼀般会⾸先关注资源的使⽤情况,什么情况下可能是是上下⽂切换过多导致的呢?CPU消耗过⾼?

  2. ConcurrentLinkedQueue是⾮阻塞的,是否意味着它会消耗过多的CPu

2019-07-04 08:11
作者回复
CPU消耗过⾼会引起上下⽂切换的增加,但并不代表这个就不正常了。正常情况下上下⽂切换在⼏百到⼏千,⾼峰时段会上升
⾄⼏万,甚⾄⼏⼗万。

如果上下⽂⻓时间处于⾼位,这个时候我们就要注意了,这种情况有可能是某个线程⻓期占⽤CPU,例如之前我提到过的正则表达式出现的严重的回溯问题,就会在某⼀次回溯时,⼀直占⽤CPU,CPU的使⽤率⾼居不下,会导致上下⽂切换激增。

另外⼀种情况,就是之前你们的业务在⾼峰值出现的上下⽂切换在某个值,但是在业务迭代之后,⾼峰期的上下⽂切换的值异常⾼于之前的监控值。⽐如,我之前说的线程⼤⼩调整,导致了⾼峰期的上下⽂⾼出了⼗⼏倍之多。

ConcurrentLinkedQueue CAS操作会消耗CPU,但会及时释放,这不⾜以影响到系统的整体性能。

2019-07-05 09:51