We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 26
Java Concurrency 101
Learn the basics in minutes
Concurrency “Concurrency is the ability of different parts or units of a program, algorithm, or problem to be executed out-of-order or in partial order, without affecting the outcome. This allows for parallel execution of the concurrent units, which can significantly improve overall speed of the execution in multi-processor and multi-core systems”. [Wikipedia] Processes and Threads Processes are self-contained execution environments with their own memory space and resources.
Threads, often called lightweight processes, are units
of execution within a process. Unlike processes, threads share resources such as memory and files, enabling efficient, but sometimes problematic, communication.
Java leverages multithreaded execution, where each
application typically starts with a main thread. Additional threads can be created to perform asynchronous tasks using the Thread class. Creating and Managing Threads There are two main approaches to using Thread objects in Java: - Direct Approach Create a new Thread instance whenever you need to start an asynchronous task. This gives you direct control over thread creation and management. - Executor Approach Instead of managing threads directly, pass your tasks to an executor. The executor handles thread management separately from your main application logic. In the next slides, we’ll talk about creating and managing thread directly, while Executors will be discussed later Creating a thread by extending the Thread class public class Main { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); } }
class MyThread extends Thread {
@Override public void run() { System.out.println("Thread is running ..."); } } Creating a thread by implementing the Runnable interface public class Main { public static void main(String[] args) { Thread thread = new Thread( new MyRunnable()); thread.start(); } }
class MyRunnable implements Runnable {
@Override public void run() { System.out.println("Runnable is running ..."); } } Creating a thread by utilizing lambda expressions with Runnable public class Main { public static void main(String[] args) { Thread thread = new Thread(() -> { System.out.println( "Thread with lambda is running ..."); }); thread.start(); } } Guarded Blocks Guarded blocks serve as a synchronization mechanism in concurrent programming, allowing threads to coordinate their execution based on specific conditions or predicates. Alongside the Object.wait(), Object.notify(), and Object.notifyAll() methods, they facilitate synchronization by enabling threads to wait within the blocks until the conditions are met. Here's an overview of their functionality: 1. Threads enter guarded blocks, often within a synchronized context; 2. Within the block, threads check a condition; 3. If the condition isn't met, threads invoke wait() and relinquish the monitor; 4. When the condition changes, another thread calls notify() or notifyAll() to wake up waiting threads; 5. The awakened threads reacquire the monitor and recheck the condition before proceeding. Thread States - NEW A thread that has been created but not yet started; - RUNNABLE A thread executing in the Java virtual machine; - BLOCKED A thread that is blocked waiting for a monitor lock; - WAITING A thread that is waiting indefinitely for another thread to perform a particular action; - TIMED_WAITING A thread that is waiting for another thread to perform an action for up to a specified waiting time; - TERMINATED A thread that has finished execution. Thread Methods - start() It starts a Thread; - sleep() It allows a thread to pause its execution for a specified period of time; - interrupt() It interrupts a thread's execution by setting its interrupt status flag; - join() It allows one thread to wait for the completion of another thread before proceeding. Multi-threading Challenges Race Conditions: Occur when multiple threads access shared resources without proper synchronization, leading to unpredictable behavior or data corruption.
Deadlocks: Threads are blocked indefinitely, waiting for each other to
release resources, often caused by acquiring multiple locks in different orders.
Starvation: Threads are perpetually denied access to resources due to
others monopolizing them, commonly seen with higher-priority threads.
Livelocks: Threads are actively resolving resource conflicts but unable to
make progress, resembling deadlocks but with continuous activity.
Thread Interference: Multiple threads accessing shared mutable state
without synchronization, resulting in unexpected behavior or lost updates.
Memory Consistency Errors: Different threads have inconsistent views of
shared memory due to lack of synchronization, leading to unexpected results.
Performance Overhead: Context switching, synchronization, and thread
coordination introduce overhead, and improper use can degrade performance. Volatile Keyword The volatile keyword ensures that any thread accessing a volatile variable reads its most recent write value. Unlike regular variables, whose values may be cached by each thread, volatile variables are always read directly from the main memory.
public class Counter {
private volatile boolean flag; public void enableFlag() { flag = true; } public void disableFlag() { flag = false; } public boolean getFlag() { return flag; } } The volatile keyword does not ensure atomicity. Therefore, it should be used judiciously and alongside other synchronization mechanisms as needed. Synchronization Synchronization is a technique used to control access to shared resources in a multithreaded environment. When multiple threads access shared data concurrently, synchronization ensures that only one thread can execute a synchronized block of code or method at a time. Synchronization using Synchronized Methods One way to synchronize code in Java involves marking the entire method with the synchronized keyword.
public class Counter {
private int count; public synchronized void inc() { // more code count++; } public int getCount() { return count; } } Synchronization using Synchronized Blocks Using synchronized blocks is preferred over synchronized methods because it applies synchronization only to the critical section of the code.
public class Counter {
private int count; public void inc() { // more code synchronized(this) { count++; } } public int getCount() { return count; } } Synchronization using Lock Objects Using explicit lock objects provide more control and flexibility compared to synchronized blocks.
public class Counter {
private int count; private Lock lock = new ReentrantLock(); public void inc() { // more code lock.lock(); try { count++; } finally { lock.unlock(); } } public int getCount() { return count; } } Synchronization: Using Atomic Variables Using atomic variables ensures atomicity without the need for explicit synchronization, simplifying the code.
public class Counter {
private AtomicInteger count = new AtomicInteger(0); public void inc() { // more code count.incrementAndGet(); } public int getCount() { return count.get(); } } High-Level Concurrency High-level concurrency encompasses advanced concurrency utilities and abstractions provided by the Java Concurrency API. These constructs encapsulate common concurrency patterns and offer expressive and efficient ways to manage threads, coordinate tasks, and access shared resources. We will talk about: - Executors and Thread Pools - Concurrent Collections - Fork/Join - Concurrent Random Numbers Executors and Thread Pools Executors and thread pools provide a convenient and efficient way to manage the execution of multiple tasks concurrently. Executors are higher-level abstractions that decouple task submission from execution, while thread pools consist of a collection of pre-allocated threads ready to execute tasks.
public class Main {
public static void main(String[] args) throws Exception { ExecutorService executor = Executors.newFixedThreadPool(3); executor.submit(new Task()); executor.shutdown(); } } class Task implements Runnable { @Override public void run() { /* code */ } } Concurrent Collections Concurrent collections offer thread-safe alternatives to standard collections, enabling secure access and modification by multiple threads simultaneously. These collections employ efficient synchronization mechanisms to ensure thread safety without compromising performance. They are grouped according to the collection interfaces they provide, including BlockingQueue, ConcurrentMap, and ConcurrentNavigableMap. Fork/Join The Fork/Join framework is a high-level concurrency mechanism for parallelizing recursive, divide-and-conquer algorithms. It enables efficient parallel processing of tasks by recursively splitting them into smaller subtasks and executing them concurrently on multiple threads. The Fork/Join framework is particularly useful for exploiting multi-core processors and achieving performance gains in compute-intensive applications. Concurrent Random Numbers The ThreadLocalRandom is ideal for generating random numbers across multiple threads or ForkJoinTasks. Simply call ThreadLocalRandom.current() followed by the desired method.
public class Main {
public static void main(String[] args) { ThreadLocalRandom tlr = ThreadLocalRandom.current(); System.out.println(tlr.nextInt(-10, 10)); System.out.println(tlr.nextBoolean()); System.out.println(tlr.nextDouble(0, 1)); } } Callable Interface The Callable interface provides a means to perform a task concurrently that returns a result and may throw an exception.
public class Main {
public static void main(String[] args) throws Exception { ExecutorService executor = Executors.newSingleThreadExecutor(); Future<Integer> future = executor.submit(new MyCallable()); System.out.println(future.get()); executor.shutdown(); } } class MyCallable implements Callable<Integer> { @Override public Integer call() throws Exception { return 10; } } CompletableFuture Like Callable, CompletableFuture represents a single task with a result. However, CompletableFuture offers extensive methods for chaining, combining, and handling asynchronous tasks in a non-blocking manner.
public class Main {
public static void main(String[] args) throws Exception { CompletableFuture .supplyAsync(new MySupplier()) .thenAccept(result -> System.out.println(result)); } } class MySupplier implements Supplier<Integer> { @Override public Integer get() { return 10; } } Immutable Objects An immutable object is an object whose state cannot be modified after it is created. It ensures thread safety and simplifies concurrent programming by eliminating the need for synchronization.
To create immutable objects:
- Declare the class as final;
- Declare all fields as private and final; - Do not provide setter methods; - Ensure that any mutable objects referenced by the immutable object are defensively copied or made immutable themselves. Share your thoughts! You can read more about Java Concurrency in the Medium article titled
"Mastering Java Concurrency: From
Thread Objects to High-Level Concurrency Features"