前言
工作多年,经常遇到灵异事件,比如说: "任务为什么不跑了" , "没什么复杂业务呀,怎么cpu这么高" ,"用了缓存,怎么查询还是这么慢" , 一些工作一两年的同学遇到这类问题可能会手足无措 ,所以我想写一些文字来和大家分享下 。文笔有限,主要是交流。 这篇是先和大家交流 jstack的用法,以及编程方面的一些建议。
1 经典场景
场景一 :任务(线程)突然不跑了
程序是一段简单的quartz 任务 ,伪代码类似:
public class DataJobs {public void run() {log.info("任务开始运行");//准备数据Object prepareData = prepare();//调用http远程接口获取Result result = getHttpRemoteData(prepareData);//存储store(result);log.info("任务结束运行");}}
查看日志可用查看到 “任务开始运行” 但后续没有打印“任务结束运行”
业务也没有正常处理 , 也没有异常 无奈之下 只好重启服务
场景二 cpu load 特别高 业务也不复杂
如下图:

业务场景是比分直播。
页面的逻辑是每隔几秒从memached 获取实时比赛信息 ,根据彩种类别获取数据,然后前端渲染。
页面访问 伪代码类似:
("/getLiveData")public String getLiveData(String lotteryCode) {//从memcache中获取比分直播数据 仅仅是一个 get key from memcahedString result = getDataFromMemcache(lotteryCode);return result;}
异常问题:页面非常卡顿 ,服务器上 cpu 利用率 和 load 非常高 。
2 jstack分析
jstack是jdk自带的线程堆栈分析工具,使用该命令可以查看或导出 Java 应用程序中线程堆栈信息。

执行命令类似:
jstack -l pid >> statck.txt有时 也可以采用 kill -3 命令 实现打印堆栈的效果
kill -3 pid
堆栈格式:
#37 prio=5 os_prio=0 tid=0x00007f91ae738000 nid=0x3bf1 runnable [0x00007f91a5b4d000]: RUNNABLEat sun.nio.ch.EPollArrayWrapper.epollWait(Native Method):269):93):86)- locked <0x00000000ee106580> (a io.netty.channel.nio.SelectedSelectionKeySet)- locked <0x00000000ee1065b0> (a java.util.Collections$UnmodifiableSet)- locked <0x00000000edff8448> (a sun.nio.ch.EPollSelectorImpl):97):62):752):408):858):138):748)Locked ownable synchronizers:None
dump格式说明:
|
属性 |
说明 |
| redisson-netty-2-4 | 线程名称 |
|
prio=5 |
该线程jvm中优先级 |
| os_prio=0 |
该线程在os中的优先级 |
|
tid |
该jvm内的thread id |
| nid |
Native thread id 本地操作系统相关 (高度依赖操作系统平台) |
在jstack输出的第二行为线程的状态,在JVM中线程状态使用枚举 java.lang.Thread.State 来表示,State的定义如下:
public enum State {NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;}
在线程转换图:

jstack关键字分析
|
关键字 |
说明 |
|
deadlock |
死锁 |
|
waiting on condition |
等待某个资源或条件发生来唤醒自己。具体需要结合jstacktrace来分析,比如线程正在sleep,网络读写繁忙而等待 |
|
Blocked |
阻塞 |
|
waiting on monitor entry |
等待获取锁 |
|
in Object.wait() |
获取锁后又执行obj.wait() |
3 解决问题
3.1 线程阻塞问题
回到场景一 依靠上面的命令 我们打印出模拟的jstack堆栈
=5 os_prio=31 tid=0x00007feb6c008000 nid=0x2603 runnable [0x000070000771a000]: RUNNABLEat java.net.SocketInputStream.socketRead0(Native Method)at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)at java.net.SocketInputStream.read(SocketInputStream.java:170)at java.net.SocketInputStream.read(SocketInputStream.java:141)at java.io.BufferedInputStream.fill(BufferedInputStream.java:246)at java.io.BufferedInputStream.read1(BufferedInputStream.java:286)at java.io.BufferedInputStream.read(BufferedInputStream.java:345)locked <0x00000007959e1830> (a java.io.BufferedInputStream)at sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:704)at sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:647)at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1535)locked <0x00000007959b1848> (a sun.net.www.protocol.http.HttpURLConnection)at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1440)locked <0x00000007959b1848> (a sun.net.www.protocol.http.HttpURLConnection)at sun.net.www.protocol.http.HttpURLConnection.getHeaderField(HttpURLConnection.java:2942)at java.net.URLConnection.getContentType(URLConnection.java:512)at com.courage.platform.sms.client.util.SmsHttpClientUtils.getResponseAsString(SmsHttpClientUtils.java:345)at com.courage.platform.sms.client.util.SmsHttpClientUtils.doGet(SmsHttpClientUtils.java:241)
由上可见,当前线程阻塞在从http链接中获取响应数据。 可能对方httpserver耗时长,或者对方server hang住了 。针对这种情况,我适当的调整了 http client 的 connect 和read timeout。
3.2 cpu高问题
回到场景二,
我们先用ps命令找到对应进程的pid(如果你有好几个目标进程,可以先用top看一下哪个占用比较高)。
接着用
top -H -p pid来找到cpu使用率比较高的一些线程 (下图是模拟图)

然后将占用最高的pid转换为16进制printf '%x\n' pid得到nid

接着直接在jstack中找到相应的堆栈信息jstack pid | grep 'nid' -C5 –color

可以看到我们已经找到了nid为0x42的堆栈信息。
比分直播 我们查询到占用cpu最高的是 GC 线程。那我们怀疑是由于GC 频率过高导致的。
然后使用 jstat 每隔1s查看内存使用情况
jstat -gcutil pid 1000 发现新生代每隔两秒就基本占满了 , 然后查看每个key对应的数据大小,发现每个value大小是500k左右 。
解决方式也比较简单 :
减少缓存数据大小
适当调整新生代的大小
首次查询全量, 后续通过websocket推送增量给前端
4 编码习惯引导
4.1 设置线程名
线程名设置主要是为了在jstack堆栈中便于查询。在logback日志中最好也标注下线程名。当遇到问题也有一个查询源。
线程名有如下两种方式:
手工设置线程名
Thread t = new Thread(new Runnable() {public void run() {//something process}});t.setName("mytestThread");t.start();
线程工厂设置 (源码来自rocketmq 4.4)
public class ThreadFactoryImpl implements ThreadFactory {private final AtomicLong threadIndex = new AtomicLong(0);private final String threadNamePrefix;private final boolean daemon;public ThreadFactoryImpl(final String threadNamePrefix) {this(threadNamePrefix, false);}public ThreadFactoryImpl(final String threadNamePrefix, boolean daemon) {this.threadNamePrefix = threadNamePrefix;this.daemon = daemon;}public Thread newThread(Runnable r) {Thread thread = new Thread(r, threadNamePrefix + this.threadIndex.incrementAndGet());thread.setDaemon(daemon);return thread;}}
使用方式:
this.traceExecutor = new ThreadPoolExecutor(10,20,1000 * 60,TimeUnit.MILLISECONDS,this.appenderQueue,new ThreadFactoryImpl("MQTraceSendThread_"));traceProducer = getAndCreateTraceProducer(rpcHook);
4.2 线程池隔离
线程隔离主要有线程池隔离,在实际使用时我们会把请求分类,然后交给不同的线程池处理,当一种业务的请求处理发生问题时,不会将故障扩散到其他线程池,从而保证其他服务可用。

以开源代码rocketmq举例:
我们来看下broker启动时 ,发送消息 & pull 消息 都是不同的线程池处理请求。
分为
发送消息线程池
pull消息线程池
查询消息线程池
回复消息线程池
代码如下:
//发送消息 处理器this.remotingServer.registerProcessor(RequestCode.SEND_MESSAGE, sendProcessor, this.sendMessageExecutor);//pull消息 处理器this.remotingServer.registerProcessor(RequestCode.PULL_MESSAGE, this.pullMessageProcessor, this.pullMessageExecutor);this.pullMessageProcessor.registerConsumeMessageHook(consumeMessageHookList);//消费管理处理器ConsumerManageProcessor consumerManageProcessor = new ConsumerManageProcessor(this);this.remotingServer.registerProcessor(RequestCode.GET_CONSUMER_LIST_BY_GROUP, consumerManageProcessor, this.consumerManageExecutor);//事务处理器this.remotingServer.registerProcessor(RequestCode.END_TRANSACTION, new EndTransactionProcessor(this), this.endTransactionExecutor);this.fastRemotingServer.registerProcessor(RequestCode.END_TRANSACTION, new EndTransactionProcessor(this), this.endTransactionExecutor);
4.3 超时设置
为了避免线程池大规模阻塞, 合理的设置超时时间非常重要。 常见超时有如下
http 读写超时 (jdk httpurlconnection)
cononnection(new URL(n = getCurl), METHOD_POST, ctype);fillHeaders(conn, headers);conn.setConnectTimeout(connectTimeout);conn.setReadTimeout(readTimeout);out = conn.getOutputStream();
加锁时间(分布式场景下 redission)
RLock lock = redisson.getLock("myLock");// traditional lock methodlock.lock();// or acquire lock and automatically unlock it after 10 secondslock.lock(10, TimeUnit.SECONDS);// or wait for lock aquisition up to 100 seconds// and automatically unlock it after 10 secondsboolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);if (res) {try {...} finally {lock.unlock();}}
网络通讯 (通过countdownlatch实现)
public RemotingCommand invokeSyncImpl(final Channel channel, final RemotingCommand request,final long timeoutMillis)throws InterruptedException, RemotingSendRequestException, RemotingTimeoutException {final int opaque = request.getOpaque();try {final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis, null, null);this.responseTable.put(opaque, responseFuture);final SocketAddress addr = channel.remoteAddress();channel.writeAndFlush(request).addListener(new ChannelFutureListener() {public void operationComplete(ChannelFuture f) throws Exception {if (f.isSuccess()) {responseFuture.setSendRequestOK(true);return;} else {responseFuture.setSendRequestOK(false);}responseTable.remove(opaque);responseFuture.setCause(f.cause());responseFuture.putResponse(null);log.warn("send a request command to channel <" + addr + "> failed.");}});RemotingCommand responseCommand =responseFuture.waitResponse(timeoutMillis);if (null == responseCommand) {if (responseFuture.isSendRequestOK()) {throw new RemotingTimeoutException(RemotingHelper.parseSocketAddressAddr(addr), timeoutMillis, responseFuture.getCause());} else {throw new RemotingSendRequestException(RemotingHelper.parseSocketAddressAddr(addr), responseFuture.getCause());}}return responseCommand;} finally {this.responseTable.remove(opaque);}}
rocketmq封装了 responseFuture 对象超时设置通过countdown来实现超时效果
public RemotingCommand waitResponse(final long timeoutMillis)throws InterruptedException {this.countDownLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);return this.responseCommand;}
5 专业产品推荐
5.1. 阿里Arthas
Arthas 是Alibaba开源的Java诊断工具 。我们从下图看到 一个简单得命令就可以查看当进程阻塞的线程

5.2 PerfMa 笨马网络
笨马网络是一家致力于打造一站式IT系统稳定性保障解决方案,专注于性能评测与调优、故障根因定位与解决的公司。
我们来看看社区版本如何查看堆栈情况 。
首先上传dump文件
通过控制台即可从 线程池 栈 锁 多个层面展示当前jvm线程信息



6 总结
玩过金庸群侠传的朋友可能听过“野球拳” ,这在游戏初期是非常基础的招式,威力也弱 。修炼也很耗时 ,很多玩家都放弃了 , 没有想到的是,野球拳练到10级,玩家就像打通任督二脉, 基本一击必杀 。
我认为学习技术也是这样 。
坚持,never give up 不断磨练自己的基本功
勇于实践 ,当出现问题 ,不要害怕 ,这是一个千载难逢学习的机会
思考,向别人学习
第一次写公众号 , 希望对大家有帮助。
原文作者:勇哥Java实战;如有侵权,请联系管理员删除。