根据您的编辑,您真正的问题是不跟踪线程何时终止,而是限制对您的库的访问。这很好,因为没有可移植的方法来跟踪线程何时终止(当然在 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;
}