Служба Java Executor не закрывает потоки при использовании с JDBC - PullRequest
0 голосов
/ 21 марта 2019

Я использую ExecutorService с FixedThreadPool для выполнения некоторого SQL через JDBC.Однако, когда я профилирую свое приложение, кажется, что количество потоков только увеличивается, и память, конечно, тоже.Проблема в том, что это как-то связано с JDBC, потому что когда я удаляю создание операторов и соединений внутри моей задачи для пула потоков, счетчик потоков вообще не увеличивается.

Вот как я добавляю задачи в свой потокpool:

       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;
}

Вот пример из профилировщика (imgur)

Кажется, что потоки не закончились должным образом, что очень странно, потому что ExecutorServiceДолжен ли он закончиться, если это фиксированный пул из 5 соединений, верно? Так что я понятия не имею, как хеды все еще там, они вызывают довольно большую утечку памяти.

Проблема заключается в создании Connection и PreparedStatementобъекты, потому что, когда я закомментирую это, число потоков останется фиксированным значением.

1 Ответ

1 голос
/ 21 марта 2019

Вы не показываете весь свой код, например isCancelled. Поэтому мы не можем помочь конкретно. Но ваш подход, кажется, не в порядке, так что читайте дальше.

ScheduledExecutorService

Вы не должны пытаться управлять временем обслуживания вашего исполнителя. Если вы звоните Thread.sleep вместе со службой исполнителя, вы, вероятно, делаете что-то не так.

Не стоит и призывать new Thread. Весь смысл службы executor состоит в том, чтобы позволить инфраструктуре управлять деталями потоков. Ты слишком много работаешь.

Для повторного вызова задачи используйте ScheduledExecutorService.

Для получения дополнительной информации см. Oracle Tutorial , class 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, чтобы перехватить любой неожиданный Exception или Error (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 поточно-ориентированным при использовании нескольких потоков. Среда executor чрезвычайно удобна и полезна, но она не волшебная. Чтобы узнать о безопасности потоков и параллелизме в Java, ежегодно читайте эту превосходную книгу: Параллелизм Java на практике Брайана Гетца и др.

При запуске.

Запуск приложения. 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

Пропустить пул соединений

По моему опыту, потребность в пуле соединений с базами данных преувеличена многими людьми. Есть несколько ловушек при использовании пула соединений. И я считаю, что установление соединения с базой данных не так дорого, как утверждают многие, особенно если локально на той же машине.

Так что я предлагаю вам пока пропустить пул соединений. Получите ваш код надежно работает при использовании свежих соединений.

Если позднее вы сможете доказать узкое место в производительности из-за соединений с базой данных, рассмотрите пул. И убедиться, что это действительно помогает. В противном случае вы бы совершили грех преждевременной оптимизации.

...