JAVA技术栈,常见生产问题汇总

alt

IT行业中目前java技术栈仍然占据着主导的地位,在生产环境抢修中,还有一些非常常见的生产问题,依然是JVM相关的问题占比非常高,今年我们就来整理探讨一下这方面的问题

常见的JVM问题类型

在参与多年的生产抢修过程中,下面列举的故障都是十分严重的,每一个故障都会导致生产系统不可用,给企业造成不可估量的损失,常见的故障问题主要是如下几种:

  • OOM内存溢出
  • CPU资源开销非常高,超过90%
  • 线程死锁
  • 线程等待

CPU资源开销高问题剖析

  • OOM内存溢出在上篇文章中已经详细分享过,可以查看《JAVA内存溢出问题深入剖析》

  • CPU资源开销高问题 这个问题很好理解,就是操作系统的CPU时间片资源都被java进程占用了,下面是一个代码示例,通过创建多个线程来抢占CPU时间片,来达到消耗光cpu资源的目的。(这里注意单个执行任务的线程无法做到),我们来看下相关的示例代码:


public class Highcpu {

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> {
                System.out.println(Thread.currentThread().getName());
                try {
                    // Thread.sleep(30 * 60 * 1000);
                    int sum = 0;
                    while (true) {
                        // sum++;
                    }

                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
            thread.setName("thread-" + i);
            thread.start();
        }

    }
}

该代码运行后,cpu资源性能立马飙到了100%,只要线程一直运行,占用资源就一直不会被释放

alt

现在我们来看一下,如何从运行中的线程信息中定位到问题代码

主要是如下几步:

  1. 确认是java进程占用cpu高
  2. 找到java进程下cpu占用高的线程,把十进制线程号转换成十六进制
  3. 使用jstack工具打印出java线程堆栈信息,定位问题代码
  • 下面是实操环节
  1. 确认是java进程占用cpu高(我的环境是windows,linux系统使用的工具会有区别)
alt
  1. 找到java进程下cpu占用高的线程,把十进制线程号转换成十六进制

这里我使用的是process explorer这个工具

alt

这么多线程中找前两个线程进行分析,分别是20312和5900这两个线程,通过计算机转换成16进制数为:4F58和170C

alt
  1. 使用jstack工具打印出java线程堆栈信息,定位问题代码 jstack 14432 > d:\heapdump\2023101601.log
alt

找到问题代码段,搞定!

线程死锁问题剖析

线程死锁问题简单解释如下:两个线程A,B A一直持有资源1,B一直持有资源2。这时A想要再持有资源1,B想要再持有资源2,一直获取不到,导致死锁。下面我们就用程序演示还原一下这个场景


public class DeadLockDemo {

    public static void main(String[] args) {

        Resource resource1 = new Resource("资源1");
        Resource resource2 = new Resource("资源2");

        doSomething(resource1, resource2);
        doSomething(resource2, resource1);
    }

    private static void doSomething(Resource resource1, Resource resource2) {
        new Thread(() -> {
            // 获取资源1的锁
            System.out.println(Thread.currentThread().getName() + "  请求" + resource1 + "的锁");
            synchronized (resource1) {
                System.out.println(Thread.currentThread().getName() + "  获取到" + resource1 + "的锁");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 获取资源2的锁
                System.out.println(Thread.currentThread().getName() + "  请求" + resource2 + "的锁");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread().getName() + "  获取到" + resource2 + "的锁");
                }
            }
        }).start();
    }
}

class Resource {

    private String name;

    public Resource(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return this.name;
    }
}

如上代码运行后,成功的实现了死锁的场景。两个线程都再等待对方资源导致无法继续执行后续任务。

alt

首先使用jps找到对应进程pid

alt

再使用jstack 17908,找到了两个死锁的线程,成功定位到了问题代码段 alt

alt

线程等待问题剖析

线程等待问题是当某一个线程一直持有锁不释放,导致其他线程无法获得该锁,一直处于等待状态。我们来看一下导致该问题的代码:

import java.util.LinkedList;
import java.util.Queue;

public class ObjectMethodTest {

    Queue<String> queue = new LinkedList<String>();

    int MAX_SIZE = 1// 假设队列长度只有1 , 只能存放一条数据

    public void produce() {
        synchronized (queue) {

            // 队列满则等待队列空间
            while (queue.size() == MAX_SIZE) {
                // 挂起当前线程,并释放通过同步块获取的queue上的锁,让消费者线程可以获取该锁,然后获取队列里面的元素。
                try {
                    queue.wait();
                    System.out.println("-----等待消费----");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            // 生产元素, 并通知唤醒消费者
            queue.add("hahaha");
            
        }
    }

    public void consume() {
        synchronized (queue) {

            while (queue.size() == 0) {
                // 挂起当前线程,并释放通过同步块获取的queue上的锁,让生产者线程可以获取该锁,然后获取队列里面的元素。
                try {
                    queue.wait();
                    System.out.println("-----等待生产-----");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            // 消费元素, 并通知唤醒生产者
            System.out.println("消费成功:" + queue.poll());
            queue.notifyAll();
        }
    }

    public static void main(String[] args) {

        ObjectMethodTest objectMethodTest = new ObjectMethodTest();

        // 10 个生产线程
        for (int i = 0; i < 10; i++) {
            new Thread(objectMethodTest::produce).start();
        }

        // 10 个消费线程
        for (int i = 0; i < 10; i++) {
            new Thread(objectMethodTest::consume).start();
        }
    }
}

代码实现逻辑说明:

定义了一个队列(Queue queue)和队列最大长度(int MAX_SIZE = 1)。

produce 方法用于生产元素。它通过synchronized (queue)来获取队列的锁,确保生产和消费操作的互斥执行。如果队列已满,生产者会进入等待状态,并一直持有该队列锁。

consume 方法用于消费元素。同样,它也使用synchronized (queue)来获取队列的锁。如果队列为空,消费者会进入等待状态,释放队列的锁,以便生产者可以获取锁并生产元素。

在 main 方法中,创建了一个 ObjectMethodTest 对象,并启动了10个生产者线程和10个消费者线程。

该场景会导致当持有该队列锁的线程,队列大小达到最大后。该线程会进入挂起状态,并一直持有该队列锁。导致其他线程无法获取,一直等待。

我们来定位一下问题代码段,主要步骤和排查死锁问题一样

  1. 通过jps找到进程pid alt

  2. 使用jstack pid 打印出线程堆栈

我们可以看到消费者线程都在等待 - waiting on <0x000000076c0bf8b8>这个锁资源,并且我们知道这个锁是一个java.util.LinkedList类型的资源锁。

并且定位到了问题代码段:at com.example.demo.controller.ObjectMethodTest.consume(ObjectMethodTest.java:38) alt

同时生产者线程也同样在等待这个锁资源 - waiting on <0x000000076c0bf8b8> (a java.util.LinkedList) 同时也指出了问题代码段

alt

总结

知其然,知其所以然。我们通过代码示例还原了如下几个常见的生产问题场景,并实操了如何进行问题排查,定位到问题代码

  • CPU资源开销非常高,超过90%
  • 线程死锁
  • 线程等待

希望上面的案例能够帮助到你,我会带来更多更干的干货分享,欢迎关注我!

本文由 mdnice 多平台发布

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值