A thread is the smallest unit of execution within a process. Concurrency refers to the ability of a program to handle multiple tasks at the same time. In Java, multiple threads can run concurrently, sharing the same resources such as memory. For example, a web server might use multiple threads to handle multiple client requests simultaneously.
When multiple threads access and modify shared resources, it can lead to inconsistent results. Synchronization is a mechanism used to ensure that only one thread can access a shared resource at a time. In Java, the synchronized
keyword can be used to achieve this.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
A race condition occurs when the behavior of a program depends on the relative timing of events in different threads. For example, if two threads try to increment a shared variable at the same time, the final value might be incorrect.
A deadlock is a situation where two or more threads are blocked forever, waiting for each other to release resources. Consider the following example where two threads hold one resource and wait for the other:
class DeadlockExample {
private static final Object resource1 = new Object();
private static final Object resource2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1: Holding resource 1...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1: Waiting for resource 2...");
synchronized (resource2) {
System.out.println("Thread 1: Holding resource 1 and 2...");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2: Holding resource 2...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2: Waiting for resource 1...");
synchronized (resource1) {
System.out.println("Thread 2: Holding resource 1 and 2...");
}
}
});
thread1.start();
thread2.start();
}
}
The Executor Framework provides a high - level API for managing threads. It includes the Executor
interface, ExecutorService
, and thread pools. Thread pools can reuse threads, reducing the overhead of creating and destroying threads.
Atomic variables, such as AtomicInteger
and AtomicLong
, provide atomic operations without the need for explicit synchronization. These operations are performed atomically, meaning they are indivisible and cannot be interrupted by other threads.
Java provides a set of concurrent collections, such as ConcurrentHashMap
, ConcurrentLinkedQueue
, etc. These collections are designed to be thread - safe and can be accessed by multiple threads simultaneously without the need for external synchronization.
Synchronizers are objects that help coordinate the activities of multiple threads. Examples include CountDownLatch
, CyclicBarrier
, and Semaphore
.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class Task implements Runnable {
@Override
public void run() {
System.out.println("Task is running on thread: " + Thread.currentThread().getName());
}
}
public class ExecutorExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
executor.submit(new Task());
}
executor.shutdown();
}
}
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicVariableExample {
private static AtomicInteger atomicCount = new AtomicInteger(0);
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
atomicCount.incrementAndGet();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
atomicCount.incrementAndGet();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + atomicCount.get());
}
}
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentCollectionExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
map.put("key" + i, i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
map.get("key" + i);
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Map size: " + map.size());
}
}
import java.util.concurrent.CountDownLatch;
public class SynchronizerExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println("Task is completed by " + Thread.currentThread().getName());
latch.countDown();
}).start();
}
latch.await();
System.out.println("All tasks are completed.");
}
}
The size of the thread pool should be carefully chosen based on the nature of the tasks. For CPU - bound tasks, the number of threads should be close to the number of CPU cores. For I/O - bound tasks, a larger number of threads can be used.
Blocking operations, such as Thread.sleep()
or waiting for I/O, can reduce the performance of a multi - threaded application. If possible, use non - blocking alternatives or asynchronous I/O.
Use the minimum amount of synchronization required. Over - synchronization can lead to performance degradation, while under - synchronization can lead to race conditions.
In multi - threaded code, exceptions can occur in different threads. It is important to handle these exceptions properly. For example, when using the ExecutorService
, exceptions thrown by tasks can be retrieved using the Future
object.
Use tools such as VisualVM or Java Mission Control to monitor the performance of multi - threaded applications. Analyze thread dumps to identify bottlenecks and tune the application accordingly.
Testing multi - threaded applications is challenging due to the non - deterministic nature of thread execution. Use testing frameworks and techniques specifically designed for multi - threaded code, such as JUnit with the @ThreadSafe
annotation.
Java Concurrency Utilities provide a powerful set of tools to simplify the development of multi - threaded applications and enhance their performance. By understanding the fundamental concepts, using the appropriate utilities, following common practices, and adhering to best practices, developers can write efficient and robust multi - threaded code. These utilities help in avoiding common pitfalls such as race conditions and deadlocks, and make it easier to manage threads and shared resources.