分类 Troubleshooting 相关 下的文章

判断Java 的 Future 为啥还没结果

在诊断Java应用的时候, 我们经常看到很多异步的调用, 当前线程等待其它线程返回结果. 有时候会发现这些线程迟迟没有往下走, 于是查看线程状态, 会看到如下的线程栈:

sun.misc.Unsafe.park(Native Method) 
java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) 
java.util.concurrent.FutureTask.awaitDone(FutureTask.java:429) 
java.util.concurrent.FutureTask.get(FutureTask.java:191) 
com.tianxiaohui.RboCache.compute(RboCache.java:46)

我们会很自然的发问, 为什么它停在这, 到底发生了什么?

初步分析

对于这种情况, 有2种情况:

  1. 这个任务还没有被执行, 所以没有结果;
  2. 这个任务在执行中, 所以还没有结果;

对于第一种情况, 要去查看线程为什么没启动, 或者线程池为啥没执行它. 通常这时候要查看线程池的core size, max size, 和workQueue 和 workers 的情况.
对于第二种情况, 则要查看执行的线程在干什么事情, 为什么还没出结果.

java.util.concurrent.Future

对于java.util.concurrent.Future 我们很容易的能从它的字段: runner 看出谁在执行, 还是没有在执行, 如下图:
future1.png
future2.png

java.util.concurrent.CompletableFuture

对于 java.util.concurrent.CompletableFuture 则稍微麻烦一些, 要看谁指向它:

  1. 如果指向它的对象除了当前线程还有另外一个线程, 那么另外一个线程就是执行线程;
  2. 如果指向它的对象除了当前线程还有一个等待队列中的某个对象, 则它还没开始执行;
    cf1.png

cf2.png

上面用到的代码:

import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Supplier;

public class WhyFutureStuck {
    
    public static void main(String[] args) {
        ExecutorService es = Executors.newFixedThreadPool(2);
        
        Future<String> f1 = es.submit((Callable<String>) new Callable<String>() {
            @Override
            public String call() throws Exception {
                Thread.sleep(3600 * 1000);
                return "result-f1";
            }
        });
        
        CompletableFuture<String> cf1 = CompletableFuture.supplyAsync((Supplier<String>) () -> {
            try {
                Thread.sleep(3600 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "result-cf1";
        }, es);
        
        //threads are used up
        Future<String> f2 = es.submit((Callable<String>) new Callable<String>() {
            @Override
            public String call() throws Exception {
                Thread.sleep(3600 * 1000);
                return "result-f2";
            }
        });
        
        CompletableFuture<String> cf2 = CompletableFuture.supplyAsync((Supplier<String>) () -> {
            try {
                Thread.sleep(3600 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "result-cf2";
        }, es);
        
        try {
            System.out.println(f1.get());
            System.out.println(f2.get());
            System.out.println(cf1.get());
            System.out.println(cf2.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

Java 9 之后 解决多个模块包含相同包名的问题

最近接手一个新的Java项目, 架构师明确要Java 11 版本, 于是在 eclipse 里面设置使用JDK 17, JDK 编译器兼容 level 设置成 11. 设置完成后, 然后重新编译, 竟然报错了, 还是几百个错误!

错误内容全部是: The package java.util is accessible from more than one module: <unnamed>, java.base.

如下图, 这些从java.util引用的类, 全部都报这个错:
java.util.package.png

这个错误代表什么意思

Java 9 开始有了模块化系统的概念(JPMS), 每个Java 文件都属于某个模块. 例如上面例子中出现的 java.util.HashMap, java.util.concurrent.TimeUnit 都属于JDK里面的 java.base 模块.

那么<unnamed> 模块是什么呢?

对于JDK本身, 它默认已经分了模块, 但是我们还有很多jar包, 并没有按照模块的方式去打包, 或者在JDK 9 之前就有了这些jar包, 我们照样可以依赖它们.

对各种模块的依赖, 会加到modulepath, 对于非模块的依赖, 继续使用classpath. classpath上的类会被统一的分配到一个叫<unnamed>的模块.

如下面的 eclipse 截图所示, 所有maven的依赖全部放到了classpath.

moduepath.png

错误含义

所以, 这个错误的真正含义是: 在我们依赖中, java.util包即出现在了 JDK 的 java.base 这个模块, 也出现在了我们依赖的classpath上. 而Java模块化系统不允许同一个包出现在2个模块里面.

如何修复

有2种可能的方案:

  1. 退回到Java 8, 不使用模块化的概念, 错误消失.
  2. 找到提供java.util包的jar包, 质问开发者为啥给自己的jar包加入jdk的包名, 是想夹带私货吗?

    我们这里只能选择第二个选项, 所以如何找到这个包含java.util的jar包成了重点.

如何找到第二个包含java.util的jar包

看到最初错误截图里面那些类, 我们很直观的可能认为: 只要使用eclipse的 open type 菜单是不是就很容易找到了, 我们试一下:

hashmap.png

从上面截图可以看到, eclipse 只找到一个HashMap, 并且源自于JDK11, 同样, 对于java.uti.Map, java.util.concurrent.TimeUnit 我们也只能找到仅有的一个属于JDK的.

明明报了java.util包来自2个模块, 为啥我们只找到一个呢?

其实, 我们上面的方式去找HashMap这个类出现在哪个包, 但是这个错误的真正的重点是包的名字, 也就是说, 有另外一个jar包里包含了java.util.Xxxx, 它不管类名是什么, 只管包名和JDK里面的java.util包重复了.

我们可以同样的方式在 eclipse 的 open type 里面输入java.util.*, 只不过会出现非常长的列表, 然后人工一个个对比.

如何手工找到这个jar包

这里提供2个方法:

  1. 方法一: 使用 eclipse open type, 然后输入 java.util.*,如下图, 按顺序找一定能找到:
    conc.png
  2. 方法二: 在 maven 的pom.xml 里面去掉一部分依赖, 看是不是这个错误消失了(同时其它依赖缺失错误会出现), 如果这个消失了, 说明去掉的这个依赖里面(直接或间接)包含这个包名. 使用一次去掉多个依赖(二分查找)可以加快速度.

如何自动化找到这个jar

eclipse 能报出这个错误, 说明它在我们的依赖中, 除了JDK之外还有一个jar包含这个包. 那么, 我们可以用同样的方法, 找到这个jar.

步骤:

  1. 通过 mvn 命令找到所有的依赖jar.
  2. 查找 jar 里面的类全名是不是包含 java.util.

mvn 命令找到所有的依赖jar

首先, 我们进入的项目pom.xml 所在的目录, 然后执行mvn 的goal: dependency:build-classpath, 把输出结果全部放到/tmp/dep.txt文件.

$ cd <project_dir>
$ ~/work/tools/apache-maven-3.9.1/bin/mvn dependency:build-classpath -Dmdep.outputFile=/tmp/dep.txt

打开/tmp/dep.txt文件, 可以看到里面是以:(MAC上)分割开的jar文件名.

遍历查找每个jar的类

第二步, 读取上面的输出, 以:分隔成多个jar, 然后遍历每个jar, 找出包含java.util的类,

    jars=(${(s/:/)$(cat /tmp/dep.txt)})
    for i in "${jars[@]}" 
    do 
        jar -tf $i | grep '^java.util' && echo "\t\tfind in ${i}"
    done

执行上面的脚本(mac 上的zsh), 得到如下结果:

java/util/
java/util/concurrent/
java/util/Hashtable.class
java/util/concurrent/ConcurrentHashMap.class
        find in /Users/eric/.m2/repository/com/boundary/high-scale-lib/1.0.6/high-scale-lib-1.0.6.jar

最终, 通过几行shell命令, 我们也找到了包含这个包名的jar包.

总结

Java 9 之后, 开始分模块, 一个项目可能既有对模块的依赖, 还有对普通jar包的依赖, 这些以前的jar包会被放到一个<unnamed>的模块. 如果一个包名出现在多个模块中(包含<unnamed>的模块), 就会出现这个错.

本文中另外一个出现在<unnamed>的模块, 可以通过遍历查找依赖jar的方式去查找到这个jar包.

JDK 里面有哪些模块?

java --list-modules

SSLv2Hello 协议引发的连接拒绝

有开发人员问我, 有没有见过这个 Java 应用报出的异常消息: IO Error: Remote host terminated the handshake. 从这个message看, 这个是说无法通过握手建立连接, 单纯从这个消息看, 看不出来什么原因.

背景

这个应用最近更改了不少文件, 然后重新发布新版本, 就出现了这个错误. 出这个错误的上下文是: 这是一个连接 Oracle 数据的连接, 使用的是TSLv1.2加密连接.

他们问了DBA, DBA 说数据库完好, 问了网络, 网络也没有其它问题.

诊断

最简单的方法是抓包, 查看具体的握手失败的原因. 抓包后看到如下的 SSL client hello 协议内容, 然后服务器就拒绝了:
client_hello.png

从上面的截图我们可以看到, tcp 三次握手, 正常. 然后客户端发送 client hello, 服务器直接就发了 FIN, 要拒绝连接.

在看 client hello 的具体消息, 从消息可以看到, 这里上面写的是 SSLv2, 难道这是1995年使用的 SSL 2.0? 看上去很奇怪.

如果比较熟悉 TLS 协议, 我们可以知道, 在TLS 层, 又分为 TLS record 协议, 它内部又封装了具体的 TLS 子协议, 分别是: Application, Alert, Handshake, ChangeCipherSpec. 而 record 协议的第一个字节表示子协议的类型.

然而我们这个截图里面, 前两个字节是 80 b3, 表示内容长度, 关键是 Wireshark 认识这种格式. 太诡异了.

然后仔细查看 Wireshark 的解析, 我们看到 Wireshark 标注的是: SSLv2 Record Layer: Client Hello.

关于 SSLv2Hello

顺着这个关键字, 我们终于找到了另外一种协议: SSLv2Hello. 这是一种伪协议, 它是 Oracle 之前发明的, 并且现在已经不在使用的. 它的主要作用是在客户端发送一个SSLv2Hello消息, 试探一下服务端使用的到底是哪种协议, 是 TLS 1.1, 还是 TLS 1.2.

JDK 里面在 JDK 7 之前默认是有这种协议的, 之后 Oracle disable 了它.

-- 未完待续 --

诊断docker container 里面的 Java 进程内存泄漏

在公司有台台式机, 平时办公不用它, CPU 性能还可以, 有16G 内存, 为了充分利用它, 在它上面装了docker, 然后通过container的方式安装了不少工具, 比如: MySQL+phpmyadmin, ElasticSearch+kibana, MongoDB+mongoUI, Splunk, Clickhouse, Prometheus, Jupter Notebook, Neo4J, Janusgraph, Nginx 等应用, 平时做个测试或者小工具, 直接连这些应用, 非常方便. 直到有一天, 给它接了一个外接显示器, 第二天早上看到下面这个系统日志, 发现这个上面有个应用一直OOM.

- 阅读剩余部分 -