ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

Facing a Multithreading Tech Interview

Lessons from a Live Coding Session

Andrii Veremiienko
ProAndroidDev
Published in
4 min readOct 14, 2024

Motivation

I’ve recently gone through several interesting tech interviews and one of the new challenges for me was a multithreading interview. It was a one-hour live-coding session where I had to solve typical Android multithreading issues. Even though I have experience with threads and coroutines, it was still a challenge. The main reason for this was my lack of practical experience in solving such tasks.

This article will be helpful for mid-level and junior developers who haven’t had extensive experience with multithreading or whose tasks have mainly involved simple GET/POST requests in a coroutine launch block (nothing wrong with that!). In this article, I present a more complex example to help you improve your skills and better prepare for similar interviews.

En avant!

Initial Implementation

The entire interview consisted of a series of tasks centered around multithreading and code optimization in a single example. One of the requirements was that all code should be written in a single class and function, so let’s focus on the key concepts. The code can always be refactored later.

Here is the initial code.

As you can see, this implementation attempts to download an image on the main thread, which is problematic. Afterward, the image is set to the imageView. Not good. The first task was to identify and resolve these issues.

Base Implementation

Initially, I downloaded the image on a background thread and then set the bitmap on the main thread (since the UI can only be updated from the main thread). My first thought was, “Ha, that was much easier than I imagined!” But, of course, it was just the beginning…

What if we want to change the previous image?

The next task was to stop an ongoing download process and start a new one. This can easily be done by canceling the current job.

It’s important to note, that even though we trigger cancellation, it only takes effect within a suspended block. In our case, this happens when we switch contexts using the withContext function, as I mentioned during the interview.

Let’s Add Some Cache

Another task was to implement a cache. I chose ConcurrentHashMap because it allows for fast access to entries, and we have unique keys in the form of URLs. Additionally, using ConcurrentHashMap ensures thread safety, which is important in a multithreaded environment. Now, when we attempt to download the same image, it will be retrieved from the cache without needing to re-download it.

One important aspect of the code below is that we double-check and save the bitmap only if the image wasn’t downloaded previously.

It was my original solution, I kept it unchanged. But after a brief discussion with ChatGPT, I realized that there still could be a race condition if multiple threads reach the same point simultaneously. As a better solution, I was recommended to use the following method, which enhances atomicity and reduces the number of lines:

val bitmap = hashMap.computeIfAbsent(url) {
loadBitmap(url)
}

However, there is still one issue that my interviewer politely brought to my attention: what if two or more requests try to download the same image simultaneously? The answer is that if hashMap is empty, all threads will initiate their own downloading processes, leading to multiple requests for the same image.

Let’s add some cache but now with async

This was the most challenging part for me, but I eventually found a solution. We can use coroutines with the async builder to handle future results. By saving the Deferred value in our cache, we can return it whenever we need the same image.

Now, there’s no need to wait for the image to download each time. We simply retrieve the existing Deferred object and wait until the image becomes available. This approach not only reduces redundant network calls but also improves the overall efficiency of our application.

Retry Policies

The next task was to add retry policies. Initially, my mind was overwhelmed by the previous challenge, and I started overthinking the solution. Then my interviewer pointed out, “You are thinking too much,” and at that moment, the simplest approach to adding the retry functionality became clear to me.

Restricting the Number of Threads

The final question was about how to restrict the number of threads used for these operations. Fortunately, this one was quite straightforward.

private val scope = CoroutineScope(newFixedThreadPoolContext(3, "Pool"))

Conclusion

As you can see, it wasn’t a particularly difficult interview, and I was pleased that I found solutions and implemented them for each of the tasks on my own.

I hope this article has been useful and that some readers have gained new insights. Good luck with your interviews!

P.S. Any suggestions for improvement are always welcome!

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Responses (2)

Write a response

Thanks for a detailed and thoughtful article.
Looks like I attended the same interview :)
I would add that it’s worth to initialize CoroutineScope with SupervisorJob. It prevents cancellation of the entire scope when one of the children coroutines…

--

Important note from Chat GPT:
The thread pool created with newFixedThreadPoolContext should be closed to avoid resource leaks. It is better to use modern alternatives such as Dispatchers.IO or Dispatchers.Default, which are more optimized and managed automatically by the system.

--