【问题标题】:Java Executor service is not closing threads when used with JDBC与 JDBC 一起使用时,Java Executor 服务未关闭线程
【发布时间】:2019-03-21 17:16:50
【问题描述】:

我使用 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) {
        //
    }

这是上面提到的代码中使用的 ConnectionPool(pool.getConnection(),我使用的是 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 对象,因为当我将其注释掉时,线程数保持在固定值。

【问题讨论】:

  • 什么?您正在创建new Thread() 在其中run 方法您正在创建ExecutorService 您提交只是创建Thread 实例?哦,伙计...当您将此类实例提交到 ExecutorService 时,最终将调用其 run 方法。这将创建另一个 ExecutorService 再次提交...

标签: java multithreading jdbc connection-pool


【解决方案1】:

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

ScheduledExecutorService

您不应该尝试管理 executor 服务的时间安排。如果您将Thread.sleep 与执行器服务一起调用,则您可能做错了什么。

您也不应该调用new Thread。执行器服务的全部意义在于让框架管理线程的细节。你太辛苦了。

对于任务的重复调用,请使用ScheduledExecutorService

有关详细信息,请参阅Oracle Tutorialclass JavaDoc,并搜索 Stack Overflow。这个话题已经讨论过很多次了。

示例应用

这是一个简短的例子。

使用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() );
    }
}

注意: 始终使用try catch 包裹您的run 方法代码,以捕获任何意外的ExceptionErrorThrowable)。任何到达 executor 的未捕获的 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 对象中定义了一个任务。
  • 我们建立了一个单线程支持的执行器服务。
  • 我们告诉该服务代表我们重复运行我们的任务,并指定了运行该任务的频率。
  • 最终,我们告诉服务停止调度该任务的进一步执行,并关闭其线程池。

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

注意:如果使用多个线程,您仍然需要使您的 Runnable 代码线程安全。执行器框架非常漂亮和有用,但它并不神奇。要了解 Java 中的线程安全性和并发性,请每年阅读这本优秀的书籍:Java Concurrency in Practice Brian Goetz 等人。

运行时。

启动应用程序。 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

跳过连接池

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

所以我建议你现在跳过连接池。让您的代码在使用新连接的同时可靠地工作。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-27
    • 1970-01-01
    • 2015-02-05
    • 1970-01-01
    相关资源
    最近更新 更多