生产出现一个奇葩的问题,某一个应用开发新增了一个异步导出功能,然后提交任务一直是进行中,我让让去检查代码有没有问题,搞了一整天,还是没问题。然后就跟他一起排查这个问题,最先想到的还是业务代码写的有问题,排查了半天终于确定业务代码没问题,然后上生产环境看了下发现生产环境这个项目也出问题了,用的人比较少,所以没人发现。然后开始追朔没人动过这个代码怎么就突然不行了,于是就回退了5个版本终于正常了,然后对比差异,发现,docker容器的基础镜像被人替换了,然后又用jstack 跟踪了下线程
发现线程卡在如下代码
线程栈体现出来的是个PipedInputStream管道流,那么回溯代码发现这里确实是个管道流。并且这个管道流启动的两个线程用的是同一个线程池。然后看了下该线程池的线程,而管道流的必须要两个线程才能协同工作一个写一个读,如果,只有一个线程那么就会被阻塞,所以又去看了下线程池配置,发现取得是cpu核心数x2,看了下旧得配置也是一样得,为什么旧得可以,新得包不可以呢?然后看了下jdk得版本,发现一个是1.8.131,不行得那个是1.8.301,去翻了下oracle得官方文档,这里有个区别,是前者在docker中取cpu核心数不准确,取得是宿主机得,而后者取得数docker中得,而我们配置得docker cpu是1核,而宿主机是16核,所以前者可以,后者不可以了。
这个问题真的是坑。
写个简单得测试用例模拟生产得这个问题:代码如下
package com.qimo.omsa.demo.thread;
import com.qimo.omsa.demo.jsttool.SSH;
import com.qimo.omsa.demo.jsttool.ServiceDeploy;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
/**
* @Description TODO
* @Author xxx
* @Date 2021/9/1 14:37
*/
public class ThreadTest {
private static ThreadFactory springThreadFactory = new CustomizableThreadFactory("test-async-");
private static ExecutorService executor=new ThreadPoolExecutor(
1,
1,
0,
TimeUnit.SECONDS,
new ArrayBlockingQueue(100),springThreadFactory);
public static void main(String[] args) throws Exception {
PipedOutputStream pos=new PipedOutputStream();
PipedInputStream pis=new PipedInputStream(pos);
System.out.println("程序开始");
Future<String> task=executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
ByteArrayOutputStream os=new ByteArrayOutputStream();
byte[] bytes = new byte[8];
int len=0;
while((len=pis.read(bytes))>-1){
System.out.println(len);
os.write(bytes,0,len);
}
os.flush();
return "管道流接收数据完成";
}
});
executor.execute(()->{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
baos.write(new String("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx").getBytes());
baos.writeTo(pos);
baos.flush();
System.out.println("写数据到管道流");
} catch (IOException e) {
e.printStackTrace();
}
});
System.out.println("主线程阻塞等待返回");
System.out.println(task.get());
}
}
开始得时候新建一个只有一个线程得线程池然后启动项目发现线程卡再了while((len=pis.read(bytes))>-1)
这行代码上
输出日志只有
程序开始
主线程阻塞等待返回
然后把线程数改为2 继续执行
程序输出了日志如下
程序开始
主线程阻塞等待返回
写数据到管道流
8
8
8
8
8
8
原因就是上面得线程先启动了,由于没有数据,所以也不返回-1线程卡住,导致后面得线程被放到线程池得阻塞队列中,一直等待该线程结束,但是该线程又一直阻塞,变相死锁。
但是如果将上面两个线程对调一个位置又可以了,写数据得线程执行完了释放了线程,然后读数据得线程执行了,就可以从缓冲区读到数据,而这样写代码就是一次性写出。