Как заставить поток Java закрыть соединение с локальной базой данных потока

При использовании соединения с локальной базой данных потока закрытие соединения требуется, когда поток существует.

Это я могу сделать только в том случае, если смогу переопределить метод run() вызывающего потока. Даже это не лучшее решение, так как во время выхода я не знаю, открывалось ли когда-либо соединение этим потоком.

На самом деле проблема более общая: как заставить поток вызывать некоторый метод финализации локального объекта потока при его выходе.

Я просмотрел исходники java 1.5 и обнаружил, что локальная карта потока имеет значение null, что в конечном итоге приведет к тому, что сборщик мусора вызовет finalize(), но я не хочу рассчитывать на уборщик мусора.

Следующее переопределение кажется неизбежным, чтобы убедиться, что соединение с базой данных закрыто:

@Override 
public void remove() {
    get().release(); 
    super.remove(); 
}

где release() закрывает соединение с базой данных, если оно было открыто. Но мы не знаем, использовал ли поток когда-либо этот локальный поток. Если get() никогда не вызывался этим потоком, то здесь будет напрасная трата усилий: будет вызвана функция ThreadLocal.initialValue(), в этом потоке будет создана карта и т. д.


Дальнейшее разъяснение и пример согласно комментарию Торбьерна:

java.lang.ThreadLocal — это тип фабрики для объекта, привязанного к потоку. Этот тип имеет геттер для объекта и фабричный метод (обычно пишется пользователем). Когда геттер вызывается, он вызывает фабричный метод только в том случае, если он никогда ранее не вызывался этим потоком.

Использование ThreadLocal позволяет разработчику привязать ресурс к потоку, даже если код потока был написан третьей стороной.

Пример. Допустим, у нас есть тип ресурса с именем MyType, и мы хотим иметь один и только один такой тип для каждого потока.

Определите в классе использования:

private static ThreadLocal<MyType> resourceFactory = new ThreadLocal<MyType>(){
    @override
    protected MyType initialValue(){
        return new MyType();
    }
}

Используйте в локальном контексте в этом классе:

public void someMethod(){
    MyType resource = resourceFactory.get();
    resource.useResource();
}

get() может вызывать initialValue() только один раз в жизненном цикле вызывающего потока. В этот момент экземпляр MyType создается и привязывается к этому потоку. Последующие вызовы get() этого потока снова ссылаются на этот объект.

Классический пример использования — когда MyType является небезопасным для потоков форматированием текста/даты/xml.

Но такие средства форматирования обычно не нужно освобождать или закрывать, в отличие от соединений с базой данных, и я использую java.lang.ThreadLocal, чтобы иметь одно соединение с базой данных для каждого потока.

На мой взгляд, java.lang.ThreadLocal почти идеально подходит для этого. Почти потому, что невозможно гарантировать закрытие ресурса, если вызывающий поток принадлежит стороннему приложению.

Мне нужны ваши мозги: расширив java.lang.ThreadLocal, мне удалось связать одно соединение с базой данных для каждого потока, для его исключительного использования, включая потоки, которые я не могу изменить или переопределить. Мне удалось убедиться, что соединения закрываются в случае, если поток умирает из-за неперехваченного исключения.

В случае нормального завершения потока сборщик мусора закрывает соединение (поскольку MyType переопределяет finalize()). На самом деле это происходит довольно быстро, но это не идеально.

Будь моя воля, в java.lang.ThreadLocal был бы другой метод:

protected void release() throws Throwable {}

Если бы этот метод существовал в java.lang.ThreadLocal, вызываемый JVM при выходе/смерти любого потока, то в моем собственном переопределении я мог бы закрыть свое соединение (и искупитель пришел бы в Сион) .

За неимением такого метода ищу другой способ подтвердить закрытие. Способ, который не будет полагаться на сборку мусора JVM.


person Joel Shemtov    schedule 09.11.2009    source источник
comment
Пожалуйста, добавьте минимальный код, показывающий то, что вы описываете. Желательно работающий.   -  person Thorbjørn Ravn Andersen    schedule 09.11.2009
comment
Добавил немного кода. Надеюсь, это объясняет одну из проблем.   -  person Joel Shemtov    schedule 09.11.2009
comment
Ну нет. Пожалуйста, создайте минимальный функциональный пример.   -  person Thorbjørn Ravn Andersen    schedule 09.11.2009
comment
Я объяснил немного дальше - концепция дизайна, проблема преимуществ и пример кода. Спасибо за внимание.   -  person Joel Shemtov    schedule 10.11.2009
comment
МММммм, отсутствуют деструкторы... слюнявый самый большой недостаток Java.   -  person Kieveli    schedule 10.11.2009


Ответы (9)


Если у вас чувствительный характер, отведите взгляд сейчас.

Я бы не ожидал, что это будет очень хорошо масштабироваться; он эффективно удваивает количество потоков в системе. Могут быть некоторые случаи использования, когда это приемлемо.

public class Estragon {
  public static class Vladimir {
    Vladimir() { System.out.println("Open"); }
    public void close() { System.out.println("Close");}
  }

  private static ThreadLocal<Vladimir> HOLDER = new ThreadLocal<Vladimir>() {
    @Override protected Vladimir initialValue() {
      return createResource();
    }
  };

  private static Vladimir createResource() {
    final Vladimir resource = new Vladimir();
    final Thread godot = Thread.currentThread();
    new Thread() {
      @Override public void run() {
        try {
          godot.join();
        } catch (InterruptedException e) {
          // thread dying; ignore
        } finally {
          resource.close();
        }
      }
    }.start();
    return resource;
  }

  public static Vladimir getResource() {
    return HOLDER.get();
  }
}

Улучшение обработки ошибок и т. д. остается в качестве упражнения для разработчика.

Вы также можете посмотреть на отслеживание потоков/ресурсов в ConcurrentHashMap с опросом другого потока isAlive. Но это решение — последнее средство для отчаявшихся: объекты, вероятно, будут проверяться слишком часто или слишком редко.

Я не могу думать ни о чем другом, что не связано с инструментами. АОП может работать.

Пул соединений был бы моим предпочтительным вариантом.

person McDowell    schedule 10.11.2009
comment
Хорошая идея! Это удвоит количество потоков, но я считаю, что мы справимся с этим. Опрос веток невозможен. Если я сохраняю карту потоков, я могу вообще отказаться от ThreadLocal. Спасибо - person Joel Shemtov; 10.11.2009
comment
Я мог бы поспорить о пуле соединений. Однако подключение к базе данных — это всего лишь пример. Более общая проблема заключается в том, что система ThreadLocal в Java страдает из-за неспособности гарантировать немедленное освобождение ресурсов при выходе из потока. Из-за этого он не идеален для ресурсов, которые необходимо высвободить. - person Joel Shemtov; 13.11.2009
comment
Я понимаю, но оставлять ресурсы открытыми до тех пор, пока поток не умрет, также менее чем оптимально (возможно) в большинстве ситуаций. Например, на сервере Java EE потоки могут быть объединены в пул и повторно использоваться для многих рабочих единиц (запросов сервлетов, транзакций EJB и т. д.), которые могут редко или случайно использоваться вашим маршрутом кода. Таким образом, виртуальная машина может накапливать много открытых, простаивающих подключений. Это решение следует использовать редко и только тогда, когда вы знаете или контролируете детали создания/использования потока. - person McDowell; 13.11.2009
comment
Вы правы, но я считаю, что ThreadLocal не предназначен для того типа использования потоков, который вы описываете. С другой стороны, это также не предназначено для противоположного случая, когда вы гарантируете полное управление созданием всех потоков (тогда вы можете назначить переменную непосредственно потоку). Здесь большинство потоков генерируется нашей самодельной локальной библиотекой либо напрямую, либо через вызовы стороннего API, поэтому я рад иметь соединение с БД, привязанное к каждому потоку, который в нем нуждается. сделан. Но это далеко не идеально, когда дело доходит до выпуска - person Joel Shemtov; 17.11.2009
comment
Это чертовски сложно! Снимаю перед вами шляпу! - person Donal Fellows; 09.06.2021

Оберните свой Runnable новым Runnable с

try {
  wrappedRunnable.run();
} finally {
  doMandatoryStuff();
}

конструкция, и вместо этого пусть выполняется THAT.

Вы даже можете превратить это в метод, например:

  Runnable closingRunnable(Runnable wrappedRunnable) {
    return new Runnable() {
      @Override
      public void run() {
        try {
          wrappedRunnable.run();
        } finally {
          doMandatoryStuff();
        }
      }
    };
  }

и вызовите этот метод, передав искомый исполняемый файл.

Вместо этого вы также можете рассмотреть возможность использования Executor. Значительно упрощает управление Runable и Callables.

Если вы используете ExecutorService, вы можете использовать его как executor.submit(closingRunnable(normalRunnable))

Если вы знаете, что вы закрываете весь свой ExecutorService и хотите, чтобы соединения закрывались в этот момент, вы можете установить фабрику потоков, которая также выполняет закрытие «после того, как все задачи выполнены и завершение работы вызвано исполнителем», например:

  ExecutorService autoClosingThreadPool(int numThreads) {
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(numThreads, numThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); // same as Executors.newFixedThreadPool
    threadPool.setThreadFactory(new ThreadFactory() {
      @Override
      public Thread newThread(Runnable r) {
        return new Thread(closingRunnable(r)); // closes it when executor is shutdown
      }
    });
    return threadPool;
  }

С точки зрения того, может ли doMandatoryStuff знать, было ли соединение когда-либо открыто или нет ранее, одна вещь, которая приходит на ум, это иметь второй ThreadLocal, который просто отслеживает, было ли оно открыто или нет (например: когда соединение открыто, получить затем установите для AtomicInteger значение 2, во время очистки проверьте, остается ли оно по умолчанию, скажем, 1...)

person Thorbjørn Ravn Andersen    schedule 09.11.2009
comment
Спасибо. Действительно, я завершаю пробег, где могу. Здесь все еще есть две проблемы: 1. Некоторые потоки, использующие локальный объект потока, являются сторонними потоками, и у меня нет возможности обернуть их run() 2. Финализация (то, что вы называете requiredStuff) требует доступа к локальному объекту потока. . Увы, если этот поток никогда не обращался к нему, то объект должен быть создан и инициализирован. В этом случае соединение с базой данных будет открыто... только для того, чтобы его можно было закрыть. (Я нашел способ обойти это - довольно уродливо, хотя) Есть ли способ проверить, был ли инициализирован локальный поток? - person Joel Shemtov; 09.11.2009
comment
Я не уверен, что полностью понимаю то, что вы описываете. Можете ли вы создать минималистичный рабочий пример? - person Thorbjørn Ravn Andersen; 09.11.2009

Обычная практика JDBC заключается в том, что вы закрываете ConnectionStatement и ResultSet) в том же самом блоке метода, в котором вы его получили.

В коде:

Connection connection = null;

try {
    connection = getConnectionSomehow();
    // Do stuff.
} finally {
    if (connection != null) {
        try {
            connection.close();
        } catch (SQLException e) {
            ignoreOrLogItButDontThrowIt(e);
        }
    }
}

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

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

person BalusC    schedule 09.11.2009
comment
Показанный выше пример — открытие соединения на лету — это то, чего я хотел бы избежать. Это возможно только в том случае, если вы можете доверять пулу соединений. Намерение моего дизайна состоит в том, чтобы иметь соединение, привязанное к потоку, поэтому каждый поток может иметь свое единственное эксклюзивное соединение, если оно ему нужно. Теперь я не создаю все потоки, которые могут вызывать мои запросы. Поэтому я не могу назначить соединение с базой данных потоку, если не использую тип java.lang.ThreadLocal. Это оказалось хорошим решением. Единственная проблема заключается в закрытии, когда поток (сторонний) выходит - person Joel Shemtov; 10.11.2009
comment
Ясно, но имхо все еще рискованно для утечек ресурсов. Поток может зайти в тупик или работать дольше, чем разрешено, чтобы соединение оставалось открытым. - person BalusC; 10.11.2009

Я действительно не понимаю, почему вы не используете традиционный пул соединений. Но я предполагаю, что у вас есть свои причины.

Сколько у вас свободы? Поскольку некоторые DI-фреймворки поддерживают жизненные циклы объектов и переменные в области потока (все они хорошо проксированы). Не могли бы вы использовать один из них? Я думаю, что Spring сделает все это из коробки, в то время как Guice потребуется сторонняя библиотека для обработки жизненных циклов и областей действия потоков.

Далее, насколько вы контролируете создание переменной ThreadLocal или создание потоков? Я предполагаю, что у вас есть полный контроль над ThreadLocal, но вы не ограничены в создании потоков?

Не могли бы вы использовать аспектно-ориентированное программирование для мониторинга новых Runnable или потоков, расширяющих метод run() для включения очистки? Вам также потребуется расширить ThreadLocal, чтобы он мог зарегистрироваться.

person Michael Lloyd Lee mlk    schedule 10.11.2009

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

person pgras    schedule 09.11.2009
comment
В вашем предложении нет ничего плохого. Однако очень часто у вас не будет доступа к началу и концу метода run(). Очень часто вы реализуете метод, который вызывается сторонним API или фреймом. Это тот случай, когда ThreadLocal может стать полезным, и именно в этом случае вы можете задаться вопросом, как убедиться, что ресурс ThreadLocal не останется для закрытия GC. - person Joel Shemtov; 02.01.2018

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

В конкретном случае, если вы вызываете потоки до такой степени, вы можете передать соединение своим методам в начале потока либо с помощью метода, который принимает настраиваемый параметр, либо с помощью некоторой формы внедрения зависимостей. Затем, поскольку у вас есть код, обеспечивающий соединение, у вас есть код, который его удаляет.

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

person Yishai    schedule 09.11.2009
comment
К слову, если вы знакомы с java.lang.ThreadLocal, то знаете, что он позволяет вам определять инстанцирование вашего объекта, но не его завершение. Конечно, есть java.lang.Object.finalize(), и я использую его, но, насколько я знаю, он вызывается не выходящим потоком, а только позже, после его смерти, сборщиком мусора. - person Joel Shemtov; 10.11.2009
comment
@ Джоэл, я думаю, мы говорим об одном и том же. Создатель (первый вызывающий метод get) должен закрыть ресурс. Все, что я говорю, это то, что вы можете отделить создателя от остального кода, хотя это может и не иметь смысла. - person Yishai; 10.11.2009

Переопределите метод get() в ThreaedLocal, чтобы он устанавливал свойство List в подклассе. Это свойство можно легко запросить, чтобы определить, был ли вызван метод get() для конкретного потока. Затем вы можете получить доступ к ThreadLocal, чтобы очистить его в этом случае.

Обновлено в ответ на комментарий

person Martin OConnor    schedule 10.11.2009
comment
Спасибо. Из вашего ответа мне непонятно, как новое свойство может определить, какой поток вызвал get(), а какой еще нет. - person Joel Shemtov; 10.11.2009
comment
В этом случае вы можете использовать List‹Thread› для хранения всех потоков, которые обращаются к ThreadLocal. - person Martin OConnor; 10.11.2009

То, что мы сделали, было

@Override
public void run() {
  try {
    // ...
  } finally {
    resource.close();
  }
}

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

person rogerdpack    schedule 05.12.2017

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

Например, если вы создаете Executor, расширяя ThreadPoolExecutor, вы можете переопределить методы afterExecution() и terminated() для выполнения этого. Первый для потока, завершающегося ненормально, а второй - для нормального.

person Hu Jinfeng    schedule 04.02.2019