gitbook/Java性能调优实战/docs/105234.md

203 lines
11 KiB
Markdown
Raw Permalink Normal View History

2022-09-03 22:05:03 +08:00
# 20 | 答疑课堂:模块三热点问题解答
你好,我是刘超。
不知不觉“多线程性能优化“已经讲完了今天这讲我来解答下各位同学在这个模块集中提出的两大问题第一个是有关监测上下文切换异常的命令排查工具第二个是有关blockingQueue的内容。
也欢迎你积极留言给我,让我知晓你想了解的内容,或者说出你的困惑,我们共同探讨。下面我就直接切入今天的主题了。
## 使用系统命令查看上下文切换
在第15讲中我提到了上下文切换其中有用到一些工具进行监测由于篇幅关系就没有详细介绍今天我就补充总结几个常用的工具给你。
### 1\. Linux命令行工具之vmstat命令
vmstat是一款指定采样周期和次数的功能性监测工具我们可以使用它监控进程上下文切换的情况。
![](https://static001.geekbang.org/resource/image/13/71/13eeee053c553863b3bdd95c07cb3b71.jpg)
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运行虚拟机窃取的时间
### 2\. Linux命令行工具之pidstat命令
我们通过上述的vmstat命令只能观察到哪个进程的上下文切换出现了异常那如果是要查看哪个线程的上下文出现了异常呢
pidstat命令就可以帮助我们监测到具体线程的上下文切换。pidstat是Sysstat中一个组件也是一款功能强大的性能监测工具。我们可以通过命令 yum install sysstat 安装该监控组件。
通过pidstat -help命令我们可以查看到有以下几个常用参数可以监测线程的性能
![](https://static001.geekbang.org/resource/image/7a/d1/7a93cba1673119e4c9162a29e9875dd1.jpg)
常用参数:
* \-u默认参数显示各个进程的cpu使用情况
* \-r显示各个进程的内存使用情况
* \-d显示各个进程的I/O使用情况
* \-w显示每个进程的上下文切换情况
* \-p指定进程号
* \-t显示进程中线程的统计信息
首先通过pidstat -w -p pid 命令行,我们可以查看到进程的上下文切换:
![](https://static001.geekbang.org/resource/image/3e/4f/3e6cee25e85826aa5d4f8f480535234f.jpg)
* cswch/s每秒主动任务上下文切换数量
* nvcswch/s每秒被动任务上下文切换数量
之后通过pidstat -w -p pid -t 命令行,我们可以查看到具体线程的上下文切换:
![](https://static001.geekbang.org/resource/image/72/6f/728b1634e3e9971307264b5736cb1c6f.jpg)
### 3\. JDK工具之jstack命令
查看具体线程的上下文切换异常我们还可以使用jstack命令查看线程堆栈的运行情况。jstack是JDK自带的线程堆栈分析工具使用该命令可以查看或导出 Java 应用程序中的线程堆栈信息。
jstack最常用的功能就是使用 jstack pid 命令查看线程堆栈信息通常是结合pidstat -p pid -t一起查看具体线程的状态也经常用来排查一些死锁的异常。
![](https://static001.geekbang.org/resource/image/0e/1d/0e61a2f4eb945f5a26bd7987d0babd1d.jpg)
每个线程堆栈的信息中都可以查看到线程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分别对应以上队列详情如下
![](https://static001.geekbang.org/resource/image/59/da/59e1d01c8a60fe722aae01db86a913da.jpg)
### 2.非阻塞队列
我们常用的线程安全的非阻塞队列是ConcurrentLinkedQueue它是一种无界线程安全队列(FIFO)基于链表结构实现利用CAS乐观锁来保证线程安全。
下面我们通过源码来分析下该队列的构造、入列以及出列的具体实现。
**构造函数:**ConcurrentLinkedQueue由head 、tail节点组成每个节点Node由节点元素item和指向下一个节点的引用 (next) 组成,节点与节点之间通过 next 关联,从而组成一张链表结构的队列。在队列初始化时, head 节点存储的元素为空tail 节点等于 head 节点。
```
public ConcurrentLinkedQueue() {
head = tail = new Node<E>(null);
}
private static class Node<E> {
volatile E item;
volatile Node<E> next;
.
.
}
```
**入列:**当一个线程入列一个数据时会将该数据封装成一个Node节点并先获取到队列的队尾节点当确定此时队尾节点的next值为null之后再通过CAS将新队尾节点的next值设为新节点。此时p != t也就是设置next值成功然后再通过CAS将队尾节点设置为当前节点即可。
```
public boolean offer(E e) {
checkNotNull(e);
//创建入队节点
final Node<E> newNode = new Node<E>(e);
//tp为尾节点默认相等采用失败即重试的方式直到入队成功
for (Node<E> t = tail, p = t;;) {
//获取队尾节点的下一个节点
Node<E> 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节点设置为nullCAS就会成功地直接返回节点元素否则还是更新head节点。
```
public E poll() {
// 设置起始点
restartFromHead:
for (;;) {
//p获取head节点
for (Node<E> 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乐观锁实现的在并发时的性能要好于其它阻塞队列因此很适合作为高并发场景下的排队队列。
今天的答疑就到这里,如果你还有其它问题,请在留言区中提出,我会一一解答。最后欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他加入讨论。