Java tutorials > Multithreading and Concurrency > Threads and Synchronization > How to use `java.util.concurrent` package?
How to use `java.util.concurrent` package?
The java.util.concurrent
package, introduced in Java 5, offers a rich set of tools and classes for concurrent programming, significantly simplifying the development of robust and efficient multithreaded applications. This tutorial explores key components of this package and demonstrates their usage with practical examples.
Introduction to `java.util.concurrent`
The These tools help to avoid common pitfalls of multithreaded programming such as race conditions, deadlocks, and livelocks, making concurrent code easier to write and maintain.java.util.concurrent
package provides a higher level abstraction over the basic thread management facilities offered by Java. It includes features such as:
Executors and ExecutorService
The In this example, ExecutorService
interface represents an asynchronous execution mechanism which is capable of executing tasks concurrently. Executors
class provide static methods to create various types of thread pools. Here's how you can use it:
Executors.newFixedThreadPool(int)
to create a thread pool with a fixed number of threads.executor.execute(Runnable)
to submit tasks for execution.executor.shutdown()
to prevent new tasks from being submitted and wait for existing tasks to complete.newFixedThreadPool(5)
creates a thread pool with 5 threads. Ten tasks are submitted to the executor, and each task is handled by one of the available threads.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
Runnable worker = new WorkerThread("Task " + i);
executor.execute(worker);
}
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
class WorkerThread implements Runnable {
private String message;
public WorkerThread(String s){
this.message=s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" (Start) message = "+message);
processmessage();// Simulate work
System.out.println(Thread.currentThread().getName()+" (End)");
}
private void processmessage() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) { e.printStackTrace(); }
}
}
Concepts Behind the Snippet (Executors)
The ExecutorService
decouples task submission from task execution. Instead of creating and managing threads manually, you submit tasks to an executor, which then manages the thread lifecycle. This provides several benefits:
Real-Life Use Case Section (Executors)
Consider a web server that handles incoming requests. Instead of creating a new thread for each request, the server can use an ExecutorService
to manage a pool of threads that process requests concurrently. This significantly improves the server's ability to handle a large number of requests without creating an excessive number of threads.
Future Interface
The Future
interface represents the result of an asynchronous computation. It provides methods to check if the computation is complete, to wait for its completion, and to retrieve the result. This is useful when you need to perform an operation that takes a long time and you don't want to block the main thread.
executor.submit(Callable)
to submit a task that returns a value.submit()
method returns a Future
object that represents the result of the task.future.get()
to retrieve the result. This method blocks until the result is available.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Callable;
public class FutureExample {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(2);
Callable<Integer> task = () -> {
System.out.println("Executing task in thread: " + Thread.currentThread().getName());
Thread.sleep(1000);
return 42;
};
Future<Integer> future = executor.submit(task);
System.out.println("Task submitted. Waiting for result...");
Integer result = future.get(); // Blocks until the result is available
System.out.println("Result: " + result);
executor.shutdown();
}
}
Concurrent Collections
The java.util.concurrent
package provides concurrent versions of standard collection classes, such as ConcurrentHashMap
, ConcurrentLinkedQueue
, and CopyOnWriteArrayList
. These collections are thread-safe and designed for concurrent access.ConcurrentHashMap
is a thread-safe hash table implementation. Multiple threads can read and write to the map concurrently without external synchronization.
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
Map<String, Integer> map = new ConcurrentHashMap<>();
// Multiple threads can safely read and write to the map
Runnable task1 = () -> {
for (int i = 0; i < 1000; i++) {
map.put("Key" + i, i);
}
};
Runnable task2 = () -> {
for (int i = 1000; i < 2000; i++) {
map.put("Key" + i, i);
}
};
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Map size: " + map.size()); // Output should be 2000
}
}
Locks and Conditions
The In this example, the waiter thread waits for the signaler thread to set the signal to true. The waiter thread releases the lock and waits on the condition until the signaler thread signals it. The java.util.concurrent.locks
package provides more flexible locking mechanisms than the built-in synchronized
keyword. The Lock
interface provides methods for acquiring and releasing locks, and the Condition
interface provides methods for waiting for specific conditions to be met.
Lock
with the same basic behavior and semantics as the implicit monitor lock accessed using synchronized
methods and statements, but with extended capabilities.Condition
allows for more precise control over thread synchronization than using wait()
and notify()
.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;
public class ConditionExample {
private static final Lock lock = new ReentrantLock();
private static final Condition condition = lock.newCondition();
private static boolean signal = false;
public static void main(String[] args) {
Thread waiter = new Thread(() -> {
lock.lock();
try {
while (!signal) {
System.out.println("Waiter: Waiting for signal...");
condition.await(); // Releases the lock and waits
}
System.out.println("Waiter: Signal received!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
Thread signaler = new Thread(() -> {
lock.lock();
try {
Thread.sleep(2000); // Simulate some work
signal = true;
System.out.println("Signaler: Sending signal...");
condition.signal(); // Wakes up a waiting thread
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
waiter.start();
signaler.start();
}
}
Atomic Variables
The In this example, two threads increment the counter variable concurrently. The java.util.concurrent.atomic
package provides classes for performing atomic operations on single variables. These classes provide lock-free, thread-safe alternatives to using synchronized
blocks for simple operations.AtomicInteger
is a class that represents an integer value that can be atomically updated. It provides methods such as incrementAndGet()
, decrementAndGet()
, and compareAndSet()
for performing atomic operations.AtomicInteger
class ensures that the increments are atomic, preventing race conditions.
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerExample {
private static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Counter value: " + counter.get()); // Expected: 2000
}
}
Best Practices
synchronized
blocks.java.util.concurrent
package has its own trade-offs in terms of performance and complexity.
Interview Tip
When discussing concurrency in Java during interviews, emphasize your understanding of the java.util.concurrent
package. Be prepared to explain the purpose of different classes and interfaces, and how they can be used to solve common concurrency problems. Also, be ready to discuss the trade-offs between different approaches, such as using synchronized
blocks vs. using Lock
objects.
When to use them
Use the java.util.concurrent
package when you need to write concurrent code that is:
Memory footprint
The memory footprint of the java.util.concurrent
package depends on the specific classes and interfaces that you use. However, in general, the memory overhead of using this package is relatively low. For example, AtomicInteger
requires only a small amount of additional memory compared to a regular int
. ConcurrentHashMap
has a slightly higher memory overhead than HashMap
, but it provides thread safety without requiring external synchronization.
Alternatives
Alternatives to using the java.util.concurrent
package include:
synchronized
keyword can be used to protect critical sections of code. However, synchronized
blocks can be less flexible and less efficient than using Lock
objects.wait()
and notify()
methods can be used to synchronize threads. However, these methods can be more difficult to use correctly than using Condition
objects.ExecutorService
.
Pros
java.util.concurrent
package are designed for high performance and low overhead.
Cons
java.util.concurrent
package can be more complex to learn and use than the basic thread management facilities offered by Java.
FAQ
-
What is the difference between
ExecutorService.shutdown()
andExecutorService.shutdownNow()
?
shutdown()
initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted.shutdownNow()
attempts to stop all actively executing tasks, halts the processing of waiting tasks, and returns a list of the tasks that were awaiting execution. -
When should I use
ReentrantLock
instead ofsynchronized
?
Use
ReentrantLock
when you need more advanced features such as fairness, timed lock waits, or the ability to interrupt a thread waiting for the lock. Thesynchronized
keyword is simpler and often sufficient for basic locking needs. -
What is the purpose of
Condition
objects in thejava.util.concurrent
package?
Condition
objects provide a way for threads to suspend execution (wait) until a specific condition is true. They offer more control and flexibility than usingwait()
andnotify()
on an object's monitor.