Module 1: Introduction to Threads

Learning Objectives

  • Explain why concurrency is more likely to help an I/O-bound application than a CPU-bound application
  • Explain what triggers a Java Thread to transition states from RUNNABLE to BLOCKED
  • Explain what triggers a Java Thread to transition states from BLOCKED to RUNNABLE
  • Explain what triggers a Java Thread to transition states from RUNNABLE to WAITING
  • Explain what triggers a Java Thread to transition states from WAITING to RUNNABLE
  • Explain what triggers a Java Thread to transition states from RUNNABLE to TERMINATED
  • Examine whether a described scenario is taking advantage of concurrency
  • Examine whether running a given code snippet has the potential to suffer from a race condition
  • Use Thread's sleep method to pause execution for a specified period of time
  • Design and implement a class that implements Runnable to execute functionality concurrently
  • Explain threads and how they can provide concurrency
  • Explain concurrency and why it can be valuable
  • Discuss when to use concurrency
  • Explain what a race condition is
  • Explain what Thread's sleep method is and when to use it
  • Explain how to use Thread's sleep method

Key Topics

Thread Basics

Learn about the fundamentals of Java threads.

  • Thread lifecycle
  • Creating threads
  • Thread vs. Runnable

Thread Management

Understand how to control thread execution.

  • Starting and stopping threads
  • Thread priority
  • Thread joining

Synchronization

Learn about thread safety and coordination.

  • Synchronized methods
  • Synchronized blocks
  • Race conditions

Common Challenges

Identify and solve threading issues.

  • Deadlocks
  • Thread starvation
  • Thread safety strategies

Introduction to Threading

Threads allow Java applications to perform multiple operations concurrently. A thread is a lightweight process that can execute independently while sharing the same resources as other threads within a process.

In this module, we'll explore how to create and use threads in Java, understand thread lifecycle, and learn about common challenges in concurrent programming.

When developing programs, you always start with one execution path (the main method). In Java, this is the main method, the place where all code is executed. However, as applications become more complex, using a single thread can create bottlenecks. Oftentimes, when programs become larger-scale and more complex, you may find that having all the code execute from one stream slows things down.Multithreading allows you to create additional execution paths that run concurrently, improving performance particularly for I/O-bound operations.

This is where multithreading (or "threading") comes in. This is functionality available in Java and in many other programming languages that lets you create additional paths of execution for your code. These execution paths are commonly referred to as threads. Threading is very useful in removing bottlenecks when your program is running. For instance, if your program needs to read data from a file on disk, the program will only move as fast as the hardware in the computer. In a single thread, this could slow down the program if that data is needed to progress to the next step. With threading, we can have the program read the data in a new thread, and then continue progressing in the main thread without any slowdown.

Think about threading as if comparing a road and a highway. Roads allow one or two streams of cars to pass through but can get congested if they're located in areas with high traffic. Highways solve that problem by allowing multiple streams of cars through, which not only clears up congestion but also lets cars go faster than on roads. Threading does the same thing and more, because not only does it allow us to spread the workload around, but it also lets us control where and when code executes. We'll get into this in more detail in later lessons, but first let us understand how to start using threads first.

When to do threading and concurrency

The first thing to understand about threading is in what situations we can use it. As mentioned above, threads are useful for speeding up programs, especially when developing code that uses Input/Output (I/O) functionality. When running a program, most of the computation happens within the CPU, iterating (repeating instructions) through an array for example. However, when we develop I/O-bound applications it is very common for the program to slow down as it waits for other components of the hardware to complete their tasks before continuing. By using threading, we can divert these processes so that code that is I/O-bound happens in a new thread, and CPU-bound code that is not reliant on it can progress.

I/O-bound applications are not the only places where we can utilize threading. In large-scale programs it may be beneficial to break down the code into multiple threads. This technique is called "concurrency." Concurrency lets you take advantage of more processing power as each thread can use a different processor core in the CPU, and thus make the program more efficient. Keep this in mind when developing! However, overusing threads will make your program too resource-heavy and negate all the benefits. Think about how to maximize concurrency effectively.

A good way to understand where concurrency can be used is to examine an application and think about what processes are happening, and which of those processes can be separated out from the main thread. For instance, think about an internet browser on an Amazon Fire Tablet. The browser probably has threads for connecting to the internet, loading an internet page, loading the media from the page, and formatting everything on that page. Not to mention the threads managing the touchscreen controls, the background processes, and how many websites to keep in memory. When designing systems concurrently, breaking everything down into discrete processes becomes a vital skill.

How to use threads

Let's learn the bare-bones about creating a new thread. There are two main methods to make a new thread object: creating a subclass of java.lang.Thread, or making a Runnable interface. We'll be learning how to make a Runnable interface in the next reading, so for now let's start with making a subclass. The code below demonstrates how to do that:

public class CargoDemo {
    public static void main (String [] args){
        System.out.println("Main thread running");
        CargoManager cargo1 = new CargoManager();
        cargo1.start();
    }
}
public class CargoManager extends Thread {
    public void run(){
        System.out.println("CargoManager thread running");
    }
}

Output:

Main thread running
CargoManager thread running

You'll notice that we only make one thread object but there are two threads running concurrently, the main thread and CargoManager! We've been working with the main thread all along, but you can also see how easy it is to create a new thread. We have the CargoManager class extend java.lang.Thread, and then override the thread class's run() method which contains all the code the thread executes. Once we have our thread class, we simply need to create an instance of it in main, cargo1. Then we need to start it by calling cargo1.start().

It's a common beginner mistake to call run() instead of start() to begin the new thread. The start()call actually begins the thread and calls run(), while run() just executes the code. Calling run() in main will just execute the code in the main thread instead of the CargoManager thread, so it's important to get that right!

In the next reading we will go deeper into thread creation by learning how to make a Runnable interface, and how we can implement threads concurrently in projects. We just need to understand the basics for now.

Thread states

Now that we are creating threads, let's look at how we can get some information out of them to understand how they run. Every thread in Java, including the main one, has a state we can access to see its status. This is primarily for debugging purposes. Adding onto the code snippet above, we can get and print the state of the thread via Thread.getState().

public static void main (String [] args){
    System.out.println("Main thread running");
    CargoManager cargo1 = new CargoManager();
    System.out.println(cargo1.getState());
    cargo1.start();
    System.out.println(cargo1.getState());
}

Output:

Main thread running
NEW
RUNNABLE
CargoManager thread running

We see two outputs: NEW and RUNNABLE. The first println() will return NEW because the thread exists but hasn't been started yet. The second println() will return RUNNABLE since the thread is now started. (Note that "CargoManager thread running" will print at the same instant from cargo1!)

Below is a list of the states we will be covering in this lesson. Some of them are related to topics that we will cover more deeply in future lessons, but it is useful to introduce them here.

  • NEW: The thread when created. It exists but hasn't been started yet.
  • RUNNABLE: Usually when the thread is running. Sometimes it only means it is ready to run, but most of the time getting this state means it's currently active and executing.
  • BLOCKED: The thread enters this state from RUNNABLE when it is blocked from obtaining a lock which is already locked. The blocked thread won't enter back into RUNNABLE until the JVM grants it the lock. Typically, we use locks to prevent multiple threads from acting on a shared resource simultaneously. We will cover locking in a later lesson on synchronization and thread safety.
  • WAITING: The thread enters this state from RUNNABLE when it calls wait() on an object. It remains in this state until notified from another thread, and then reenters RUNNABLE.
  • TERMINATED: When a thread has exited. A thread ends when it is no longer able to execute code, either by completing its task or by an error, no matter the reason. It's important to note that WAITING and BLOCKED can mean that the thread has stopped progressing in its task, but hasn't ended.

Conclusion

In this lesson we learned how to extend the Thread class as well as essential thread states. For the next reading, we'll broaden our knowledge by implementing a Runnable interface, which gives us more programming flexibility.

Creating Threads in Java

There are two primary ways to create threads in Java:

// Method 1: Extending Thread class
public class MyThread extends Thread {
    public void run() {
        System.out.println("Thread is running");
    }
}

// Usage
MyThread thread = new MyThread();
thread.start();

// Method 2: Implementing Runnable interface
public class MyRunnable implements Runnable {
    public void run() {
        System.out.println("Thread is running");
    }
}

// Usage
Thread thread = new Thread(new MyRunnable());
thread.start();

Remember to always call start() rather than run() directly. Calling start() creates a new thread that executes the run() method, while calling run() directly just executes the method in the current thread.

Thread Lifecycle States

  • NEW: Thread exists but hasn't been started yet
  • RUNNABLE: Thread is actively running or ready to run
  • BLOCKED: Thread is waiting to acquire a lock
  • WAITING: Thread is waiting indefinitely for another thread to perform an action
  • TERMINATED: Thread has completed execution or been stopped

You can check a thread's state using thread.getState() method.

Runnable Interfaces and Error Checking

Thread interfaces

In the last lesson we learned how to implement threads by extending the Thread class. However, we can gain greater functionality out of threads and concurrency by implementing a Runnable interface. Runnable not only lets us use threads but also allows us to use inheritance, which frees us to do much more than make a subclass of Thread.

In the code below, see how we're able to inherit using Runnable:

public class RunnableDemo {

    public static void main (String [] args) {
        System.out.println("Main Thread running");
        CargoManager cargo1 = new CargoManager();
        cargo1.cargoPrint(3);
        Thread cargoThread = new Thread(cargo1);
        cargoThread.start();
    }

}

public class CargoTracking {

    public void cargoPrint(int num) {
        System.out.println("There are "+ num +" pallets of cargo.");
    }

}

public class CargoManager extends CargoTracking implements Runnable {

    public CargoManager() {
        super();
        System.out.println("CargoManager created");
    }

    public void run() {
        System.out.println("CargoManager running");
    }

}

Output:

Main Thread running
CargoManager created
There are 3 pallets of cargo.
CargoManager running

By declaring implements Runnable on CargoManager and implementing the public void run() method, we are able to use CargoManager like a regular class, and then pass it into a new Thread object when we want to run the thread. You'll note that we still override a run() method like when we made a subclass of Thread. Generally, it is a good practice to use Runnable when creating threads, as opposed to making a thread subclass. Even if you don't need to inherit a class, Runnable leaves the option open without too much fuss.

In the code below, we don't extend CargoTracking and can still run it in a thread:

public class RunnableDemo {

    public static void main (String [] args) {
        System.out.println("Main Thread running");
        Thread cargoThread = new Thread(new CargoManager());
        cargoThread.start();
    }

}

public class CargoManager implements Runnable {

    public CargoManager() {
        System.out.println("CargoManager created");
    }

    public void run() {
        System.out.println("CargoManager running");
    }

}

Output:

Main Thread running
CargoManager created
CargoManager running

Understanding race conditions

Now that we're able to create threads, we should understand the bugs that can pop up by using them. Look at the code below, we are tracking cargo being shipped and delivered from a warehouse. We first get a count of the inventory of cargo at the start of the day, then run threads for shipping cargo out and receiving deliveries. The final step is printing out what cargo we have at the end of the day.

public class CargoDemo {
    public static int currentInventory = 100;

    public static void main(String [] args) {
        System.out.println("Start of day inventory: " + currentInventory);
        Thread shippingThread = new Thread(new ShippingManager());
        Thread deliveryThread = new Thread(new DeliveryManager());
        shippingThread.start();
        deliveryThread.start();

        //Final count:
        System.out.println("End of day inventory: " + currentInventory);
    }
}

public class DeliveryManager implements Runnable {

    public void run() {
        CargoDemo.currentInventory += 300;
        System.out.println("DeliveryManager inventory: " + CargoDemo.currentInventory);
    }
}

public class ShippingManager implements Runnable {

    public void run() {
        CargoDemo.currentInventory -= 100;
        System.out.println("ShippingManager inventory: " + CargoDemo.currentInventory);
    }

}

(Possible) Run 1:

Figure 1

Figure 1: Sequence diagram for one possibility when running the main() method of CargoDemo First, the main thread creates the new threads for ShippingManager and DeliveryManager. These threads are then run in parallel. The main thread finishes first, followed by the ShippingManager thread, and finally the DeliveryManager thread.

// run 1 output
Start of day inventory: 100
End of day inventory: 100
ShippingManager inventory: 0
DeliveryManager inventory: 300

(Possible) Run 2:

Figure 2

Figure 2: Sequence diagram for one possibility when running the main() method of CargoDemo First, the main thread creates the new threads for ShippingManager and DeliveryManager. These threads are then run in parallel. The DeliveryManager thread finishes first, followed by the ShippingManager thread, and finally the main thread.

// run 2 output
Start of day inventory: 100
DeliveryManager inventory: 400
ShippingManager inventory: 300
End of day inventory: 300

Take a look at these two possible outputs for the main method. In each, we have four counts of CargoDemo.currentInventory, but you'll notice the math seems different between the two runs, resulting in different totals depending on the order in which the threads complete.

Race Conditions are a common issue in software development when multiple threads share the same information or state. In a run condition, the program's behavior is dependent on the timing or sequence of events. They appear in many situations where order is not guaranteed: distributed systems often face race conditions, as do programs like ours that execute concurrent threads. One very common race condition is reading a value before the value is written or updated, where reading and writing happen independently.

Our ShippingManager and DeliveryManager are manipulating a shared resource, our currentInventory. The race condition we have here is that we are expecting all these tasks to happen in a certain order, but because the Managers execute concurrently, we don't have control over when the calculations and printing occur.

When we start the threads, they immediately start and execute their code independently from one another. When they print or calculate currentInventory, it is the value the thread currently read at that moment. Because of this blind spot, the threads are all concurrently overriding currentInventory when they read and write to it. We don't get a consistent result each time we run the program, much less the result we want!

It is important to note that this code will not throw an exception! Race conditions often appear as unexpected results, not as exceptions. Usually the only exceptions from race conditions are when two incompatible actions happen at the same time from a concurrent setup, such as two threads reading the same file. Whenever you develop concurrently, make sure you know what is happening to any data being handled, inside the threads and out.

In later lessons we will cover how to lock objects and files from a thread, which helps mitigate race conditions. But for now, we'll cover some basic methods to prevent race conditions.

Sleep and waiting

Race conditions can be resolved by ensuring that threads execute in a specific order. In later lessons, we'll learn reliable ways to pause a thread while it waits for a condition to occur. In this reading, purely for demonstrating that restricting the order prevents race conditions, we'll use a very simple approach: Thread.sleep().

Thread.sleep() is a method that can help control when events happen by waiting between them. sleep() will pause the current thread for the number of milliseconds passed into it. Since we can't predict how long a thread will take, we also can't guarantee how long we should sleep() to ensure that a thread completes its work before we start our own. These problems make it so sleep() is rarely the optimal way to address race conditions, and you won't likely see it used to solve them in production code, but it will do for our example.

Taking the main method from above, we can address the race condition by making our main method pause, or "sleep" after starting the threads for two seconds. As mentioned, the amount of time is arbitrary and there's no guarantee the threads will actually finish within that time, but it's enough to affect our example code's execution order.

Finally, we also need to declare the checked InterruptedException which can be thrown if the thread is interrupted. (We'll dive further into interrupting threads at a later time.)

The updated code looks like:

public static void main (String [] args) throws InterruptedException {
    System.out.println("Start of day inventory: " + currentInventory);
    Thread shippingThread = new Thread(new ShippingManager());
    Thread deliveryThread = new Thread(new DeliveryManager());

    shippingThread.start();
    Thread.sleep(2000); // 2000 milliseconds = 2 seconds
    deliveryThread.start();
    Thread.sleep(2000);

    //Final count:
    System.out.println("End of day inventory: " + currentInventory);
}

Output:

Start of day inventory: 100
ShippingManager inventory: 0
DeliveryManager inventory: 300
End of day inventory: 300

With the calls to sleep, we're forcing our main method to pause execution until the specified amount of time passes after starting each Manager's thread. This effectively makes our concurrent code single threaded since we will start the threads and pause until they execute, but we did address the problem of inconsistent sequence of events when reading and updating our currentInventory variable. The downside of using sleep is there's no guarantee the events will always take specified time to complete. Java provides more robust ways to address race conditions in multi-threaded code which we will explore in future lessons.

The important concepts to note with Thread.sleep() are that it only pauses the thread that the method is called in, and that all it does is pause. In this example, Thread.sleep(2000) is pausing the main java thread for two seconds while the other threads continue running.

What is sleep good for?

We've learned about sleep and how it's an unreliable way to address race conditions, but sleep is a handy tool to apply to other situations. A common pattern is for a program to sleep for a fixed amount of time while waiting for something to happen.

One example is checking the shipping status of an Amazon package. If it hasn't been shipped yet, our program could sleep for a fixed amount of time before checking again; there's no need use up resources to immediately check the status again. We can even count how many times we've checked and how long we've waited, and if it's more than a certain threshold, it might mean there's a bug in our system.

Further going into the above example, often a server will allocate a certain amount of calls for their clients, for example, a server might only allow 2 calls per second from one client, and reject any further requests within that second with an error response. The client can sleep between calls to avoid receiving an error from calling the service too often.

The two examples are more related to issues facing distributed systems, where sleep is a simple but effective solution. Like we mentioned, there are more robust solutions we can use to solve race conditions when working with multi-threaded Java programs which we'll dive into in later lessons.

Conclusion

These are the foundations of using threads and working with concurrency. In future lessons we'll dig deeper into their functionality, design, and how to get the most out of them. While there's still a lot to learn, understanding the basic concepts about threading and maximizing your program's efficiency are the first step to fully utilizing them!

Guided Project

Mastery Task 4: Make a Run(nable) For It

Now that we have functionality that allows clients to submit books for publishing by placing them in the BookPublishRequestManager, we need to implement the functionality that actually processes the requests to get some books published!

Milestone 1: Implement BookPublishTask

Currently, the only way that any code in our service gets executed is via a request from a client. However, we don't want to require our clients to make another request to execute the publishing of their book! So how do we get our books published? We can accomplish this by starting a new thread to execute a Runnable that publishes a book. To simplify things a bit for you, we have provided logic that will start a thread and regularly execute a Runnable. You will only be responsible for implementing the Runnable class that contains the publishing logic.

The BookPublisher we have provided is responsible for scheduling the execution of the Runnable repeatedly. If you open up the BookPublisher class and take a look a the start method, you'll see that a Runnable is being scheduled to execute every second. You don't need to call the start method, it is called when your service starts up and currently is executing the NoOpTask that was also provided to you.

You are responsible for writing a new class BookPublishTask, that implements Runnable and processes a publish request from the BookPublishRequestManager. If the BookPublishRequestManager has no publishing requests the BookPublishTask should return immediately without taking action. You will also need to update CatalogDao with new methods for the BookPublishTask to publish new books to our Kindle catalog.

Refer to the 'Asynchronous Book Publishing' section of the design doc for BookPublishTask's sequence diagram and implementation notes. Take special care to consider what happens and what steps to take if an exception is thrown in BookPublishTask's run method!

In order to switch the BookPublisher to start scheduling your new BookPublishTask instead of the NoOpTask, you will need to update the Dagger code that passes a NoOpTask to the BookPublisher constructor. Once you've made this switch, you can delete NoOpTask and its test class.

To test, submit a book publish request by calling SubmitBookForPublishing. You should then get back a publishingRecordId, which you'll use to check the status by calling GetPublishingStatus. You should be able to see the different states that the request has gone through in the publish status history. Once you see that the publish request has hit the SUCCESSFUL state, you can call GetBook with the bookId to see the new or updated book!

Note: there is a limitation to our current implementation that will affect your testing. We are maintaining all publishing requests in memory in the BookPublishRequestManager, which means that when you restart your service any requests that were in the BookPublishRequestManager previously are lost forever. This is not ideal, but we've kept things simple for the sake of this project. To remove this limitation, we could use different technologies such as SQSLinks to an external site. or persisting the requests in a datastore like DynamoDB, but that's out of scope for this project.

Run MasteryTaskFourSubmitBookForPublishingTests to validate your changes.

Milestone 2: Make BookPublishRequestQueue and BookPublishRequest thread safe

We now have a working BookPublishTask running which processes our BookPublishRequests! Each request from a client is processed in its own thread, therefore we have multiple threads writing to the Queue in our BookPublishRequestManager, and another thread reading from it in order to publish. Whenever multiple threads are accessing the same resource, you can have big trouble! You will want to think of ways to ensure that only one thread at a time is writing to or reading from the shared resource. This will ensure that BookPublishRequests added to the Queue are in the correct order, and BookPublishRequests are removed and processed in the correct order.

We need to update BookPublishRequestManager to be thread-safe so that it behaves as expected even if multiple threads are accessing it. Sometimes you will have to write extra code to ensure thread-safety, but Java also provides thread-safe implementations of data structures you can use. Java provides a thread-safe queue called ConcurrentLinkedQueue which we can use instead of a LinkedList. Like LinkedList, it also implements the Queue interface.

The ConcurrentLinkedQueue will ensure that when multiple threads are writing to the queue, the BookPublishRequests will be added in the correct order, and that reads from the queue will then access BookPublishRequests in the correct order. Update your service so that BookPublishRequestManager uses a ConcurrentLinkedQueue instead of a LinkedList.

Optional: You can browse ConcurrentLinkedQueue's documentation for more information about its implementation.

Dealing with multiple threads accessing a shared resource is only an issue when multiple threads can change the state of the resource. If the shared resource can't change, we don't need to be concerned that one thread might not know about another thread's write. Therefore another tool we have to write thread-safe code is to make the shared resource immutable.

A BookPublishRequest will never be a resource shared by multiple threads, but let's practice using our immutability tool anyway. Update BookPublishRequest be immutable, and by doing so we'll prevent unintended subclassing!

Exit checklist:

  • You've implemented BookPublishTask, a Runnable which processes a request from the BookPublishRequestManger to publish a books to the catalog.
  • You've updated BookPublishRequestManger and BookPublishRequest to make them thread safe.
  • You've added unit tests to cover your new code.
  • MasteryTaskFourSubmitBookForPublishingTests pass

Resources

Intro to Threads Code-Along Starter

Starter code for implementing thread concepts.

Solution code for the threading implementation.

Hacking Passwords Project

A practical project demonstrating threading concepts.