No history yet

Introduction to Java Concurrency

What is Concurrency?

Most modern computers can do multiple things at once. You can browse the web while listening to music and downloading a file. This is concurrency in action. It's the ability of a system to handle multiple tasks by making progress on them in overlapping time periods.

Think of a coffee shop. If there's only one barista, they handle one order at a time from start to finish: take the order, grind the beans, pull the espresso, steam the milk, and serve. This is sequential execution.

Lesson image

Now, imagine two baristas. One might take orders while the other operates the espresso machine. They are working on different parts of different orders at the same time. This is concurrency. The shop is handling multiple orders simultaneously, making the whole operation faster and more efficient.

In Java, we achieve concurrency using threads. A thread is the smallest unit of execution within a program. A single Java program can have many threads, each running its own sequence of instructions. It's like having multiple baristas working inside your application.

Java Threads

Every Java application runs with at least one thread, called the main thread. We can create and start our own threads to perform tasks in the background or handle multiple operations at once. The most common way to define a task for a thread is by implementing the Runnable interface.

class Task implements Runnable {
    @Override
    public void run() {
        // The code that the thread will execute
        System.out.println("Hello from a new thread!");
    }
}

// To run this task in a new thread:
Task myTask = new Task();
Thread myThread = new Thread(myTask);
myThread.start(); // This starts the new thread

When you call myThread.start(), the Java Virtual Machine (JVM) creates a new thread of execution and calls the run() method of your Task. The main thread and your new thread are now running concurrently.

The Trouble with Sharing

Things get interesting when threads need to access the same data. Imagine two threads trying to update a shared counter. This can lead to a problem called a race condition.

race condition

noun

An undesirable situation that occurs when a device or system attempts to perform two or more operations at the same time, but because of the nature of the device or system, the operations must be done in the proper sequence to be done correctly.

Let's say a counter is at 0, and two threads both want to increment it. The operation counter++ isn't a single step. It's actually three:

  1. Read the current value of counter (0).
  2. Add 1 to that value (1).
  3. Write the new value back to counter (1).

Now, see how this can go wrong:

StepThread 1Thread 2Counter Value
1Reads counter (0)0
2Reads counter (0)0
3Adds 1 (result is 1)0
4Adds 1 (result is 1)0
5Writes 1 to counter1
6Writes 1 to counter1

Even though two threads tried to increment the counter, the final value is 1, not 2. The outcome depends on the unpredictable timing of the threads. This is a race condition. To prevent this, we need synchronization.

Synchronization ensures that only one thread can access a shared resource or piece of code at a time. This protected piece of code is called a critical section.

In Java, we can use the synchronized keyword to create a critical section. When a thread enters a synchronized method or block, it acquires a lock. Other threads are blocked from entering until the first thread exits and releases the lock.

class SafeCounter {
    private int count = 0;

    // Only one thread can execute this method at a time.
    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

By making the increment() method synchronized, we guarantee that the read-add-write operation happens as a single, atomic unit. The race condition is solved.

Deadlock

While synchronization solves some problems, it can introduce others. The most notorious is deadlock. A deadlock occurs when two or more threads are blocked forever, each waiting for the other to release a resource.

Imagine two threads, Thread A and Thread B, and two resources, Lock 1 and Lock 2.

  1. Thread A acquires Lock 1.
  2. Thread B acquires Lock 2.
  3. Thread A tries to acquire Lock 2, but it's held by Thread B, so Thread A waits.
  4. Thread B tries to acquire Lock 1, but it's held by Thread A, so Thread B waits.

Both threads are now stuck waiting for each other, and neither can make progress. They are in a deadlock.

Deadlocks are one of the hardest concurrency bugs to find and fix because they might only happen under very specific timing conditions. A common way to avoid them is to ensure all threads acquire locks in the same order.

Understanding concepts such as thread lifecycle, synchronization, and thread safety is essential for creating responsive and efficient applications.

These fundamental concepts—threads, race conditions, synchronization, and deadlocks—are the building blocks for understanding concurrent programming in Java. Mastering them is key to writing robust multi-threaded applications. Now let's test your knowledge.