Java Concurrency: Pitfalls of ScheduledExecutorService
ScheduledExecutorService is an executor service that allows you to schedule future and recurring asynchronous tasks in Java. While using ScheduledExecutorService interface is very easy to understand and intuitive to use, there are some finer implementation details that one must know.
Let’s consider an example use case.
Create a UserMessageCache class that maintains a message cache per user. When user comes online, we setup the cache. Cache stores messages in memory. Each message has its own expiry TTL (Time to Live) value. When user goes offline, we should purge entire cache and free all resources.
Example code looks like below -
Few things to notice here are -
- We have a static ScheduledExecutorService with 1 thread.
- As soon as user becomes active, we create an instance of UserMessageCache and submit a recurring task to ScheduledExecutorService that runs every 20 sec. We have a reference to this task called scheduledFuture.
- Whenever scheduled task is executed, it iterate through all messages and removes expired messages.
- When user goes away, some upper level class calls purge(). That will cancel our scheduledFuture task and clear all messages from the cache.
After running this for some large number of test users and profiling it, we observed that instances of User do not get Garbage Collected. This implementation was causing serious memory leaks.
Why?
- Task scheduled in the constructor calls the evictExpired() of the message cache. It maintains a strong reference to the UserMessageCache instance and hence to User object.
- ScheduledExecutorService is an interface and ScheduledThreadPoolExecutor is implementation. ScheduledThreadPoolExecutor maintains an internal work queue of submitted tasks. If a task is cancelled, it does not remove the task from the queue. So it maintains the reference to task.
Since our ScheduledExecutorService is static instance, it lives forever, keeping reference to all the tasks (even cancelled). Those tasks keep reference to UserMessageCache instances and so on.
Solution:
Since Java 7, ScheduledThreadPoolExecutor exposed a new method setRemoveOnCancelPolicy. As per java docs,
Sets the policy on whether cancelled tasks should be immediately removed from the work queue at time of cancellation. This value is by default
false
.
Java kept the default value to false for backward compatibility reasons. But this default leads to memory leaks when using ScheduledExecutorService. Also, this function is not exposed via ScheduledExecutorService interface since it is implementation specific thing. So correct way is to use ScheduledThreadPoolExecutor directly as below -
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);// Explicitly call setRemoveOnCancelPolicy on the instance
executor.setRemoveOnCancelPolicy(true);