那些天,我用错的JAVA线程池用法

本文通过对比newFixedThreadPool与newCachedThreadPool的使用,分析了如何优化线程池以提高系统的吞吐量(TPS)。通过具体的代码示例和JMeter测试,展示了正确的线程池使用方式能够显著提升系统性能。
最近项目一个项目要结项了,但客户要求TPS能达到上千,而用我写的代码再怎么弄成只能达到30+的TPS,然后我又将代码中能缓存的都缓存了,能拆分的也都拆分了,拆分时用的线程池来实现的;其实现的代码主要为以前写的一篇博客中的实现方式来实现的。如下:

多线程之futureTask(future,callable)实例,jdbc数据多线程查询(https://blog.youkuaiyun.com/puhaiyang/article/details/78041046)

在其中用到了线程池,为了方便,线程池是采用如下代码new出来的

final ExecutorService executorService = Executors.newFixedThreadPool(10);  

通过自己仔细想想后,感觉这代码总有哪里写得不对,为此特意写了下DEMO代码来,并用jmeter自己跑一下自己测下:

 @RequestMapping(value = "doTest")
    public Object doTest(@RequestParam(defaultValue = "false") Boolean shutdown,
                         @RequestParam(defaultValue = "10") Integer threadCount,
                         @RequestParam(defaultValue = "100") Integer sleepTime,
                         @RequestParam(defaultValue = "10") Integer queryCount) {
        long beginTime = System.currentTimeMillis();
        final ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
        for (int i = 0; i < queryCount; i++) {
            int finalI = i;
            Callable<Integer> callable = new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    Thread.sleep(sleepTime);
                    logger.debug("index:{} threadInfo:{}", finalI, Thread.currentThread().toString());
                    return finalI;
                }
            };
            FutureTask futureTask = new FutureTask(callable);
            executorService.submit(futureTask);
        }
        if (shutdown) {
            executorService.shutdown();
        }

        Long endTime = System.currentTimeMillis();
        endTime = endTime - beginTime;
        logger.info("info:{}", endTime);
        return atomicInteger.addAndGet(endTime.intValue()) + "   this:" + endTime;
    } 

代码如上所示,然后我用jmeter对此进行了测试,测试1000个请求去访问,每个任务线程休眠时间设的为1秒,TPS为20多。

一想这确实挺低的,然后分析其原因,想着是不是springBoot的线程数给的太少了,于是乎又把tomcat的最大线程数进行了修改,由默认的200修改为了500,但发现没啥大的变化,想了想后,可能问题不是tomcat的配置导致的。

server:
  tomcat:
    max-threads: 500 

然后又通过Java VisualVM工具看了看线程信息,没发现啥问题。

然后出去静了静,听了一两首音乐后想着起来Executors还有一个newCachedThreadPool()的用法,它与newFixedThreadPool()的区别通过源码可以大概知道个一二:

newFixedThreadPool:

    /**
     * Creates a thread pool that reuses a fixed number of threads
     * operating off a shared unbounded queue.  At any point, at most
     * {@code nThreads} threads will be active processing tasks.
     * If additional tasks are submitted when all threads are active,
     * they will wait in the queue until a thread is available.
     * If any thread terminates due to a failure during execution
     * prior to shutdown, a new one will take its place if needed to
     * execute subsequent tasks.  The threads in the pool will exist
     * until it is explicitly {@link ExecutorService#shutdown shutdown}.
     *
     * @param nThreads the number of threads in the pool
     * @return the newly created thread pool
     * @throws IllegalArgumentException if {@code nThreads <= 0}
     */
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

newCachedThreadPool:

    /**
     * Creates a thread pool that creates new threads as needed, but
     * will reuse previously constructed threads when they are
     * available.  These pools will typically improve the performance
     * of programs that execute many short-lived asynchronous tasks.
     * Calls to {@code execute} will reuse previously constructed
     * threads if available. If no existing thread is available, a new
     * thread will be created and added to the pool. Threads that have
     * not been used for sixty seconds are terminated and removed from
     * the cache. Thus, a pool that remains idle for long enough will
     * not consume any resources. Note that pools with similar
     * properties but different details (for example, timeout parameters)
     * may be created using {@link ThreadPoolExecutor} constructors.
     *
     * @return the newly created thread pool
     */
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

newFixedThreadPool是创建一个大小固定的线程池,线程数固定,也不会被回收

newCachedThreadPool是创建一个大小为MAX_VALUE的线程数,并具有缓存功能,如果60秒内线程一直 处于空闲,则会进行回收

另外,线程池的shutdown方法doc文档的解释如下:

Initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted. Invocation has no additional effect if already shut down.
This method does not wait for previously submitted tasks to complete execution. Use awaitTermination to do that.

指的是等待线程池执行shutdown方法后就不再接收新的执行目标了,等当前线程池中的现场执行完毕后,此线程池就会关闭掉了。

通过查看JAVA源码中的注释信息后才得知原来我之前写的代码有了一个大大的BUG,不应该执行完一次后就立即把线程池给shutdown掉,这样的话,线程池的意义就没多大的意思了,跟new Thread的就差不多了。尴尬了!

然后乎将测试代码修改为如下的代码:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicInteger;

@RestController
@RequestMapping(value = "test")
public class TestController {
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    private AtomicInteger atomicInteger = new AtomicInteger(0);
    final ExecutorService executorService = Executors.newCachedThreadPool();

    @RequestMapping(value = "doTest")
    public Object doTest(@RequestParam(defaultValue = "false") Boolean shutdown,
                         @RequestParam(defaultValue = "10") Integer threadCount,
                         @RequestParam(defaultValue = "100") Integer sleepTime,
                         @RequestParam(defaultValue = "10") Integer queryCount) {
        long beginTime = System.currentTimeMillis();
//        final ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
        for (int i = 0; i < queryCount; i++) {
            int finalI = i;
            Callable<Integer> callable = new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    Thread.sleep(sleepTime);
                    logger.debug("index:{} threadInfo:{}", finalI, Thread.currentThread().toString());
                    return finalI;
                }
            };
            FutureTask futureTask = new FutureTask(callable);
            executorService.submit(futureTask);
        }
        if (shutdown) {
            executorService.shutdown();
        }

        Long endTime = System.currentTimeMillis();
        endTime = endTime - beginTime;
        logger.info("info:{}", endTime);
        return atomicInteger.addAndGet(endTime.intValue()) + "   this:" + endTime;
    }

}

调用时,shutdown传入false,并且线程池的new方法放到上面的公共方法区域中,而不应该是来一个请求就new一个线程池出来。然后将同样的请求用jmeter测试后,发现能达到300多了,比之前的20多提升了许多倍!

总结:

通过上面的测试,发现了一个我之前用错的JAVA线程池的用法,通过jmeter工具测试后才知道其性能是如何的大。

同时在通过修改springboot的配置信息后,发现springBoot能创建的线程池最大线程数也与其tomcat的最大线程数有关,具体身体关系还得靠后面的慢慢探索了。(贼尴尬,这么基础的代码问题居然给犯下了这么大一个错误.还好及时地修改了。哈哈)

<think>我们正在处理一个关于Java程序崩溃的问题:当使用自定义线程池提交调用DLL的任务时,一段时间后程序崩溃;而直接调用DLL任务则正常。我们需要分析可能的原因。 根据引用[1]提到的一个重要问题:循环调用同一个DLL文件时,必须释放上一次的资源,否则会占用端口(每次调用都会在线程里进行一次网络通讯)。这提示我们,在多次调用DLL的情况下,如果没有正确释放资源,可能会导致资源泄露(如端口占用),进而可能引起程序崩溃。 引用[3]提到,调用DLL时可能因为DLL的打包方式(32位debug/release模式)导致找不到DLL文件的问题,但这个问题通常在加载时就会发生,而不是运行一段时间后崩溃。所以这里可能不是主要原因,但也不能完全排除。 另外,我们需要考虑多线程环境下调用DLL可能带来的问题: 1. DLL本身是否是线程安全的?如果DLL内部使用全局变量或静态变量,那么多线程同时调用可能导致数据竞争,进而引发崩溃。 2. 资源释放问题:在每次调用DLL后,是否释放了DLL内部申请的资源?如果没有,随着调用次数的增加,资源耗尽(如内存、端口等)会导致崩溃。 结合用户描述:使用自定义线程池提交任务(即多线程调用)会出现崩溃,而直接调用(可能是单线程顺序调用)则正常,这指向了多线程环境下的问题。 可能的原因分析: 1. DLL非线程安全:当多个线程同时调用DLL中的函数时,如果函数内部使用了共享资源(全局变量、静态变量等)而没有同步机制,就会导致竞争条件,引发崩溃。 2. 资源泄露:在每次调用DLL后,没有正确释放DLL内部申请的资源(如内存、文件句柄、网络端口等)。在单次调用中不明显,但在线程池中多次调用后,资源耗尽导致崩溃。 3. 线程本地存储问题:如果DLL使用了线程本地存储(TLS),但在多线程环境下使用不当,也可能导致问题。 4. 线程池配置问题:线程池中的线程数量过多,导致系统资源不足(如线程栈空间不足),或者线程池未正确关闭导致资源未释放。 针对引用[1]中提到的“循环调用同一个DLL文件时,必须要释放掉上一次的资源”,这个资源释放的动作应该由谁来做?通常,DLL函数应该提供相应的清理函数,由调用者(即Java代码)在每次调用后调用。如果没有正确调用,就会导致资源泄露。 在Java中,我们通过JNI或JNA调用DLL,通常会在本地方法调用完成后,再调用一个清理方法(如果DLL提供了的话)。如果没有提供清理方法,那么DLL内部应该能够自动释放资源(例如在函数退出时自动释放栈内存),但如果是堆内存或者系统资源(如文件句柄、网络端口)则需要显式释放。 因此,建议的排查步骤: 1. 检查DLL的文档,确认其是否线程安全。如果不安全,则需要同步调用(例如使用synchronized关键字将调用DLL的代码块包裹起来,或者使用单线程线程池)。 2. 检查DLL是否提供了资源释放的接口,如果有,确保在每次调用后都调用该接口进行释放。 3. 检查线程池的使用:是否合理设置了线程池大小?是否在程序退出时正确关闭了线程池?可以考虑使用有界队列,避免任务过多导致内存溢出。 4. 监控资源使用情况:在程序运行过程中,监控内存、句柄数等资源的使用情况,观察是否随着运行时间增长而不断增加,如果是则说明存在资源泄露。 另外,引用[3]提到的DLL打包方式(32位debug/release)可能导致加载问题,但通常加载失败会在程序启动时发生。如果程序是运行一段时间后崩溃,则更可能是由于资源泄露或线程安全问题。 总结可能原因: - 多线程同时调用非线程安全的DLL函数(主要原因) - 每次调用后未释放DLL占用的资源(资源泄露) - 线程池本身配置不当导致资源耗尽(如线程数过多,任务队列过长) 建议的解决方案: 1. 同步调用:将调用DLL的方法加上同步锁,确保同一时间只有一个线程调用该DLL。但这样会降低并发性能。 2. 使用单线程线程池:这样所有调用DLL的任务都会在一个线程中顺序执行,避免并发问题。 3. 联系DLL提供方,确认线程安全性和资源释放要求,并获取更新版本(如果可能)。 4. 在每次调用DLL后,调用一个清理函数(如果DLL提供了的话)。 如果以上方法都不能解决问题,可能需要进一步分析崩溃时的日志或内存转储文件(dump文件),以确定崩溃的具体位置。 相关问题: 1. 如何判断一个DLL文件是否是线程安全的? 2. 在Java中使用JNI/JNA调用DLL时,如何确保资源被正确释放? 3. 如何监控Java程序运行时的资源泄露情况? 4. 如何配置线程池以避免资源耗尽? </think>### Java使用线程池调用DLL崩溃原因分析 当使用自定义线程池提交调用DLL的任务时出现崩溃,而直接调用正常,可能原因如下: #### 1. **资源未释放导致累积占用** [^1] - **根本原因**:DLL每次调用可能占用网络端口或线程资源,若未显式释放资源,线程池的复用特性会使资源占用随时间累积 - **对比场景**:直接调用时每次新建JVM进程会强制释放资源 - **典型表现**:报错`java.lang.OutOfMemoryError`或`端口占用异常` - **解决方案**: ```java // 在任务执行后强制释放资源 public class DllTask implements Runnable { @Override public void run() { try { Native.load("mydll", MyDllInterface.class).dllMethod(); } finally { Native.free(Pointer.nativeValue(/* 资源指针 */)); // 显式释放 } } } ``` #### 2. **线程安全冲突** [^2][^4] - **问题本质**:多数DLL设计为单线程调用,线程池并发访问可能导致: - 全局变量竞争 - 内存状态冲突 - 硬件资源争用(如GPU) - **崩溃特征**:随机性崩溃,日志含`ACCESS_VIOLATION`或`Segmentation Fault` - **验证方法**:将线程池设为单线程测试 ```java ExecutorService singleThreadPool = Executors.newSingleThreadExecutor(); ``` #### 3. **DLL调用环境不兼容** [^3] - **版本冲突**:Debug/Release版本混合使用导致内存管理异常 - **位宽错配**:32位DLL被64位JVM加载引发崩溃 - **检查清单**: ```shell dumpbin /headers mydll.dll | findstr "machine" # 检查DLL位宽 java -version # 检查JVM位宽 ``` #### 4. **线程栈溢出** [^1] - **特殊场景**:当DLL涉及递归或大内存操作时 - **线程池配置**:默认栈大小(1MB)可能不足 - **解决方案**:增大线程栈 ```java new ThreadPoolExecutor(..., new ThreadFactory() { public Thread newThread(Runnable r) { return new Thread(null, r, "dll-thread", 8 * 1024 * 1024); // 8MB栈 } }); ``` ### 调试建议 1. **崩溃日志分析**:检查`hs_err_pid.log`文件的`EXCEPTION_ACCESS_VIOLATION`段 2. **资源监控**:运行期间使用`jconsole`观察: - 非堆内存(Native Memory) - 线程数增长趋势 3. **最小化复现**:逐步增加线程池并发度,定位崩溃阈值 > **关键结论**:根本差异在于线程池的**资源复用机制**放大了DLL的资源管理缺陷,而直接调用通过进程隔离规避了这些问题[^1][^3]。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

水中加点糖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值