【问题标题】:How can I be notified when a thread (that I didn't start) ends?当一个线程(我没有开始)结束时如何通知我?
【发布时间】:2014-08-05 19:23:25
【问题描述】:

我在 Jar 文件中有一个库,它需要跟踪使用我的库的线程数。当一个新线程进来时没问题:我将它添加到一个列表中。但是当它死时我需要从列表中删除它。

这是在一个 Jar 文件中,所以我无法控制何时或多少线程通过。由于我没有启动线程,因此我无法强制应用程序(使用我的 Jar)调用我的 Jar 中的方法,该方法显示“此线程正在结束,将其从您的列表中删除”。我希望真的 不必不断地使用 Thread.isAlive() 遍历列表中的所有线程。

顺便说一句:这是一些 C++ 代码的移植,它驻留在 DLL 中,可以轻松处理 DLL_THREAD_DETACH 消息。我想要 Java 中类似的东西。

编辑:

保留线程列表的原因是:出于业务原因,我们需要限制使用我们库的线程数量。当一个线程进入我们的库时,我们检查它是否在列表中。如果没有,则添加。如果它在列表中,我们检索一些特定于线程的数据。当线程死亡时,我们需要将其从列表中删除。理想情况下,我希望在它死亡时收到通知,以便将其从列表中删除。我可以将数据存储在 ThreadLocal 中,但这仍然无法帮助我获得有关线程何时终止的通知。

编辑2: 最初的第一句话是:“我在 Jar 文件中有一个库,需要跟踪使用库中对象的线程。”

【问题讨论】:

  • 是线程持有资源需要事后清理?
  • jtahlborn:不,它没有持有资源。一个线程进来,做一些工作,然后消失。
  • 您的库是否创建线程?
  • @NESPowerGlove:没有。有人更改了这个问题的标题。它是:“当一个线程我没有开始结束时如何通知我?”

标签: java multithreading


【解决方案1】:

通常您会让 GC 清理资源。您可以向线程中添加一个组件,该组件将在不再可访问时被清理。

如果您使用自定义线程组,当线程从组中删除时,它会通知我。如果您使用组中的线程启动 JAR,它也将成为组的一部分。您还可以更改线程组,以便通过反射通知它。

但是,每隔几秒轮询一次线程可能会更简单。

【讨论】:

  • 所以我无法控制何时或多少线程通过。 - 我认为 OP 没有使用自定义线程组。所以,也许轮询是唯一的解决方案。 :P
  • @TheLostMind 我以为 OP 是开发人员,可以添加自定义类。
  • ya.. 如果他可以添加自定义类,那就没问题了。
  • 删除线程时如何通知自定义ThreadGroup?
  • @Holger 上次我这样做时,我修改了 ThreadGroup,不幸的是,这似乎仍然是唯一的选择。也许这不是一个好主意,但是会调用threadTerminated(Thread)。
【解决方案2】:

您可以结合使用 ThreadLocal 和 WeakReference。创建某种“票”对象,当线程进入库时,创建一个新票并将其放入 ThreadLocal。此外,为票证实例创建一个 WeakReference(带有 ReferenceQueue)并将其放入库中的列表中。当线程退出时,票证将被垃圾收集,您的 WeakReference 将被排队。通过轮询 ReferenceQueue,您基本上可以获得指示线程何时退出的“事件”。

【讨论】:

  • 谢谢,但我需要此列表实时准确。我迫不及待地等待 GC 稍后清除这些“票证”。
  • 好吧,如果您不打算轮询当前正在运行的线程,那么无论如何您都不会获得“实时”。这种方法的资源消耗应该少很多。
  • 有什么理由需要“票”而不是简单地使用指向线程本身的WeakReference?
  • 虽然在这种情况下我可能会考虑使用幻像引用。
  • @kdgregory - 除了我没有想到使用线程本身之外,没有理由买票(尽管即使在线程完成运行后也可能在其他地方引用)。
【解决方案3】:

根据您的编辑,您真正的问题是不跟踪线程何时终止,而是限制对您的库的访问。这很好,因为没有可移植的方法来跟踪线程何时终止(当然在 Java API 中也没有办法)。

我会使用被动技术来解决这个问题,而不是尝试生成和响应事件的主动技术。您说您已经在进入库时创建线程本地数据,这意味着您已经拥有执行被动检查的切点。我将实现一个如下所示的ThreadManager 类(您可以轻松地将方法/变量设为静态):

public class MyThreadLocalData {
    // ...
}

public class TooManyThreadsException
extends RuntimeException {
    // ...
}

public class ThreadManager
{
    private final static int MAX_SIZE = 10;

    private ConcurrentHashMap<Thread,MyThreadLocalData> threadTable = new ConcurrentHashMap<Thread,ThreadManager.MyThreadLocalData>();
    private Object tableLock = new Object();

    public MyThreadLocalData getThreadLocalData() {
        MyThreadLocalData data = threadTable.get(Thread.currentThread());
        if (data != null) return data;

        synchronized (tableLock) {
            if (threadTable.size() >= MAX_SIZE) {
                doCleanup();
            }            

            if (threadTable.size() >= MAX_SIZE) {
                throw new TooManyThreadsException();
            }

            data = createThreadLocalData();
            threadTable.put(Thread.currentThread(), data);
            return data;
        }
    }

线程本地数据保存在threadTable 中。这是一个ConcurrentHashMap,这意味着它提供了快速的并发读取以及并发迭代(这在下面很重要)。在好的情况下,线程已经在这里,所以我们只返回它的线程本地数据。

在新线程调用库的情况下,我们需要创建它的线程本地数据。如果我们的线程数少于限制,这将很快进行:我们创建数据,将其存储在映射中,然后返回它(createThreadLocalData() 可以替换为new,但我更喜欢代码中的工厂方法,例如这个)。

可悲的情况是,当新线程进入时,表已经达到最大大小。因为我们无法知道线程何时完成,所以我选择简单地将死线程留在表中,直到我们需要空间——就像 JVM 和内存管理一样。如果我们需要空间,我们执行doCleanup() 来清除死线程(垃圾)。如果在我们清除死线程后仍然没有足够的空间,我们会抛出(我们也可以实现等待,但这会增加复杂性并且对于库来说通常是个坏主意)。

同步很重要。如果我们同时有两个新线程通过,我们需要阻止一个,而另一个尝试添加到表中。关键部分必须包括检查、选择性清理和添加新项目的全部。如果您不使 整个 操作成为原子操作,则可能会超出限制。但是请注意,最初的get() 确实不需要在原子部分,所以我们不需要同步整个方法。

好的,继续doCleanup():这只是迭代映射并查找不再活动的线程。如果找到,它会调用析构函数(“anti-factory”)来获取其线程本地数据:

private void doCleanup() {
    for (Thread thread : threadTable.keySet()) {
        if (! thread.isAlive()) {
            MyThreadLocalData data = threadTable.remove(thread);
            if (data != null) {
                destroyThreadLocalData(data);
            }
        }
    }

}

即使这个函数是从一个同步块中调用的,它的编写方式就好像它可以同时调用一样。 ConcurrentHashMap 的优点之一是它生成的任何迭代器都可以同时使用,并在调用时提供地图视图。然而,这意味着两个线程可能会检查同一个映射条目,我们不想调用析构函数两次。所以我们使用remove() 来获取条目,如果它为空,我们就知道它已经(/正在)被另一个线程清理了。

事实证明,您可能希望同时调用该方法。就个人而言,我认为“必要时清理”方法最简单,但如果不使用线程本地数据,保存它可能会很昂贵。如果是这种情况,请创建一个Timer,它将重复调用doCleanup():

public Timer scheduleCleanup(long interval) {
    TimerTask task = new TimerTask() {
        @Override
        public void run() {
            doCleanup();
        }
    };
    Timer timer = new Timer(getClass().getName(), true);
    timer.scheduleAtFixedRate(task, 0L, interval);
    return timer;
}

【讨论】:

  • 哇!感谢您的精彩描述和代码。你的建议是我担心我不得不做的,但我很高兴知道那里没有一些更简单/更明显的解决方案。我喜欢在我们需要空间之前将死线留在那里的想法。抱歉,我无法投票赞成您的答案 b/c 我没有足够的声誉。 :-(
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-26
  • 2010-11-05
相关资源
最近更新 更多