除日志外,还有没有别的方式跟踪线上服务问题呢?或者,跟踪并排除日志里无法发现的问题?

方法当然是有的,就是通过现场快照定位并发现问题。我们所说的现场,主要指这两方面:

Java 线程栈。线程栈是Java线程工作的快照,可以获得当前线程在做什么;

Java 内存堆。堆是JVM的内存快照,可以获取内存分配相关信息。

今天,我们从 Java 线程角度,研究下基于线程栈如果排除问题。

1. Java 线程状态变换

在正式介绍线程栈之前,有必要先了解下 Java 线程的相关状态。

Java 线程就是在这几个状态间完成自己的整个生命周期。

状态

是够消耗 CPU

描述RUNNABLE

不确定

运行中 或者 就绪

WAITING

不消耗

1. object monitor 2. unpark

TIME_WAITING

不消耗

1. object monitor 2. unpark 3. sleep

BLOCKED

不消耗

object monitor

2. Java 线程栈

线程栈是问题的第一现场,从线程栈中可以获得很多日志以外的瞬时信息。

2.1 获取栈

jstack pid

kill -3 pid

强烈建议使用 jstack 命令!一者,方便重定向;二者,最大限度的避免 kill 这种高危命令的使用。

3.2 栈信息

核心信息:

线程名。务必给线程起一个优雅的名字;

Java 线程 id。全局唯一,16进制显示;

Native 线程 id。OS 线程id,16进制,与系统资源对应起来;

状态。线程所处状态,最关心 RUNNABLE 状态,实实在在消耗 CPU;

锁信息。获取锁、等待锁;

调用栈信息。方法调用链,类、方法、代码行号,问题排查关键;

3.3 线程栈视角

从线程中获取信息,有两个视角。

单次线程栈

总线程数量

是否发生死锁

线程所处状态

线程调用栈

多次线程栈

是否一直执行同一段代码

是否一直等待同一个锁

一般会导出多份线程栈,共 10 份,每个 2s 打一份。

3. 问题排查

线程栈不同于日志,是程序运行时的快照,可以定位很多诡异问题。

3.1 死锁

死锁是程序最为严重的问题,导致进程 hold 在那,无法处理正常请求。

死锁发生存在几个条件:

存在互斥条件;

保持并请求资源;

不能剥夺资源;

出现环路等待

3.1.1 Object 死锁

主要指使用 synchronized 关键字,通过对象锁保护资源,导致的死锁。

测试代码如下:

private final Object objectA = new Object();

private final Object objectB = new Object();

private String objectMethod1(){

synchronized (this.objectA){

sleepForMs(10);

synchronized (this.objectB){

return getCurrentTime();

}

}

}

private String objectMethod2(){

synchronized (this.objectB){

sleepForMs(10);

synchronized (this.objectA){

return getCurrentTime();

}

}

}

private ExecutorService deadlockExecutor = Executors.newFixedThreadPool(20, new BasicThreadFactory

.Builder()

.namingPattern("DeadLock-Thread-%d")

.build()

);

@RequestMapping("object")

public DeferredResult object(){

DeferredResult result = new DeferredResult<>(10 * 1000L, "TimeOut");

CompletableFuture r1 = CompletableFuture.supplyAsync(this::objectMethod1, this.deadlockExecutor);

CompletableFuture r2 = CompletableFuture.supplyAsync(this::objectMethod2, this.deadlockExecutor);

CompletableFuture.allOf(r1, r2).thenRun( ()-> result.setResult("SUCCESS"));

return result;

}

请求 /deadlock/object 返回超时,打印 Java 栈信息,发生死锁:

从栈信息中,我们可以获得以下信息:

发生死锁现象

发生死锁的相关线程

线程获取哪个锁,又再等待什么锁

3.1.2 Lock 死锁

主要指使用 Lock 对象进行资源保护,从而导致的死锁。

测试代码如下:

private final Lock lockA = new ReentrantLock();

private final Lock lockB = new ReentrantLock();

private String lockMethod1(){

try {

this.lockA.lock();

sleepForMs(10);

try {

this.lockB.lock();

return getCurrentTime();

}finally {

this.lockB.unlock();;

}

}finally {

this.lockA.unlock();

}

}

private String lockMethod2(){

try {

this.lockB.lock();

sleepForMs(10);

try {

this.lockA.lock();

return getCurrentTime();

}finally {

this.lockA.unlock();;

}

}finally {

this.lockB.unlock();

}

}

@RequestMapping("lock")

public DeferredResult lock(){

DeferredResult result = new DeferredResult<>(10 * 1000L, "TimeOut");

CompletableFuture r1 = CompletableFuture.supplyAsync(this::lockMethod1, this.deadlockExecutor);

CompletableFuture r2 = CompletableFuture.supplyAsync(this::lockMethod2, this.deadlockExecutor);

CompletableFuture.allOf(r1, r2).thenRun( ()-> result.setResult("SUCCESS"));

return result;

}

请求 /deadlock/lock 返回超时,打印 Java 栈信息,发生死锁:

和上个栈信息非常相似,发生了死锁现象。但,丢失了很重要的一个信息“线程获得哪个锁,又在申请哪个锁”,这可能就是 JVM 内置锁和 AQS 家族的区别。

3.2 线程数过高

线程数过高,主要由于线程池的不合理使用,比如没有设置最大线程数。

测试代码:

@RestController

@RequestMapping("many-thread")

public class ManyThreadController {

private ExecutorService executorService = Executors.newCachedThreadPool(new BasicThreadFactory

.Builder()

.namingPattern("Many-Thread-%d")

.build()

);

@RequestMapping("/{tasks}")

public DeferredResult manyThreads(@PathVariable int tasks){

DeferredResult result = new DeferredResult<>(10 * 1000L, "TimeOut");

CompletableFuture[] futures = new CompletableFuture[tasks];

for (int i=0;i

futures[i] = CompletableFuture.supplyAsync(this::getValue, executorService);

}

CompletableFuture.allOf(futures).thenRun( ()-> result.setResult("SUCCESS"));

return result;

}

private String getValue() {

sleepForMs(50);

return getCurrentTime();

}

}

请求 /many-thread/2000 ,查看栈信息:

存在 1729 个相似线程,如果在次加大 loop ,还可能会出现异常信息,有兴趣可以自行测试。

3.3 CPU 过高

一般是大集合处理或死循环导致。

测试代码如下:

@RestController

@RequestMapping("high-cpu")

public class HighCPUController {

private ExecutorService executorService = Executors.newFixedThreadPool(1, new BasicThreadFactory

.Builder()

.namingPattern("High-CPU-%d")

.build()

);

@RequestMapping("/{loop}")

public DeferredResult highCpu(@PathVariable long loop){

DeferredResult result = new DeferredResult<>(10 * 1000L, "TimeOut");

CompletableFuture.supplyAsync(()->{

for (int i=0;i

try {

Math.cos(i + 10);

}catch (Exception e){

}

}

return getCurrentTime();

}, executorService).thenAccept(r->result.setResult(r));

return result;

}

}

请求 /high-cpu/100000000000, CPU 会飙升。

3.3.1 多次线程栈对比

多次获取线程栈,特定线程长期停留在一个运行代码。

3.3.2 线程跟踪

先得到高 CPU 线程,在通过 nid 与线程栈线程对应,从而定位问题线程。

top -Hp pid。获取高 CPU 的线程号;

将线程号转换为 16 进制;

在线程栈中通过 nid 查找对应的线程;

3.4 资源不足

各种 pool 最常见问题。

Pool 工作原理:

测试代码如下:

@RestController

@RequestMapping("low-resource")

public class LowResourceController {

private ExecutorService executorService = Executors.newCachedThreadPool(new BasicThreadFactory

.Builder()

.namingPattern("Low-Resource-%d")

.build()

);

@Autowired

private StringRedisTemplate redisTemplate;

@RequestMapping("/{batch}")

public DeferredResult lowReource(@PathVariable int batch){

DeferredResult result = new DeferredResult<>(10 * 1000L, "TimeOut");

CompletableFuture[] futures = new CompletableFuture[batch];

for (int i=0;i

futures[i] = CompletableFuture.supplyAsync(this::getValue, executorService);

}

CompletableFuture.allOf(futures).thenRun( ()-> result.setResult("SUCCESS"));

return result;

}

private String getValue() {

try {

return redisTemplate.execute((RedisCallback)(redisConnection -> {

sleepForMs(5000);

return getCurrentTime() + redisConnection;

}));

}catch (Exception e){

e.printStackTrace();

}

return "ERROR";

}

}

请求 /low-resource/1000 超时后,查看堆栈信息:

可见,存在 998 个线程在等待 Jedis 资源。

3.5 锁级联

线程可以形成自己的依赖链条,增加问题排查的难度。

3.5.1 Future 级联

代码如下:

@RequestMapping("future")

public DeferredResult future(){

DeferredResult result = new DeferredResult<>(10 * 1000L, "TimeOut");

Future future = this.executorService.submit(()->{

sleepForMs(5000);

return getCurrentTime();

});

CompletableFuture[] futures = new CompletableFuture[CONCURRENT_COUNT];

for (int i=0;i

futures[i] = CompletableFuture.supplyAsync(()->{

try {

return future.get();

}catch (Exception e){

}

return "ERROR";

}, executorService);

}

CompletableFuture.allOf(futures).thenRun(()->result.setResult("SUCCESS"));

return result;

}

访问 /wait-chain/future 后,查看线程栈信息:

共有 100 个线程在 future.get 处进行等待。

3.5.2 Guave Cache 级联

Guava Cache 是最常用的 Local Cache,其内部做了并发处理,让多个线程请求同一个 Key,会发生什么事情呢?

测试代码如下:

private final LoadingCache cache;

public WaitChainController(){

cache = CacheBuilder.newBuilder()

.build(new CacheLoader() {

@Override

public String load(String s) throws Exception {

sleepForMs(5000);

return getCurrentTime();

}

});

}

@RequestMapping("guava-cache")

public DeferredResult guavaCache(){

DeferredResult result = new DeferredResult<>(10 * 1000L, "TimeOut");

CompletableFuture[] futures = new CompletableFuture[CONCURRENT_COUNT];

for (int i=0;i

futures[i] = CompletableFuture.supplyAsync(this::loadFromGuava, executorService);

}

CompletableFuture.allOf(futures).thenRun(()->result.setResult("SUCCESS"));

return result;

}

访问 /wait-chain/guava-cache 后,查看线程栈信息:

可见有 98 个线程在 Sync.get 处等待,整个现象和 Future 非常相似。

3.5.3 Logger 级联

日志是最常用的组件,也是最容易忽略的组件,如果多个线程同时访问日志的写操作,会产生什么精致的?

测试代码如下:

@RequestMapping("logger")

public DeferredResult logger(){

DeferredResult result = new DeferredResult<>(10 * 1000L, "TimeOut");

CompletableFuture[] futures = new CompletableFuture[CONCURRENT_COUNT];

for (int i=0;i

futures[i] = CompletableFuture.supplyAsync(this::writeLogger, executorService);

}

CompletableFuture.allOf(futures).thenRun(()->result.setResult("SUCCESS"));

return result;

}

private String writeLogger(){

for (int i = 0;i<10000;i++){

LOGGER.info("{}", i);

}

return getCurrentTime();

}

访问 /wait-chain/logger 后,查看线程栈信息.

写堆栈:

从日志中可见,Wait-Chain-Thread-52 线程正在执行文件写操作。

等待栈:

而有 98 个线程处于等待锁的状态。

4. 小结

Java 线程栈是线程运行时快照,可以帮助我们定位很多问题。掌握这一技能会让我们在日常工作中得心应手。

最后附上 项目源码

java线程卡住排查_基于 Java 线程栈 排查问题相关推荐

  1. java项目----教务管理系统_基于Java的教务管理系统

    java项目----教务管理系统_基于Java的教务管理系统 2022-04-22 18:18·java基础 最近为客户开发了一套学校用教务管理系统,主要实现学生.课程.老师.选课等相关的信息化管理功 ...

  2. java网上书店系统_基于JAVA/JSP的网上书店系统

    第一章 JAVA的网络功能与编程 1-1 JAVA语言简介 Java是一种简单易用.完全面向对象.具有平台无关性且安全可靠的主要面向Internet的开发工具.自从1995年正式问世以来,Java的快 ...

  3. java中变量 关键字_基于java的voliate关键字详解

    voliate关键字的作用: 一.内存可见性 基于缓存一致性协议,当用voliate关键字修饰的变量改动时,cpu会通知其他线程,缓存已被修改,需要更新缓存.这样每个线程都能获取到最新的变量值. 二. ...

  4. java农产品查询系统_基于java的农产品销售系统的设计与实现论文.docx

    基于java的农产品销售系统的设计与实现论文.docx 分类号_______________ 密级________________ UDC _______________ 学号_毕业设计(论文)论文题 ...

  5. java 计算移动平均线_基于Java语言开发的个性化股票分析技术:移动平均线(MA)...

    基于Java语言开发的个性化股票分析技术:移动平均线(MA) 基于 Java 语言开发的个性化股票分析技术:移动平均线(MA)移动平均线(MA)是以道·琼斯的"平均成本概念"为理论 ...

  6. 大学生java项目创意申请表_基于java ee的大学生作品展示系统.pdf

    基于java ee的大学生作品展示系统.pdf 还剩 4页未读, 继续阅读 下载文档到电脑,马上远离加班熬夜! 亲,喜欢就下载吧,价低环保! 内容要点: 第34卷第3期 2015年6月 南昌工程学院学 ...

  7. java 静态对象赋值_基于Java class对象说明、Java 静态变量声明和赋值说明(详解)...

    先看下JDK中的说明: java.lang.Object java.lang.Class Instances of the class Class represent classes and inte ...

  8. java课程设计拼图_基于Java拼图游戏的设计与实现(含录像)

    基于拼图游戏的设计与实现(含录像) 摘    要 本拼图游戏是基于J2SE平台开发的,它是一个Application,它的游戏规则和诺亚舟里的拼图游戏是一样的.这个游戏将一张大图切割成N张小图,然后在 ...

  9. 基于java的信访项目_基于JAVA的某省人大信访信息系统的设计与实现

    分 类 号 : TP 3 1 1 . 5 单 位 代码 : 1 01 8 3 研 究 生 学 号 : 2 0 1 35 3 H3 08 密 级 : 公 开 吉 林 大 学 硕 士 学 位 ...

最新文章

  1. linux系统 长久记录所有用户所有操作记录
  2. JSP和Servlet的六种中文乱码处理方法
  3. 输入流与输出流的区别
  4. Anaconda是什么?Anconda下载安装教程 - Python零基础入门教程
  5. 图之遍历--广度优先遍历
  6. PAT 1073 多选题常见计分法(20)(代码+思路)
  7. 影响 Chrome、Edge 等浏览器的 V8 引擎0day
  8. ORA-22859 无效的列修改
  9. iOS开发那些-如何打包iOS应用程序
  10. php中的date()函数d y m l等字母的表示方法
  11. 5、手把手教React Native实战之盒子模型BoxApp
  12. 康佳电视android,康佳电视怎么连接手机 康佳电视连接手机步骤【图文介绍】
  13. 24个希腊字母及其读音表
  14. 【STM32技巧】HX711称重芯片详细说明
  15. 易语言调用c 文本乱码,解决易语言编程乱码的问题
  16. 成长,进一寸有一寸的欢喜
  17. Skia引擎API整理介绍(skia in Android 2.3 trunk)
  18. 阿里DruidDataSource访问RDS安全级别受限问题排查
  19. java手动注册filter,SpringBoot注册Filter的两种实现方式
  20. 软件工程《构建之法》—概论

热门文章

  1. C语言填空题10道,3道C语言填空题,谁帮忙做下,谢谢
  2. tina中信号带宽_如何评测示波器带宽
  3. 二级计算机vf里的sql,计算机等级考试二级VF考点:SQL语言
  4. Win11系统点击回滚没有反应是怎么回事
  5. windows7怎么清空电脑只剩系统
  6. windows7系统电脑管理员权限怎么更改
  7. null、undefined、NaN区分解析和条件判定,以及在IF条件中的判定
  8. 轮询、长轮询与Web Socket的前端实现
  9. @RequestBody 和 @RequestParam可以同时使用
  10. pandas series取值_【小学生级】pandas入门到精通备查表——AI未来系列3