我在Jar文件中有一个库,需要跟踪使用我的库的线程数。当新线程进来时没问题:我将它添加到列表中。但我需要在列表死亡时从列表中删除它。
这是一个Jar文件,所以我无法控制何时或多少线程通过。由于我没有启动该线程,我无法强制应用程序(使用我的Jar)调用我的Jar中的方法,该方法说,"此线程正在结束,将其从列表中删除"。我非常真的而不必经常使用Thread.isAlive()来遍历列表中的所有线程。
顺便说一句:这是一些C ++代码的端口,它驻留在DLL中,可以轻松处理DLL_THREAD_DETACH消息。我喜欢Java中类似的东西。
编辑:
保留线程列表的原因是:出于业务原因,我们需要限制使用我们库的线程数。当一个线程进入我们的库时,我们检查它是否在列表中。如果没有,则添加它。如果它在列表中,我们将检索一些特定于线程的数据。当线程死亡时,我们需要将其从列表中删除。理想情况下,我希望在它死亡时收到通知,以便我可以将其从列表中删除。我可以将数据存储在ThreadLocal中,但这仍然无法帮助我获知线程何时死亡的通知。
EDIT2: 原始的第一句话是:"我在Jar文件中有一个库,需要跟踪使用库中对象的线程。"
答案 0 :(得分:2)
通常你会让GC清理资源。您可以向线程添加一个组件,当该线程不再可访问时,该组件将被清理。
如果您使用自定义ThreadGroup,则会在从组中删除线程时通知我。如果使用组中的线程启动JAR,它也将成为组的一部分。您还可以更改线程组,以便通过反射进行通知。
但是,每隔几秒轮询一次线程可能会更简单。
答案 1 :(得分:2)
您可以使用ThreadLocal和WeakReference的组合。创建某种"票"对象以及当线程进入库时,创建一个新票证并将其放入ThreadLocal中。此外,为故障单实例创建一个WeakReference(带有ReferenceQueue)并将其放在库中的列表中。当线程退出时,票证将被垃圾收集并且您的WeakReference将排队。通过轮询ReferenceQueue,您基本上可以获得"事件"指示线程何时退出。
答案 2 :(得分:1)
根据您的修改,当线程死亡时,您真正的问题是不跟踪,而是限制对您的库的访问。哪个好,因为没有可移植的方式来跟踪线程何时死亡(当然在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()
:这只是迭代地图并查找不再存在的线程。如果找到一个,它会调用析构函数(&#34;反工厂&#34;)作为其线程本地数据:
private void doCleanup() {
for (Thread thread : threadTable.keySet()) {
if (! thread.isAlive()) {
MyThreadLocalData data = threadTable.remove(thread);
if (data != null) {
destroyThreadLocalData(data);
}
}
}
}
即使从同步块中调用此函数,它也可以被编写,就好像可以同时调用它一样。 ConcurrentHashMap
的一个很好的特性是它产生的任何迭代器可以同时使用,并在调用时提供地图视图。但是,这意味着两个线程可能会检查相同的映射条目,并且我们不想两次调用析构函数。所以我们使用remove()
来获取条目,如果它为null,我们就知道它已被(/正在)另一个线程清理过。
事实证明,您可能希望同时调用该方法。就个人而言,我认为&#34;必要时进行清理&#34;方法是最简单的,但是如果不使用它,那么你的线程本地数据可能会很昂贵。如果是这种情况,请创建一个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;
}