Threads are the cornerstone of any multitasking operating system and can be thought of as mini-processes running within a main process, the purpose of which is to enable at least the appearance of parallel execution paths within applications. In this chapter we will explore the importance of using threads in Android app development and demonstrate how they are created and managed.
The Application Main Thread
When an Android application is first started, the runtime system creates a single thread in which all application components will run by default. This thread is generally referred to as the main thread. The primary role of the main thread is to handle the user interface in terms of event handling and interaction with views in the user interface. Any additional components that are started within the application will, by default, also run on the main thread.
Any component within an application that performs a time consuming task using the main thread will cause the entire application to appear to lock up until the task is completed. This will typically result in the operating system displaying an “Application is not responding” warning to the user. Clearly, this is far from the desired behavior for any application. This can be avoided simply by launching the task to be performed in a separate thread, allowing the main thread to continue unhindered with other tasks.
Thread Handlers
Clearly, one of the key rules of Android development is to never perform time-consuming operations on the main thread of an application. The second, equally important, rule is that the code within a separate thread must never, under any circumstances, directly update any aspect of the user interface.
Any changes to the user interface must always be performed from within the main thread. The reason for this is that the Android UI toolkit is not thread-safe. Attempts to work with non-thread-safe code from within multiple threads will typically result in intermittent problems and unpredictable application behavior.
If the code executing in a thread needs to interact with the user interface, it must do so by synchronizing with the main UI thread. This is achieved by creating a handler within the main thread, which, in turn, receives messages from another thread and updates the user interface accordingly.
A Threading Example
The remainder of this chapter will work through some simple examples intended to provide a basic introduction to threads. The first step will be to highlight the importance of performing time-consuming tasks in a separate thread from the main thread.
Launch Android Studio, select the New Project option from the welcome screen and, within the resulting new project dialog, choose the Empty Activity template before clicking on the Next button.
Enter ThreadExample into the Name field and specify com.ebookfrenzy.threadexample as the package name. Before clicking on the Finish button, change the Minimum API level setting to API 26: Android 8.0 (Oreo) and the Language menu to Java. Convert the project to use view binding by following the steps in section 11.8 Migrating a Project to View Binding.
Building the App
Load the activity_main.xml file for the project into the Layout Editor tool. Select the default TextView component and change the ID for the view to myTextView in the Properties tool window.
Add a Button view to the user interface positioned directly beneath the existing TextView object as illustrated in Figure 59-1. Once the button has been added, click on the Infer Constraints button in the toolbar to add the missing constraints.
Change the text to “Press Me” and extract the string to a resource named press_me. With the button view still selected in the layout, locate the onClick property and enter buttonClick as the method name.
Next, load the MainActivity.java file into an editing panel and add code for the buttonClick() method which will be called when the Button view is tapped by the user. Since the goal here is to demonstrate the problem of performing lengthy tasks on the main thread, the code will simply pause for 20 seconds before displaying different text on the TextView object:
.
.
public void buttonClick(View view) {
long endTime = System.currentTimeMillis() + 20 * 1000;
while (System.currentTimeMillis() < endTime) {
synchronized (this) {
try {
wait(endTime - System.currentTimeMillis());
} catch (Exception e) {
}
}
}
binding.myTextView.setText("Button Pressed");
}
.
.
Code language: Java (java)
With the code changes complete, run the application on either a physical device or an emulator. Once the application is running, tap the button, at which point the application will appear to freeze. It will, for example, not be possible to touch the button a second time and in some situations the operating system will report the application as being unresponsive as shown in Figure 59-2.
Clearly, anything that is going to take time to complete within the buttonClick() method needs to be performed within a separate thread.
Creating a New Thread
To create a new thread, the code to be executed in that thread needs to be placed within the run() method of a Runnable instance. A new Thread object then needs to be created, passing through a reference to the Runnable instance to the constructor. Finally, the start() method of the thread object needs to be called to start the thread running. To perform the task within the buttonClick() method, therefore, the following changes need to be made:
public void buttonClick(View view) {
Runnable runnable = new Runnable() {
public void run() {
long endTime = System.currentTimeMillis() + 20 * 1000;
while (System.currentTimeMillis() < endTime) {
synchronized (this) {
try {
wait(endTime - System.currentTimeMillis());
} catch (Exception e) {
}
}
}
}
binding.myTextView.setText("Button Pressed");
};
Thread myThread = new Thread(runnable);
myThread.start();
Code language: Java (java)
In fact, the runnable declaration can be simplified if desired by making use of a Java lambda expression. Making this change would result in the following declaration:
.
.
Runnable runnable = () -> {
long endTime = System.currentTimeMillis() + 20 * 1000;
while (System.currentTimeMillis() < endTime) {
synchronized (this) {
try {
wait(endTime - System.currentTimeMillis());
} catch (Exception e) {
}
}
}
};
Thread myThread = new Thread(runnable);
myThread.start();
.
.
Code language: Java (java)
When the application is now run, touching the button causes the delay to be performed in a new thread leaving the main thread to continue handling the user interface, including responding to additional button presses. In fact, each time the button is touched, a new thread will be created, allowing the task to be performed multiple times concurrently.
A close inspection of the updated code for the buttonClick() method will reveal that the code to update the TextView has been removed. As previously stated, updating a user interface element from within a thread other than the main thread violates a key rule of Android development. To update the user interface, therefore, it will be necessary to implement a Handler for the thread.
Implementing a Thread Handler
Thread handlers are implemented in the main thread of an application and are primarily used to make updates to the user interface in response to messages sent by other threads running within the application’s process.
Handlers are subclassed from the Android Handler class and can be used either by specifying a Runnable to be executed when required by the thread, or by overriding the handleMessage() callback method within the Handler subclass which will be called when messages are sent to the handler by a thread.
For the purposes of this example, a handler will be implemented to update the user interface from within the previously created thread. Load the MainActivity.java file into the Android Studio editor and modify the code to add a Handler instance to the activity:
.
.
import android.os.Handler;
import android.os.Message;
import android.os.Looper;
public class MainActivity extends AppCompatActivity {
.
.
Handler handler = new Handler(Looper.getMainLooper()) {
@Override public void handleMessage(Message msg) {
binding.myTextView.setText("Message Received");
}
};
.
.
Code language: Java (java)
The above code changes have declared a handler and implemented within that handler the handleMessage() callback which will be called when the thread sends the handler a message. In this instance, the code simply displays a string on the TextView object in the user interface.
All that now remains is to modify the thread created in the buttonClick() method to send a message to the handler when the delay has completed:
public void buttonClick(View view) {
Runnable runnable = () -> {
long endTime = System.currentTimeMillis() + 20 * 1000;
while (System.currentTimeMillis() < endTime) {
synchronized (this) {
try {
wait(endTime - System.currentTimeMillis());
} catch (Exception e) {
}
}
}
handler.sendEmptyMessage(0);
}
};
Thread myThread = new Thread(runnable);
myThread.start();
}
Code language: Java (java)
Note that the only change that has been made is to make a call to the sendEmptyMessage() method of the handler. Since the handler does not currently do anything with the content of any messages it receives it is sent an empty message object.
Compile and run the application and, once executing, touch the button. After a 20 second delay, the new text will appear in the TextView object in the user interface.
Passing a Message to the Handler
While the previous example triggered a call to the handleMessage() handler callback, it did not take advantage of the message object to send data to the handler. In this phase of the tutorial, the example will be further modified to pass data between the thread and the handler. First, the updated thread in the buttonClick() method will obtain the date and time from the system in string format and store that information in a Bundle object. A call will then be made to the obtainMessage() method of the handler object to get a message object from the message pool. Finally, the bundle will be added to the message object before being sent via a call to the sendMessage() method of the handler object:
public void buttonClick(View view) {
Runnable runnable = () -> {
long endTime = System.currentTimeMillis() + 20 * 1000;
while (System.currentTimeMillis() < endTime) {
synchronized (this) {
try {
wait(endTime - System.currentTimeMillis());
} catch (Exception e) {
}
}
}
Message msg = handler.obtainMessage();
Bundle bundle = new Bundle();
bundle.putString("myKey", "Thread Completed");
msg.setData(bundle);
handler.sendMessage(msg);
};
Thread myThread = new Thread(runnable);
myThread.start();
}
Code language: Java (java)
Next, update the handleMessage() method of the handler to extract the date and time string from the bundle object in the message and display it on the TextView object:
Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
Bundle bundle = msg.getData();
String string = bundle.getString("myKey");
binding.myTextView.setText(string);
}
};
Code language: Java (java)
Finally, compile and run the application and test that touching the button now causes the “Thread Complete” message to appear on the TextView object after the thread finishes.
Java Executor Concurrency
So far in this chapter we have looked exclusively at directly creating and managing Java threads. While acceptable for simple multi-threading tasks, this can prove to be inadequate when working with complex situations involving large number of threads. There is, for example, a system overhead involved in starting and stopping threads. An app that creates and destroys large number of threads is, therefore, at risk of exhibiting degraded performance. The basic threading API also does not provide pre-built options for scheduling or repeating task execution, or for returning results from a task.
The shortcomings of working directly with threads can be overcome by making use of the Executor classes of the Java Concurrent framework (part of the java.util.concurrent package). This framework allows for a pool of active threads to be created and manages how tasks are assigned to those threads. This allows existing threads to be reused for other tasks without the need to constantly create new threads.
This framework also provides additional functionality including the ability to return a result on completion of a task (referred to as a Callable task), check the status of a thread and to schedule tasks to run either after a timeout or at repeated time intervals.
Working with Runnable Tasks
The first step in exploring this framework is to modify the buttonClicked() method to use a concurrency framework Executor to run the task in a separate thread:
.
.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
.
.
public void buttonClick(View view) {
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(new Runnable() {
public void run() {
long endTime = System.currentTimeMillis() + 10 * 1000;
while (System.currentTimeMillis() < endTime) {
synchronized (this) {
try {
wait(endTime - System.currentTimeMillis());
} catch (Exception e) {
}
}
}
Message msg = handler.obtainMessage();
Bundle bundle = new Bundle();
bundle.putString("myKey", "Button Pressed");
msg.setData(bundle);
handler.sendMessage(msg);
}
});
executor.shutdown();
}
.
.
Code language: Java (java)
When the above code is executed, the timeout will be performed on a separate thread as before. The changes made to the method, however, require some explanation. First, a reference to an ExecutorService instance is obtained from the system Executors instance:
ExecutorService executor = Executors.newSingleThreadExecutor();
Code language: Java (java)
In this case, a pool containing only one thread is requested. A pool with a specified number of threads could have been requested as follows:
ExecutorService executor = Executors.newFixedThreadPool(10);
Code language: Java (java)
Next, a Runnable task is started on the thread via a call to the submit() method of the executor service instance:
executor.submit(new Runnable(){
public void run(){
long endTime = System.currentTimeMillis() + 20 * 1000;
.
.
Code language: Java (java)
Note that the above declaration can be simplified by converting it to a lambda as follows:
executor.submit(() -> {
long endTime = System.currentTimeMillis() + 20 * 1000;
.
.
Code language: Java (java)
From this point on, the task will run until completion. Once completed however, the executor service will continue to run. If you have no further use for the service, it should be shutdown.
Shutting down an Executor Service
ExecutorService provides a few techniques for initiating a shutdown. To notify the service that it should shutdown automatically after the currently running tasks have reached completion, a call to the shutdown() method should be made as follows:
executor.shutdown();
Code language: Java (java)
A call to the shutdownNow() method, on the other hand, stops all tasks running on the service and, cancels the processing of pending tasks:
executor.shutdownNow();
Code language: Java (java)
Working with Callable Tasks and Futures
As previously mentioned, the ExecutorService supports so called “Callable” tasks which are able to return a result after the task is completed. Tasks running on separate thread are typically expected to take some time to complete (otherwise they probably would not need to run on a separate thread in the first place). This raises the question of how the result is returned to the code in the thread from which the task was launched. This is achieved using the Future value type which represents a value which will be provided at some point in the future.
When a callable task is executed it returns a Future instance which may then be used by the app to obtain the result when the task completes. To see this in action, begin by editing the activity_main.xml file to add an additional button labeled “Status” with the onClick property configured to call method named statusClick:
Next, modify the buttonClick() method to execute a Callable task configured to return a String value via a Future variable:
.
.
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
.
.
public class MainActivity extends AppCompatActivity {
Future<String> future;
.
.
public void buttonClick(View view) {
ExecutorService executor = Executors.newSingleThreadExecutor();
future = executor.submit(new Callable<String>() {
public String call() {
long endTime = System.currentTimeMillis() + 10 * 1000;
while (System.currentTimeMillis() < endTime) {
synchronized (this) {
try {
wait(endTime - System.currentTimeMillis());
} catch (Exception e) {
}
}
}
return("Task Completed");
}
});
executor.shutdown();
}
.
.
Code language: Java (java)
Note that in addition to importing the java.util.concurrent.Future package and declaring a Future variable for storing a string value, some changes have also been made to the way in which the task is launched:
future = executor.submit(new Callable<String>() { public String call() {
The key points here are that instead of submitting a Runnable task to the executor service, we are now passing through a Callable task (declared to return a String value). Note also that the result of the task is assigned to the Future variable. In addition, call() is used instead of the run() method used previously when submitting a Runnable task. Finally, a return statement has been added to return a string value:
return("Task Completed")
Code language: Java (java)
Handling a Future Result
The buttonClick() method is now configured to launch a Callable task with the return value assigned to the Future variable. The app now needs to know when the task is complete and the result available. One option is to call the get() method of the Future variable. Since this method is able to throw exceptions if the execution fails or is interrupted, this must be performed in a try/catch statement as follows:
String result = null;
try {
result = future.get();
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
Code language: Java (java)
Unfortunately, the get() method will block the current thread until the task running in the thread completes, thereby defeating the purpose of running the task in a separate thread in the first place. Another option is to provide the get() method call with a timeout after which it will return control to the current thread. The following code, for example, will cause the get() call to timeout after 3 seconds:
result = future.get(3, TimeUnit.SECONDS);
Code language: Java (java)
A better alternative, however, is to call the isDone() method of the Future instance to check the status of the thread and only call the get() method once the task is complete. To implement this behavior, add the statusClick() method to the MainActivity.java file as follows:
.
.
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
.
.
public void statusClick(View view) {
if ((future != null && future.isDone()) {
String result = null;
try {
result = future.get(3, TimeUnit.SECONDS);
} catch (ExecutionException | InterruptedException
| TimeoutException e) {
e.printStackTrace();
}
binding.myTextView.setText(result);
} else {
binding.myTextView.setText("Waiting");
}
}
Code language: Java (java)
With the changes made, run the app and click on the “Press Me” button. While the task is running, click on the Status button. As long as the task is still running, the “Waiting” message will be displayed in the TextView. Once the task completes, however, the isDone() method will return a true value, the get() method will be called and the string returned by the task (“Task Complete”) displayed on the TextView.
Scheduling Tasks
The final area to be covered involves the use of ExecutorService to schedule task execution. This involves use of a ScheduledExecutorService instance on which the schedule() method needs to be called passing through the Runnable task to be executed together with a time delay. The schedule() call will return a ScheduledFuture instance which may be used to identify the remaining time before the task is due to start.
The following code, for example, schedules a task to run after a 30 second delay and accesses the remaining delay time:
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
Runnable task = () -> {
// Code to perform task here
};
ScheduledFuture<?> future = executor.schedule(task, 30, TimeUnit.SECONDS);
long delayRemaining = future.getDelay(TimeUnit.SECONDS);
Code language: Java (java)
Similarly, the ScheduledExecutorService may be used to execute a task repeatedly at regular intervals starting after an optional initial delay. The following code, for example, causes a task to be performed every 10 seconds after an initial 30 second delay:
executor.scheduleAtFixedRate(task, 30, 10, TimeUnit.SECONDS);
Code language: Java (java)
The scheduleAtFixedRate() method will launch the next instance of the task regardless of whether or not the previously scheduled task has completed. To specify a fixed period of time between the end of a task execution and the start of the next execution, use the scheduleWithFixedDelay() method. In the following example, the first task is scheduled after a 0 second delay, with each subsequent execution taking place 10 seconds after completion of the proceeding task:
executor.scheduleWithFixedDelay(task, 0, 10, TimeUnit.SECONDS);
Code language: Java (java)
Summary
The goal of this chapter was to provide an overview of threading within Android applications. When an application is first launched in a process, the runtime system creates a main thread in which all subsequently launched application components run by default. The primary role of the main thread is to handle the user interface, so any time consuming tasks performed in that thread will give the appearance that the application has locked up. It is essential, therefore, that tasks likely to take time to complete be started in a separate thread.
Because the Android user interface toolkit is not thread-safe, changes to the user interface should not be made in any thread other than the main thread. User interface changes can be implemented by creating a handler in the main thread to which messages may be sent from within other, non-main threads.
Threads can be created either directly, or using the executor services of the Java Concurrent framework. For more complex threading requirements, this framework provides automatic management of thread pools, returning of results from tasks and execution scheduling.