Concurrent

Threads are fundamental to creating concurrent programs in Java. Java provides built-in support for multithreaded programming.

A thread is a separate path of execution in a program. A single process can contain multiple threads, all executing concurrently.

Creating a Thread:

There are two primary ways to create a thread in Java:

class MyThread extends Thread {
  public void run() {
    for (int i = 0; i < 5; i++) {
      System.out.println("Thread " + i);
    }
  }
}
public class Main {
  public static void main(String[] args) {
    MyThread t1 = new MyThread();
    t1.start();
  }
}

Implementing the Runnable interface:

class MyRunnable implements Runnable {
  public void run() {
    for (int i = 0; i < 5; i++) {
      System.out.println("Runnable " + i);
    }
  }
}
public class Main {
  public static void main(String[] args) {
    Thread t1 = new Thread(new MyRunnable());
    t1.start();
  }
}

Thread Lifecycle

ExecutorService with Future

Commonly Used Methods from Executors Class:

// Creates a thread pool with 5 threads.
ExecutorService executorService = Executors.newFixedThreadPool(5);  
// Submit tasks to the executor
Future<String> future = executorService.submit(() -> {
  // Do some task...
  return "Task result";
});
// Fetch the result
try {
  String result = future.get();  // This blocks until the result is available
  System.out.println(result);
} catch(InterruptedException | ExecutionException e) {
  e.printStackTrace();
}
// Shutdown the executor
executorService.shutdown();

Problems with threads

Multithreading, while powerful, introduces several challenges and potential pitfalls. Here are some common problems associated with threads:

Mitigating these problems often requires careful design, thorough testing, and a deep understanding of concurrency. Various tools and best practices, such as static analysis tools, thread sanitizers, and design patterns (like the single writer principle), can also help in addressing these challenges.

Thread overhead

The overhead of system threads refers to the additional resources and time required by the operating system to manage and switch between threads. The exact overhead can vary depending on the operating system and the hardware, but here are some general components of the overhead associated with system threads:

Limit the number of threads to what's necessary.

Exercises

Basic Thread Creation

Objective: Create a program with three threads. Each thread prints its name and sleeps for a random time between 1 to 5 seconds. Tasks: Create three threads.

Basic Executor

Objective: Familiarize yourself with the executor framework. Tasks:

Future and Callable

Objective: Understand how to submit tasks that return a result. Tasks:

Solutions

Basic Thread Creation

public class Exercise1 {
  public static void main(String[] args) {
    // Create three threads
    for (int i = 1; i <= 3; i++) {
      Thread thread = new Thread(() -> {
        try {
          // Sleep for a random time between 1 to 5 seconds
          int sleepTime = 1000 * (1 + (int) (Math.random() * 5));
          System.out.println(Thread.currentThread().getName() + " is sleeping for " + sleepTime + " ms");
          Thread.sleep(sleepTime);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " has woken up");
      });
      thread.start();
    }
  }
}

Basic Executor

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Exercise3 {
  public static void main(String[] args) {
    ExecutorService executor = Executors.newFixedThreadPool(2);

    for (int i = 0; i < 5; i++) {
      executor.submit(() -> {
        try {
          int sleepTime = 1000 * (1 + (int) (Math.random() * 5));
          System.out.println(Thread.currentThread().getName() + " is working");
          Thread.sleep(sleepTime);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " has finished work");
      });
    }

    executor.shutdown();
  }
}

Future and Callable

import java.util.concurrent.*;

public class Exercise4 {
  public static void main(String[] args) {
    ExecutorService executor = Executors.newFixedThreadPool(3);
    List<Future<String>> futures = new ArrayList<>();

    for (int i = 0; i < 5; i++) {
      Future<String> future = executor.submit(() -> {
        int sleepTime = 1000 * (1 + (int) (Math.random() * 5));
        Thread.sleep(sleepTime);
        return Thread.currentThread().getName();
      });

      futures.add(future);
    }

    for (Future<String> future : futures) {
      try {
        System.out.println("Task in thread " + future.get() + " has finished");
      } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
      }
    }

    executor.shutdown();
  }
}