与JDBC一起使用时,Java Executor服务不会关闭线程

时间:2019-03-21 17:16:51

标签: java multithreading jdbc connection-pool

我将ExecutorService与FixedThreadPool结合使用,以通过JDBC执行一些SQL。但是,当我分析我的应用程序时,线程数似乎只是在增加,内存当然也在增加。问题在于,它与JDBC有某种关系,因为当我删除线程池任务中的创建语句和连接时,线程计数根本没有增加。

这是我将任务汇总到线程池中的方式:

       new Thread() {
            public void run() {
                ExecutorService executorService = Executors.newFixedThreadPool(5);
                    while (!isCancelled) {
                        executorService.submit(RunnableTask.this);
                        Thread.sleep(interval);
                    }
                    executorService.shutdown(); //wait for all tasks to finish and then shutdown executor
                    executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); //wait till shutdown finished
                } catch (InterruptedException ex) {
                    //
                }
            }
        };

这是我在任务中要做的:

    try (Connection con = pool.getConnection(); PreparedStatement st = (this.isCall ? con.prepareCall(this.sql) : con.prepareStatement(this.sql))) {
        st.execute();
    } catch (Exception e) {
        //
    }

这是上述代码(pool.getConnection()中使用的ConnectionPool,我使用的是Apache DBCP2:

import java.sql.Connection;
import java.sql.SQLException;
import org.apache.commons.dbcp2.BasicDataSource;

public class MySQLPool {

private BasicDataSource dataSource;

public MySQLPool setup(String driver, String uri, String user, String password, int maxConnections) throws Exception {
    if (this.dataSource == null) {
        BasicDataSource ds = new BasicDataSource();
        ds.setDriverClassName(Main.driver);
        ds.setUrl(uri);
        ds.setUsername(user);
        ds.setPassword(password);
        ds.setMaxTotal(maxConnections);
        ds.setMaxWaitMillis(2000);
        this.dataSource = ds;
    }
    return this;
}

Here is a example from profiler (imgur)

似乎线程未正确结束,这很奇怪,因为如果ExecutorService是5个连接的固定池,它应该用光它们,对吗?因此,我不知道这些线程仍然如何存在,它们会导致相当大的内存泄漏。

问题在于创建Connection和PreparedStatement对象,因为当我注释掉它时,线程数保持固定值。

1 个答案:

答案 0 :(得分:1)

您不会显示所有代码,例如isCancelled。因此,我们无法提供具体帮助。但是您的方法似乎不合常规,所以请继续阅读。

ScheduledExecutorService

您不应尝试管理执行程序服务的时间。如果结合执行程序服务调用Thread.sleep,则可能做错了什么。

您也不应该调用new Thread。执行程序服务的全部重点是让框架管理线程的细节。你太辛苦了

要重复调用任务,请使用ScheduledExecutorService

有关更多信息,请参见Oracle Tutorialclass JavaDoc,然后搜索堆栈溢出。这个话题已经被解决了很多次。

示例应用

这是一个简短的例子。

使用Executors实用工具类创建您的线程池。我们只需要一个线程即可重复调用数据库。查看您的部分代码示例,我看不到您为什么尝试运行5个线程。如果要对数据库进行一系列顺序调用,则只需一个线程。

让您的Runnable调用数据库。

package work.basil.example;

import java.sql.Connection;
import java.time.Instant;

public class DatabaseCaller implements Runnable
{
    private Connection connection = null;

    public DatabaseCaller ( Connection connection )
    {
        this.connection = connection;
    }

    @Override
    public void run ()
    {
        // Query the database. Report results, etc.
        System.out.println( "Querying the database now. " + Instant.now() );
    }
}

注意:始终将run方法的代码与try catch一起包装,以捕获任何意外的ExceptionError({{1} }。到达执行者的任何未抛出的抛出将导致其停止工作。该任务将不再计划进一步运行。

实例化该Throwable,并安排它重复运行。

Runnable

注意这有多简单。

  • 我们在实例化的package work.basil.example; import java.sql.Connection; import java.time.Instant; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; public class DbRepeat { public static void main ( String[] args ) { DbRepeat app = new DbRepeat(); app.doIt(); } private void doIt () { System.out.println( "Starting app. " + Instant.now() ); Connection conn = null; // FIXME: Instantiate a `Connection` object here. Runnable runnable = new DatabaseCaller( conn ); ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor(); long initialDelay = 0; long delay = 5; ScheduledFuture future = ses.scheduleWithFixedDelay( runnable , initialDelay , delay , TimeUnit.SECONDS ); // Let our demo run a few minutes. try { Thread.sleep( TimeUnit.MINUTES.toMillis( 2 ) ); // Let this app run a few minutes as a demo. } catch ( InterruptedException e ) { System.out.println( "Somebody woke up our sleeping thread. Message # 1b296f04-3721-48de-82a8-d03b986a4b55." ); } // Always shutdown your scheduled executor service. Otherwise its backing thread pool could continue to run, outliving the lifecycle of your app. ses.shutdown(); System.out.println( "Ending app. " + Instant.now() ); } } 对象中定义了一个任务。
  • 我们建立了由单个线程支持的执行程序服务。
  • 我们告诉该服务代表我们重复运行任务,并指定了执行该任务的频率。
  • 最终,我们告诉该服务停止安排该任务的进一步执行,并关闭其线程池。

我们从来没有直接处理线程。我们让执行者框架处理线程的所有细节。

警告:如果使用多个线程,您仍然需要使您的Runnable代码成为线程安全。执行程序框架非常流畅并且很有帮助,但这并不是魔术。要了解Java中的线程安全性和并发性,请每年阅读这本出色的书:Brian Goetz等人的Java Concurrency in Practice

运行时。

  

启动应用。 2019-03-21T19:46:09.531740Z

     

现在查询数据库。 2019-03-21T19:46:09.579573Z

     

现在查询数据库。 2019-03-21T19:46:14.585629Z

     

...

     

现在查询数据库。 2019-03-21T19:47:59.647485Z

     

现在查询数据库。 2019-03-21T19:48:04.650555Z

     

结束应用。 2019-03-21T19:48:09.579407Z

跳过连接池

根据我的经验,许多人夸大了对数据库连接池的需求。使用连接池有一些陷阱。而且我发现建立数据库连接并不像许多人声称的那样昂贵,尤其是在同一台计算机上本地连接的情况下。

因此,我建议您暂时跳过连接池。使用全新的连接时,使您的代码可靠地工作。

如果以后由于数据库连接而导致性能瓶颈,请考虑使用一个池。并验证它确实有帮助。否则,您将犯过早优化的罪过。