Java 101 - Concurrency

Java 101 - Concurrency

Writing efficient, correct, and easy to read concurrent code in Java

Introduction

Concurrency is a fundamental aspect of modern programming and is critical for writing high-performance and scalable applications. Java, as one of the most widely used programming languages, has a rich set of concurrency constructs that enable developers to write concurrent code clearly and concisely.

In this blog post, we will explore Java's concurrency features and provide code examples to demonstrate how they can be used in practice. We will start by discussing the basics of concurrent programming in Java and then delve into more advanced topics such as thread synchronization, thread communication, and atomic operations.

Concurrency in Java

Concurrency refers to the ability of a program to execute multiple tasks at the same time. In Java, concurrency is achieved by using threads. A thread is a separate path of execution within a single program, and each thread can run in parallel with other threads.

Java provides several built-in mechanisms for creating and managing threads, including the java.util.concurrent package, which contains a comprehensive set of high-level concurrency utilities, as well as lower-level mechanisms like the java.lang.Thread class and synchronized blocks.

Creating and Running Threads in Java

The simplest way to create a new thread in Java is to extend the java.lang.Thread class and override its run() method. The run() method contains the code that the thread will execute when it is started.

Here is a simple example:

class MyThread extends Thread {
  public void run() {
    System.out.println("Hello from MyThread");
  }
}

public class Main {
  public static void main(String[] args) {
    MyThread myThread = new MyThread();
    myThread.start();
  }
}

In this example, we define a new class called MyThread that extends the java.lang.Thread class. The run() method of the MyThread class simply prints "Hello from MyThread". To start the thread, we create an instance of the MyThread class and call its start() method.

Another way to create a new thread in Java is to use the java.util.concurrent.Executor framework. The Executor framework provides a high-level mechanism for executing tasks in a thread pool, which is a collection of worker threads that are reused to execute tasks.

Here is an example:

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

public class Main {
  public static void main(String[] args) {
    Executor executor = Executors.newSingleThreadExecutor();
    executor.execute(() -> System.out.println("Hello from Executor"));
  }
}

In this example, we create a single-thread executor using the Executors.newSingleThreadExecutor() method. To execute a task in the thread pool, we call the execute() method on the executor and pass it a runnable object that contains the code we want to run.

Synchronizing Threads in Java

Synchronization is the process of ensuring that multiple threads do not access shared resources simultaneously, which can lead to race conditions and other synchronization-related problems. In Java, synchronization is achieved using the synchronized keyword.

A synchronized block is a block of code that is protected by a lock. When a thread enters a synchronized block, it acquires the lock and holds it until it exits the block. Other threads that attempt to enter the block will be blocked until the lock is released.

Here is an example:

class Counter {
  private int count = 0;

  public void increment() {
    synchronized (this) {
      count++;
    }
  }

  public int getCount() {
    return count;
  }
}

class IncrementThread extends Thread {
  private Counter counter;

  public IncrementThread(Counter counter) {
    this.counter = counter;
  }

  public void run() {
    for (int i = 0; i < 1000; i++) {
      counter.increment();
    }
  }
}

public class Main {
  public static void main(String[] args) throws InterruptedException {
    Counter counter = new Counter();
    IncrementThread t1 = new IncrementThread(counter);
    IncrementThread t2 = new IncrementThread(counter);
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter.getCount());
  }
}

In this example, we have a Counter class with a count field and an increment() method that increments the count. The increment() method is protected by a synchronized block that ensures that only one thread can increment the count at a time.

We also have an IncrementThread class that extends java.lang.Thread and increments the count 1000 times. In the main() method, we create two instances of the IncrementThread class and start them. The join() method is used to wait for the threads to finish.

When we run this program, the output will always be 2000, which is what we expect. Without the synchronized block, the output would be unpredictable because multiple threads would be accessing the count field simultaneously.

Communicating Between Threads in Java

Thread communication refers to the process of exchanging data between threads. Java provides several mechanisms for communicating between threads, including wait() and notify(), volatile variables, and the java.util.concurrent package.

The wait() and notify() methods are low-level mechanisms for communication between threads that are synchronized on the same object. The wait() method is used to block a thread until it is notified by another thread, and the notify() method is used to notify a waiting thread.

Here is an example:

class Message {
    private String message;
    private boolean isMessageSet = false;

    public synchronized void setMessage(String message) {
        while (isMessageSet) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.message = message;
        isMessageSet = true;
        notify();
    }

    public synchronized String getMessage() {
        while (!isMessageSet) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        isMessageSet = false;
        notify();
        return message;
    }
}

class SetMessageThread extends Thread {
    private Message message;

    public SetMessageThread(Message message) {
        this.message = message;
    }

    public void run() {
        for (int i = 0; i < 100; i++) {
            message.setMessage("Message " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class GetMessageThread extends Thread {
    private Message message;

    public GetMessageThread(Message message) {
        this.message = message;
    }

    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(message.getMessage());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Message message = new Message();
        SetMessageThread setMessageThread = new SetMessageThread(message);
        GetMessageThread getMessageThread = new GetMessageThread(message);
        setMessageThread.start();
        getMessageThread.start();
        setMessageThread.join();
        getMessageThread.join();
    }
}

In this example, we have a Message class with a message field, a boolean flag (isMessageSet), and two methods: setMessage() and getMessage(). The setMessage() method sets the message and notifies the waiting thread, and the getMessage() method retrieves the message and waits for the message to be set if it has not yet been set. Both methods are protected by a synchronized block to ensure that only one thread can access the message at a time.

We also have two threads, SetMessageThread and GetMessageThread, that respectively set and get the message 100 times. In the main() method, we create instances of these two threads, start them, and wait for them to finish.

By using the wait() and notify() methods, we can ensure that the SetMessageThread and GetMessageThread communicate with each other and access the message in a coordinated manner.

Volatile Variables

Volatile variables are a type of variable in Java that can be accessed by multiple threads. When a thread accesses a volatile variable, it is guaranteed to see the most recent value written by any other thread, even if the other thread is running on a different processor.

Here is an example:

class Counter {
  private volatile int count = 0;

  public void increment() {
    count++;
  }

  public int getCount() {
    return count;
  }
}

class IncrementThread extends Thread {
  private Counter counter;

  public IncrementThread(Counter counter) {
    this.counter = counter;
  }

  public void run() {
    for (int i = 0; i < 1000; i++) {
      counter.increment();
    }
  }
}

public class Main {
  public static void main(String[] args) throws InterruptedException {
    Counter counter = new Counter();
    IncrementThread t1 = new IncrementThread(counter);
    IncrementThread t2 = new IncrementThread(counter);
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter.getCount());
  }
}

In this example, we have a Counter class with a volatile count field and an increment() method that increments the count. The volatile keyword ensures that all threads see the most recent value of the count field.

We also have an IncrementThread class that extends java.lang.Thread and increments the count 1000 times. In the main() method, we create two instances of the IncrementThread class and start them. The join() method is used to wait for the threads to finish.

When we run this program, the output will always be 2000, which is what we expect.

Java Util Concurrent Package

The java.util.concurrent package provides a high-level framework for concurrent programming in Java. It contains several classes and interfaces that can be used to write concurrent programs in a more elegant and easier-to-read manner.

Here are a few examples of classes and interfaces from the java.util.concurrent package:

  • java.util.concurrent.Executor: An interface that represents an asynchronous execution facility.

  • java.util.concurrent.ExecutorService: A subinterface of Executor that provides methods for managing the lifecycle of an executor.

  • java.util.concurrent.Future: A representation of the result of an asynchronous computation.

  • java.util.concurrent.Callable: An interface that represents a task that can be executed and that returns a result.

Here is an example of using an ExecutorService:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

class SumTask implements Callable < Integer > {
    private int n;

    public SumTask(int n) {
        this.n = n;
    }

    public Integer call() {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return sum;
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future < Integer > result = executor.submit(new SumTask(100));
        executor.shutdown();
        System.out.println(result.get());
    }
}

In this example, we have a SumTask class that implements java.util.concurrent.Callable and calculates the sum of the first n positive integers. The Callable interface represents a task that can be executed and that returns a result.

In the main() method, we create an instance of java.util.concurrent.ExecutorService using the java.util.concurrent.Executors.newSingleThreadExecutor() factory method. This creates an executor that uses a single worker thread to execute tasks.

We then submit the SumTask to the executor using the submit() method and get the result using the get() method of the java.util.concurrent.Future interface. The Future interface represents the result of an asynchronous computation.

The executor is then shut down using the shutdown() method.

This is just a simple example of using the java.util.concurrent package. There are many other classes and interfaces in this package that can be used to write more complex concurrent programs.

Conclusion

Concurrency is an important aspect of modern software development and Java provides several features to support concurrent programming. By using the synchronized keyword, the wait() and notify() methods, volatile variables, and the java.util.concurrent package, we can write concurrent programs that are efficient, correct, and easy to read.