printf fan
printf fan

Reputation: 155

How can I be notified when a thread (that I didn't start) ends?

I have a library in a Jar file that needs to keep track of how many threads that use my library. When a new thread comes in is no problem: I add it to a list. But I need to remove the thread from the list when it dies.

This is in a Jar file so I have no control over when or how many threads come through. Since I didn't start the thread, I cannot force the app (that uses my Jar) to call a method in my Jar that says, "this thread is ending, remove it from your list". I'd REALLY rather not have to constantly run through all the threads in the list with Thread.isAlive().

By the way: this is a port of some C++ code which resides in a DLL and easily handles the DLL_THREAD_DETACH message. I'd like something similar in Java.

Edit:

The reason for keeping a list of threads is: we need to limit the number of threads that use our library - for business reasons. When a thread enters our library we check to see if it's in the list. If not, it's added. If it is in the list, we retrieve some thread-specific data. When the thread dies, we need to remove it from the list. Ideally, I'd like to be notified when it dies so I can remove it from the list. I can store the data in ThreadLocal, but that still doesn't help me get notification of when the thread dies.

Edit2: Original first sentence was: "I have a library in a Jar file that needs to keep track of threads that use objects in the library."

Upvotes: 1

Views: 256

Answers (3)

kdgregory
kdgregory

Reputation: 39606

Based on your edits, your real problem is not tracking when a thread dies, but instead limiting access to your library. Which is good, because there's no portable way to track when a thread dies (and certainly no way within the Java API).

I would approach this using a passive technique, rather than an active technique of trying to generate and respond to an event. You say that you're already creating thread-local data on entry to your library, which means that you already have the cutpoint to perform a passive check. I would implement a ThreadManager class that looks like the following (you could as easily make the methods/variables static):

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;
        }
    }

The thread-local data is maintained in threadTable. This is a ConcurrentHashMap, which means that it provides fast concurrent reads, as well as concurrent iteration (that will be important below). In the happy case, the thread has already been here, so we just return its thread-local data.

In the case where a new thread has called into the library, we need to create its thread-local data. If we have fewer threads than the limit, this proceeds quickly: we create the data, store it in the map, and return it (createThreadLocalData() could be replaced with a new, but I tend to like factory methods in code like this).

The sad case is where the table is already at its maximum size when a new thread enters. Because we have no way to know when a thread is done, I chose to simply leave the dead threads in the table until we need space -- just like the JVM and memory management. If we need space, we execute doCleanup() to purge the dead threads (garbage). If there still isn't enough space once we've cleared dead threads, we throw (we could also implement waiting, but that would increase complexity and is generally a bad idea for a library).

Synchronization is important. If we have two new threads come through at the same time, we need to block one while the other tries to get added to the table. The critical section must include the entirety of checking, optionally cleaning up, and adding the new item. If you don't make that entire operation atomic, you risk exceeding your limit. Note, however, that the initial get() does not need to be in the atomic section, so we don't need to synchronize the entire method.

OK, on to doCleanup(): this simply iterates the map and looks for threads that are no longer alive. If it finds one, it calls the destructor ("anti-factory") for its thread-local data:

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

}

Even though this function is called from within a synchronized block, it's written as if it could be called concurrently. One of the nice features of ConcurrentHashMap is that any iterators it produces can be used concurrently, and give a view of the map at the time of call. However, that means that two threads might check the same map entry, and we don't want to call the destructor twice. So we use remove() to get the entry, and if it's null we know that it's already been (/being) cleaned up by another thread.

As it turns out, you might want to call the method concurrently. Personally, I think the "clean up when necessary" approach is simplest, but your thread-local data might be expensive to hold if it's not going to be used. If that's the case, create a Timer that will repeatedly call 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;
}

Upvotes: 2

jtahlborn
jtahlborn

Reputation: 53694

You can use a combination of ThreadLocal and WeakReference. Create some sort of "ticket" object and when a thread enters the library, create a new ticket and put it in the ThreadLocal. Also, create a WeakReference (with a ReferenceQueue) to the ticket instance and put it in a list inside your library. When the thread exits, the ticket will be garbage collected and your WeakReference will be queued. by polling the ReferenceQueue, you can essentially get "events" indicating when a thread exits.

Upvotes: 2

Peter Lawrey
Peter Lawrey

Reputation: 533620

Normally you would let the GC clean up resources. You can add a component to the thread which will be cleaned up when it is not longer accessible.

If you use a custom ThreadGroup, it will me notified when a thread is removed from the group. If you start the JAR using a thread in the group, it will also be part of the group. You can also change a threads group so it will be notifed via reflection.

However, polling the threads every few second is likely to be simpler.

Upvotes: 2

Related Questions