Introduction
Thread synchronization in Java is a mechanism that ensures that two or more concurrent threads do not simultaneously execute some particular program segment known as the critical section. Synchronization is essential for preventing thread interference and consistency problems when multiple threads access shared resources.
Key Points:
- Critical Section: A segment of code that accesses shared resources and must not be executed by more than one thread at a time.
- Thread Interference: Occurs when multiple threads modify shared data simultaneously, leading to inconsistent results.
- Synchronization Mechanisms: Java provides various mechanisms for synchronizing threads, including synchronized methods, synchronized blocks, and higher-level concurrency utilities.
Table of Contents
- Synchronized Methods
- Synchronized Blocks
- Static Synchronization
- Lock Interface
- ReentrantLock
- Inter-Thread Communication
- Example: Comprehensive Usage of Synchronization
- Best Practices
- Conclusion
1. Synchronized() Methods
A synchronized method in Java is a method that can be accessed by only one thread at a time for a particular instance.
Example:
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class SynchronizedMethodExample {
public static void main(String[] args) {
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();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Count: " + counter.getCount()); // Expected output: 2000
}
}
2. Synchronized Blocks
A synchronized block is a block of code that is synchronized on a particular object. It allows more fine-grained control over synchronization compared to synchronized methods.
Example:
class Counter {
private int count = 0;
public void increment() {
synchronized (this) {
count++;
}
}
public int getCount() {
return count;
}
}
public class SynchronizedBlockExample {
public static void main(String[] args) {
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();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Count: " + counter.getCount()); // Expected output: 2000
}
}
3. Static Synchronization
Static synchronization is used to synchronize static methods. When a static method is synchronized, the lock is on the class’s Class
object, not on an instance of the class.
Example:
class Counter {
private static int count = 0;
public static synchronized void increment() {
count++;
}
public static int getCount() {
return count;
}
}
public class StaticSynchronizationExample {
public static void main(String[] args) {
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();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Count: " + Counter.getCount()); // Expected output: 2000
}
}
4. Lock Interface
The Lock
interface provides more extensive locking operations than can be obtained using synchronized methods and statements. It provides the ability to lock, try to lock, and unlock.
Example:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
public class LockExample {
public static void main(String[] args) {
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();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Count: " + counter.getCount()); // Expected output: 2000
}
}
5. ReentrantLock
ReentrantLock
is a concrete implementation of the Lock
interface. It offers the same basic functionality as the implicit locks used by synchronized methods and statements but with extended capabilities.
Example:
import java.util.concurrent.locks.ReentrantLock;
class Counter {
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) {
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();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Count: " + counter.getCount()); // Expected output: 2000
}
}
6. Inter-Thread Communication
Inter-thread communication in Java is facilitated through methods like wait()
, notify()
, and notifyAll()
.
Example:
class SharedResource {
private int value = 0;
private boolean available = false;
public synchronized void produce(int value) throws InterruptedException {
while (available) {
wait();
}
this.value = value;
available = true;
notify();
}
public synchronized int consume() throws InterruptedException {
while (!available) {
wait();
}
available = false;
notify();
return value;
}
}
public class InterThreadCommunicationExample {
public static void main(String[] args) {
SharedResource sharedResource = new SharedResource();
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
sharedResource.produce(i);
System.out.println("Produced: " + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
int value = sharedResource.consume();
System.out.println("Consumed: " + value);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producer.start();
consumer.start();
}
}
7. Example: Comprehensive Usage of Synchronization
Example: Multi-Threaded Counter with Synchronization
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
public class ComprehensiveSynchronizationExample {
public static void main(String[] args) {
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();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Count: " + counter.getCount()); // Expected output: 2000
}
}
Output:
Count: 2000
8. Best Practices
- Minimize Synchronized Blocks: Keep synchronized blocks as short as possible to reduce contention and improve performance.
- Use Atomic Variables: For simple atomic operations, use classes from
java.util.concurrent.atomic
(e.g.,AtomicInteger
,AtomicBoolean
). - Prefer High-Level Concurrency Utilities: Use high-level concurrency utilities from the
java.util.concurrent
package for complex synchronization needs. - Avoid Nested Locks: Avoid nested locks to prevent deadlock situations.
- Use
try-finally
for Locking: Always usetry-finally
blocks when working with explicit locks to ensure that locks are released properly. - Understand the Cost of Synchronization: Synchronization adds overhead, so use it judiciously to balance correctness and performance.
- Document Synchronization Policies: Clearly document your synchronization policies to make the code easier to understand and maintain.
9. Conclusion
Thread synchronization in Java is crucial for preventing thread interference and ensuring consistency when multiple threads access shared resources. Java provides several synchronization mechanisms, including synchronized methods, synchronized blocks, the Lock
interface, and inter-thread communication methods. By following best practices and leveraging high-level concurrency utilities, you can write efficient, thread-safe, and maintainable multithreaded applications.