AsyncTask
Android development
concurrency
parallel processing
multithreading

Running multiple AsyncTasks at the same time -- not possible?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Many Android developers hit a confusing moment with AsyncTask: they start several tasks and see them run one by one. That behavior is expected on modern Android because execute uses a serial executor by default. You can run tasks in parallel, but you need to do it intentionally and safely.

How AsyncTask Execution Actually Works

AsyncTask has two execution styles. If you call execute, tasks are queued on a serial executor. If you call executeOnExecutor with THREAD_POOL_EXECUTOR, tasks may run concurrently.

java
1private static class DownloadTask extends AsyncTask<String, Void, String> {
2    @Override
3    protected String doInBackground(String... urls) {
4        String url = urls[0];
5        // Simulate network work
6        try { Thread.sleep(1200); } catch (InterruptedException ignored) {}
7        return "done: " + url;
8    }
9
10    @Override
11    protected void onPostExecute(String result) {
12        Log.d("DownloadTask", result);
13    }
14}
15
16new DownloadTask().execute("https://a.example.com");
17new DownloadTask().execute("https://b.example.com");

The two calls above are usually serialized. That is often good for stability, but it surprises people expecting parallel network requests.

Running Multiple Tasks in Parallel

If you need true concurrent work, use executeOnExecutor explicitly.

java
new DownloadTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "https://a.example.com");
new DownloadTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "https://b.example.com");
new DownloadTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "https://c.example.com");

This enables parallel execution, but concurrency creates new responsibilities:

  • protect shared mutable state
  • avoid updating UI from background threads
  • handle cancellation and lifecycle transitions

A safe pattern is to keep tasks independent and aggregate results in a thread-safe collection.

java
1private final List<String> results = Collections.synchronizedList(new ArrayList<>());
2
3private static class SafeTask extends AsyncTask<Integer, Void, String> {
4    private final List<String> sink;
5
6    SafeTask(List<String> sink) {
7        this.sink = sink;
8    }
9
10    @Override
11    protected String doInBackground(Integer... ids) {
12        int id = ids[0];
13        try { Thread.sleep(500L * id); } catch (InterruptedException ignored) {}
14        return "task-" + id;
15    }
16
17    @Override
18    protected void onPostExecute(String value) {
19        sink.add(value);
20    }
21}

Modern Replacement Strategy

AsyncTask is deprecated, so new code should prefer ExecutorService, Kotlin coroutines, or WorkManager depending on the workload.

For simple parallel background work in Java, ExecutorService is straightforward and test-friendly:

java
1ExecutorService pool = Executors.newFixedThreadPool(3);
2List<Callable<String>> jobs = List.of(
3    () -> { Thread.sleep(800); return "alpha"; },
4    () -> { Thread.sleep(300); return "beta"; },
5    () -> { Thread.sleep(600); return "gamma"; }
6);
7
8List<Future<String>> futures = pool.invokeAll(jobs);
9for (Future<String> future : futures) {
10    Log.d("Executor", future.get());
11}
12pool.shutdown();

For UI-heavy Android apps, coroutines with a ViewModel usually produce cleaner lifecycle-aware code than task classes tied to an Activity.

Choosing the Right Concurrency Primitive

Not every background job should be handled the same way. Pick based on task lifetime and reliability needs. For short in-app work triggered by the current screen, an app-level executor or coroutine scope is usually enough. For deferrable work that must survive app restarts, WorkManager is the safer choice because it persists constraints and retries.

A practical migration path is to wrap old AsyncTask use cases behind an interface, then replace implementations one flow at a time. This avoids a risky all-at-once rewrite and lets you validate behavior with instrumentation tests.

Common Pitfalls

  • Launching many parallel tasks for network calls can overload the server or hit app-level rate limits.
  • Holding a direct Activity reference inside a task can leak memory during rotation. Use weak references or lifecycle-aware components.
  • Assuming task completion order is equal to start order is wrong for concurrent execution. Always handle out-of-order results.
  • Ignoring cancellation leads to wasted work and stale UI updates. Check isCancelled in background loops.
  • Continuing to build new features on AsyncTask increases migration cost later. Prefer modern primitives for new development.

Summary

  • Multiple AsyncTask calls are often serialized when using execute.
  • Parallel behavior requires executeOnExecutor and explicit concurrency control.
  • Thread-safe result handling and lifecycle awareness are mandatory for stability.
  • ExecutorService, coroutines, and WorkManager are better long-term choices.
  • Treat concurrency as a design concern, not only a syntax change.

Course illustration
Course illustration

All Rights Reserved.