ThreadPoolExecutor in Android
- Authors
- Name
- Amit Shekhar
- Published on
I am Amit Shekhar, Co-Founder @ Outcome School, I have taught and mentored many developers, and their efforts landed them high-paying tech jobs, helped many tech companies in solving their unique problems, and created many open-source libraries being used by top companies. I am passionate about sharing knowledge through open-source, blogs, and videos.
Join Outcome School and get high paying tech job: Outcome School
Before we start, I would like to mention that, I have released a video playlist to help you crack the Android Interview: Check out Android Interview Questions and Answers.
In this blog, we will learn about the thread pools, thread pool executors, and their use within Android. We’ll cover these topics thoroughly, with lots of example code.
Although nowadays, we all use RxJava, Kotlin-Coroutines for background task execution, it is important to know about the ThreadPoolExecutor as all of these libraries are made on top of ThreadPoolExecutor.
Thread Pools
A thread pool manages a pool of worker threads (the exact number varies depending upon its implementation).
A task queue holds tasks waiting to be executed by anyone of the idle threads in the pool. Tasks are added to the queue by producers, whereas the worker threads act as consumers by consuming the tasks from the queue whenever there’s an idle thread ready to perform a new background execution.
ThreadPoolExecutor
The ThreadPoolExecutor executes a given task using one of its threads from the thread pool.
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue
);
What are these parameters?
- corePoolSize: The minimum number of threads to keep in the pool. Initially, there are zero threads in the pool. But as tasks are added to the queue, new threads are created. If there are idle threads — but the thread count is lower than the corePoolSize — then new threads will keep on being created.
- maximumPoolSize: — The maximum number of threads allowed in the pool. If this exceeds the corePoolSize — and the current number of threads is >= corePoolSize — then the new worker threads will be created only if the queue is full.
- keepAliveTime: When the number of threads is greater than the core, the noncore threads (excess idle threads) will wait for a new task, and if they don’t get one within the time defined by this parameter, they will terminate.
- unit: The unit of time for keepAliveTime.
- workQueue: The task queue, which will only hold runnable tasks. It will have to be a BlockingQueue.
Why use Thread Pool Executor in an Android or JAVA application?
- It’s a powerful task execution framework as it supports task addition in a queue, task cancellation, and task prioritization.
- It reduces the overhead associated with thread creation, as it manages a required number of threads in its thread pool.
Using ThreadPoolExecutor in Android
First of all, create a PriorityThreadFactory:
public class PriorityThreadFactory implements ThreadFactory {
private final int mThreadPriority;
public PriorityThreadFactory(int threadPriority) {
mThreadPriority = threadPriority;
}
@Override
public Thread newThread(final Runnable runnable) {
Runnable wrapperRunnable = new Runnable() {
@Override
public void run() {
try {
Process.setThreadPriority(mThreadPriority);
} catch (Throwable t) {
}
runnable.run();
}
};
return new Thread(wrapperRunnable);
}
}
Create a MainThreadExecutor:
public class MainThreadExecutor implements Executor {
private final Handler handler = new Handler(Looper.getMainLooper());
@Override
public void execute(Runnable runnable) {
handler.post(runnable);
}
}
Create a DefaultExecutorSupplier:
/*
* Singleton class for default executor supplier
*/
public class DefaultExecutorSupplier{
/*
* Number of cores to decide the number of threads
*/
public static final int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
/*
* thread pool executor for background tasks
*/
private final ThreadPoolExecutor mForBackgroundTasks;
/*
* thread pool executor for light weight background tasks
*/
private final ThreadPoolExecutor mForLightWeightBackgroundTasks;
/*
* thread pool executor for main thread tasks
*/
private final Executor mMainThreadExecutor;
/*
* an instance of DefaultExecutorSupplier
*/
private static DefaultExecutorSupplier sInstance;
/*
* returns the instance of DefaultExecutorSupplier
*/
public static DefaultExecutorSupplier getInstance() {
if (sInstance == null) {
synchronized(DefaultExecutorSupplier.class){
sInstance = new DefaultExecutorSupplier();
}
return sInstance;
}
/*
* constructor for DefaultExecutorSupplier
*/
private DefaultExecutorSupplier() {
// setting the thread factory
ThreadFactory backgroundPriorityThreadFactory = new
PriorityThreadFactory(Process.THREAD_PRIORITY_BACKGROUND);
// setting the thread pool executor for mForBackgroundTasks;
mForBackgroundTasks = new ThreadPoolExecutor(
NUMBER_OF_CORES * 2,
NUMBER_OF_CORES * 2,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(),
backgroundPriorityThreadFactory
);
// setting the thread pool executor for mForLightWeightBackgroundTasks;
mForLightWeightBackgroundTasks = new ThreadPoolExecutor(
NUMBER_OF_CORES * 2,
NUMBER_OF_CORES * 2,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(),
backgroundPriorityThreadFactory
);
// setting the thread pool executor for mMainThreadExecutor;
mMainThreadExecutor = new MainThreadExecutor();
}
/*
* returns the thread pool executor for background task
*/
public ThreadPoolExecutor forBackgroundTasks() {
return mForBackgroundTasks;
}
/*
* returns the thread pool executor for light weight background task
*/
public ThreadPoolExecutor forLightWeightBackgroundTasks() {
return mForLightWeightBackgroundTasks;
}
/*
* returns the thread pool executor for main thread task
*/
public Executor forMainThreadTasks() {
return mMainThreadExecutor;
}
}
Note: The number of threads available for different thread pools will depend on your requirements.
Now use it as below in your code
/*
* Using it for Background Tasks
*/
public void doSomeBackgroundWork(){
DefaultExecutorSupplier.getInstance().forBackgroundTasks()
.execute(new Runnable() {
@Override
public void run() {
// do some background work here.
}
});
}
/*
* Using it for Light-Weight Background Tasks
*/
public void doSomeLightWeightBackgroundWork(){
DefaultExecutorSupplier.getInstance().forLightWeightBackgroundTasks()
.execute(new Runnable() {
@Override
public void run() {
// do some light-weight background work here.
}
});
}
/*
* Using it for MainThread Tasks
*/
public void doSomeMainThreadWork(){
DefaultExecutorSupplier.getInstance().forMainThreadTasks()
.execute(new Runnable() {
@Override
public void run() {
// do some Main Thread work here.
}
});
}
In this way, we can create a different thread pool for network tasks, I/O tasks, heavy background tasks, and other tasks.
How do I cancel a task?
To cancel a task, you have to get the future of that task. So instead of using execute, you would need to use submit, which will return the future. Now, this future can be used to cancel the task.
/*
* Get the future of the task by submitting it to the pool
*/
Future future = DefaultExecutorSupplier.getInstance().forBackgroundTasks()
.submit(new Runnable() {
@Override
public void run() {
// do some background work here.
}
});
/*
* cancelling the task
*/
future.cancel(true);
How to set the priority of a task?
Let’s say there are 20 tasks in a queue, and the thread pool only holds 4 threads. We execute new tasks based on their priority since the thread pool can only execute 4 at a time.
But let’s say we need the last task that we have pushed in the queue to be executed first. We would need to set the IMMEDIATE priority for that task so that when the thread takes a new task from the queue, it executes this task first (since it has the highest priority).
To set the priority of a task, we need to create a thread pool executor.
Create an ENUM for Priority:
/**
* Priority levels
*/
public enum Priority {
/**
* NOTE: DO NOT CHANGE ORDERING OF THOSE CONSTANTS UNDER ANY CIRCUMSTANCES.
* Doing so will make ordering incorrect.
*/
/**
* Lowest priority level. Used for prefetches of data.
*/
LOW,
/**
* Medium priority level. Used for warming of data that might soon get visible.
*/
MEDIUM,
/**
* Highest priority level. Used for data that are currently visible on screen.
*/
HIGH,
/**
* Highest priority level. Used for data that are required instantly(mainly for emergency).
*/
IMMEDIATE;
}
Create a PriorityRunnable:
public class PriorityRunnable implements Runnable {
private final Priority priority;
public PriorityRunnable(Priority priority) {
this.priority = priority;
}
@Override
public void run() {
// nothing to do here.
}
public Priority getPriority() {
return priority;
}
}
Create a PriorityThreadPoolExecutor, which extends ThreadPoolExecutor. We have to create PriorityFutureTask, which will implement Comparable<PriorityFutureTask>
public class PriorityThreadPoolExecutor extends ThreadPoolExecutor {
public PriorityThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, ThreadFactory threadFactory) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit,new PriorityBlockingQueue<Runnable>(), threadFactory);
}
@Override
public Future<?> submit(Runnable task) {
PriorityFutureTask futureTask = new PriorityFutureTask((PriorityRunnable) task);
execute(futureTask);
return futureTask;
}
private static final class PriorityFutureTask extends FutureTask<PriorityRunnable>
implements Comparable<PriorityFutureTask> {
private final PriorityRunnable priorityRunnable;
public PriorityFutureTask(PriorityRunnable priorityRunnable) {
super(priorityRunnable, null);
this.priorityRunnable = priorityRunnable;
}
/*
* compareTo() method is defined in interface java.lang.Comparable and it is used
* to implement natural sorting on java classes. natural sorting means the the sort
* order which naturally applies on object e.g. lexical order for String, numeric
* order for Integer or Sorting employee by there ID etc. most of the java core
* classes including String and Integer implements CompareTo() method and provide
* natural sorting.
*/
@Override
public int compareTo(PriorityFutureTask other) {
Priority p1 = priorityRunnable.getPriority();
Priority p2 = other.priorityRunnable.getPriority();
return p2.ordinal() - p1.ordinal();
}
}
}
First of all, in DefaultExecutorSupplier, instead of ThreadPoolExecutor, use PriorityThreadPoolExecutor like this:
public class DefaultExecutorSupplier{
private final PriorityThreadPoolExecutor mForBackgroundTasks;
private DefaultExecutorSupplier() {
mForBackgroundTasks = new PriorityThreadPoolExecutor(
NUMBER_OF_CORES * 2,
NUMBER_OF_CORES * 2,
60L,
TimeUnit.SECONDS,
backgroundPriorityThreadFactory
);
}
}
Here’s an example of how we can set HIGH priority to a task:
/*
* do some task at high priority
*/
public void doSomeTaskAtHighPriority(){
DefaultExecutorSupplier.getInstance().forBackgroundTasks()
.submit(new PriorityRunnable(Priority.HIGH) {
@Override
public void run() {
// do some background work here at high priority.
}
});
}
In this way, a task can be prioritized.
The above implementation is also applicable to any JAVA applications.
I hope this will be useful to you.
Prepare yourself for Android Interview: Android Interview Questions
That's it for now.
Thanks
Amit Shekhar
Co-Founder @ Outcome School
You can connect with me on:
Follow Outcome School on: