A thread is a lightweight subprocess within a process. Concurrency refers to the ability of a program to handle multiple tasks simultaneously. In Java, multiple threads can run concurrently, sharing the same memory space. For example, a web server might use multiple threads to handle multiple client requests at the same time.
Shared resources are data or objects that can be accessed by multiple threads. When multiple threads access and modify shared resources concurrently, it can lead to inconsistent results. Synchronization is a mechanism used to control access to shared resources, ensuring that only one thread can access the resource at a time.
Memory visibility is about ensuring that changes made by one thread to a shared variable are visible to other threads. Java’s memory model defines rules for when and how changes to variables are made visible across threads. For example, if one thread updates a shared variable, other threads should be able to see the updated value.
There are two common ways to create threads in Java:
Thread
class:class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running");
}
}
public class ThreadCreationExample {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
Runnable
interface:class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable is running");
}
}
public class RunnableExample {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
synchronized
keyword:class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class SynchronizedExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Count: " + counter.getCount());
}
}
ReentrantLock
:import java.util.concurrent.locks.ReentrantLock;
class LockCounter {
private int count = 0;
private ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
public class ReentrantLockExample {
public static void main(String[] args) throws InterruptedException {
LockCounter counter = new LockCounter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Count: " + counter.getCount());
}
}
wait()
, notify()
, and notifyAll()
:class Message {
private String msg;
private boolean empty = true;
public synchronized String read() {
while (empty) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
empty = true;
notifyAll();
return msg;
}
public synchronized void write(String msg) {
while (!empty) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
empty = false;
this.msg = msg;
notifyAll();
}
}
public class ThreadCommunicationExample {
public static void main(String[] args) {
Message msg = new Message();
Thread writer = new Thread(() -> {
msg.write("Hello, World!");
});
Thread reader = new Thread(() -> {
System.out.println(msg.read());
});
writer.start();
reader.start();
}
}
A race condition occurs when multiple threads access and modify shared resources concurrently, leading to inconsistent results. To avoid race conditions, use synchronization mechanisms such as the synchronized
keyword or ReentrantLock
to ensure that only one thread can access the shared resource at a time.
A deadlock is a situation where two or more threads are blocked forever, each waiting for the other to release a resource. To prevent deadlocks, follow these guidelines:
Proper resource management is crucial in multi-threaded applications. Make sure to release resources such as locks, file handles, and network connections when they are no longer needed. For example, when using ReentrantLock
, always unlock it in the finally
block to ensure that the lock is released even if an exception occurs.
Java provides high - level concurrency utilities in the java.util.concurrent
package, such as ExecutorService
, Semaphore
, and CountDownLatch
. These utilities simplify the development of multi-threaded applications and reduce the risk of errors.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorServiceExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> {
System.out.println("Task 1 is running");
});
executor.submit(() -> {
System.out.println("Task 2 is running");
});
executor.shutdown();
}
}
Thread pooling is a technique where a fixed number of threads are created and reused to execute multiple tasks. This reduces the overhead of creating and destroying threads, improving the performance of the application. Use ExecutorService
to create and manage thread pools.
java.util.concurrent.atomic
package provides atomic variables such as AtomicInteger
and AtomicLong
, which can be used to perform atomic operations without the need for explicit synchronization.Writing safe and efficient Java multi-threaded code requires a good understanding of fundamental concepts, proper usage of synchronization mechanisms, and following common and best practices. By using high - level concurrency utilities, thread pooling, and optimizing code, developers can create robust and high - performance multi-threaded applications. However, it is important to be aware of potential issues such as race conditions and deadlocks and take appropriate measures to prevent them.