如何强制Java线程关闭线程本地数据库连接

14

当使用线程本地数据库连接时,需要在线程存在时关闭连接。

只有当我能覆盖调用线程的run()方法时,才能做到这一点。即使如此,这也不是一个很好的解决方案,因为在退出时,我不知道该线程是否曾经打开过连接。

问题实际上更普遍:如何强制线程在退出时调用线程本地对象的某些终止方法。

我查看了Java 1.5的源代码,并发现线程本地映射被设置为null,这最终会导致垃圾收集器调用finalize(),但我不想依赖于垃圾收集器。

为确保关闭数据库连接,必须进行以下重写:

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

在这里,release() 方法关闭数据库连接,如果它已经被打开。但我们不知道该线程是否曾经使用过这个线程本地变量。如果该线程从未调用过 get() 方法,则会浪费很多资源:将调用 ThreadLocal.initialValue() 方法,在此线程上创建一个映射表等。


根据 Thorbjørn 的评论进一步解释和示例:

java.lang.ThreadLocal 是一种绑定到线程的对象工厂。这种类型有一个获取器(getter)和一个工厂方法(通常由用户编写)。当 getter 被调用时,仅在该线程之前从未调用过它时才调用工厂方法。

使用 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垃圾收集的方式。

请添加最少的代码来展示您所描述的内容。最好是可运行的。 - Thorbjørn Ravn Andersen
添加了一些代码。希望这解释了其中的一个问题。 - Joel Shemtov
不, 请创建一个最小的功能示例。 - Thorbjørn Ravn Andersen
我进一步解释了设计概念、优势问题和代码示例。感谢关注。 - Joel Shemtov
MMMmmm 缺少析构函数... 流口水 Java 最大的缺陷。 - Kieveli
9个回答

14

如果你比较敏感的话,现在可以不用看了。

我认为这种方法在大规模应用上可能不太可行;它会有效地增加系统中线程的数量。但也许有一些情况可以接受使用它。

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 方法。但这种解决方案是绝望的最后选择——对象可能会被检查得过于频繁或不够频繁。
除了仪器化之外,我想不到其他办法。AOP 可能有效。
连接池是我的首选选项。

好主意!这将使线程数量翻倍,但我相信我们可以处理好。轮询线程是不可行的。如果我保留线程的映射表,我可能会放弃ThreadLocal。 - Joel Shemtov
我可以就连接池进行争论。然而,数据库连接只是一个例子。更一般的问题是,Java的ThreadLocal系统由于无法保证线程退出时立即释放资源而受到限制。因此,对于需要释放的资源来说,它并不完美。 - Joel Shemtov
我理解,但在线程结束之前保持资源开放在(可以说)大多数情况下也不是最优的。例如,在Java EE服务器上,线程可能会被池化并重复使用于许多工作单元(servlet请求、EJB事务等),这些工作单元可能很少或偶然地被您的代码路径使用。因此,虚拟机可能会积累许多打开的空闲连接。这种解决方案应该很少使用,并且仅在您知道或控制线程创建/使用细节时才使用。 - McDowell
你说得没错,但我认为ThreadLocal并不是为你所描述的线程使用类型而设计的。另一方面,它也不适用于相反的情况,即当您可以直接将变量分配给线程时(这样您就可以完全控制所有线程创建)。在这种情况下,大多数线程都是由我们自己制作的本地库直接生成或通过调用第三方API生成的,因此我很高兴将DB连接绑定到每个需要它的线程 - 我认为这就是ThreadLocal的用途。但是在释放方面还不够完美。 - Joel Shemtov

6

将您的Runnable用一个新的Runnable包装起来,使用

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

进行构造,并执行它。

你甚至可以将其制作成一个方法,例如:

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

你可以调用该方法并传入你需要的可运行对象。

你也可以考虑使用Executor,这样更容易管理Runnable和Callable。

如果你使用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...)


1
谢谢。的确,我在能够处理的地方包装了运行。这里仍然存在两个问题:
  1. 一些使用线程本地对象的线程是第三方线程,我无法包装它们的run()。
  2. 最终化(您称之为mandatoryStuff)需要访问线程本地对象。但是,如果该对象还没有被此线程访问过,则必须实例化和初始化该对象。在这种情况下,将打开数据库连接...只是为了关闭它。
(我已经找到了一个解决方法 - 虽然很丑)是否有一种方法可以检查线程本地对象是否已初始化?
- Joel Shemtov
我不确定我完全理解你所描述的内容。你能否创建一个最简化的、可工作的示例? - Thorbjørn Ravn Andersen

4
通常的JDBC做法是在获得Connection(以及Statement和ResultSet)的同一个方法块中关闭它。
代码如下:
Connection connection = null;

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

考虑到这一点,您的问题让我觉得您的设计存在问题。在最短的范围内获取和关闭这些昂贵的外部资源将使应用程序免受潜在的资源泄漏和崩溃。

如果您最初的意图是改善连接性能,则需要查看连接池。您可以使用例如C3P0 API。或者,如果它是一个Web应用程序,则使用应用服务器的内置连接池设施,以DataSource的形式。有关详细信息,请参阅应用服务器特定文档。


上面展示的例子 - 即时打开连接 - 是我希望避免的。只有在您可以信任连接池的情况下才是可行的。我的设计意图是将连接绑定到线程 - 因此每个线程都可以拥有它自己独特的连接,如果需要的话。现在,我并不创建所有可能调用我的查询的线程。因此,除非使用java.lang.ThreadLocal类型,否则无法将数据库连接分配给线程。结果证明这是一个好的解决方案。唯一的问题是在(第三方)线程退出时如何关闭连接。 - Joel Shemtov
清除资源泄漏的方法很明确,但在我看来仍然存在风险。线程可能会死锁或运行时间超过连接允许保持打开的时间。 - BalusC

3

我不是很明白为什么您不使用传统的连接池。但我会假设您有自己的理由。

您有多少自由度?因为一些依赖注入框架支持对象生命周期和线程范围变量(都被很好地代理)。你可以用其中一个吗?我认为Spring可以在开箱即用的情况下完成所有操作,而Guice需要一个第三方库来处理生命周期和线程范围。

接下来,您对ThreadLocal变量的创建或线程的创建有多少控制权?我猜您对ThreadLocal有完全的控制权,但在创建线程方面却几乎没有任何限制?

您可以使用面向切面编程来监视包含清理的新Runnable或扩展run()方法的线程吗?您还需要扩展ThreadLocal以便它可以注册自身。


1
我们所做的是:
@Override
public void run() {
  try {
    // ...
  } finally {
    resource.close();
  }
}

基本上,对于线程中的所有路径,始终(可能打开然后)关闭它。如果有帮助的话 :)

1
你必须在一个地方打开连接,所以你也必须处理关闭。根据你的环境,线程可能会被重用,你不能期望在线程被垃圾回收之前应用程序就已经关闭了。

你的建议没有问题。然而,很多时候你无法访问run()方法的开头和结尾。很多时候你会实现一个被第三方API或框架调用的方法。这种情况下ThreadLocal可以变得非常有用,你可能会想知道如何确保ThreadLocal资源不会被GC关闭。 - Joel Shemtov

1

我认为在一般情况下,除了经典的方法之外,没有更好的解决方案:获取资源的代码必须负责关闭它。

在特定情况下,如果您需要调用线程,可以在线程开始时将连接传递给您的方法,可以使用带有自定义参数的方法或通过某种形式的依赖注入。然后,由于您拥有提供连接的代码,因此您也有删除它的代码。

基于注释的依赖注入可能适用于这里,因为不需要连接的代码不会获得连接,因此不需要关闭,但是听起来您的设计已经太过深入,无法像那样进行改装。


如果你熟悉java.lang.ThreadLocal,你就知道它允许你定义对象的实例化,但不能定义其最终化。当然有java.lang.Object.finalize(),我也使用它,但据我所知,它不是由退出线程调用的,而是在垃圾回收器在其死亡后稍后调用的。 - Joel Shemtov
@Joel,我认为我们在说同一件事情。创建者(get方法的第一个调用者)必须关闭资源。我所说的是,你可以将创建者与其余代码解耦,尽管这样做可能没有意义。 - Yishai

1

重写ThreadLocal中的get()方法,以便它在子类上设置一个List属性。这个属性可以轻松地查询,以确定该线程是否已调用get()方法。在这种情况下,您可以访问ThreadLocal来清理它。

响应评论后更新


谢谢。从你的回答中,我无法确定新属性如何区分已经调用get()的线程和尚未调用的线程。 - Joel Shemtov
在这种情况下,您可以使用List<Thread>来存储所有访问ThreadLocal的线程。 - Martin OConnor

0

我正在研究同样的问题。 目前看来,你必须使用finalize(),尽管它现在已被弃用。 由于任务被提交给某个Executor,除非Executor向您显示线程何时退出,否则您永远不会知道线程确切地退出,这意味着您在某种程度上可以控制Executor。

例如,如果通过扩展ThreadPoolExecutor构建Executor,则可以重写afterExecution()和terminated()方法以实现此目的。前者用于线程异常退出,而后者用于正常退出。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接