Overview
The synchronized keyword in Java is used to control access to critical sections of code by multiple threads. It ensures that only one thread can execute a synchronized block/method at a time.
1. Synchronized Methods
Instance Method Synchronization
public class SynchronizedMethods {
static class Counter {
private int count = 0;
// Synchronized instance method
public synchronized void increment() {
count++;
System.out.println(Thread.currentThread().getName() +
" incremented to: " + count);
}
// Synchronized instance method
public synchronized void decrement() {
count--;
System.out.println(Thread.currentThread().getName() +
" decremented to: " + count);
}
public synchronized int getCount() {
return count;
}
}
static class CounterTask implements Runnable {
private Counter counter;
private boolean increment;
public CounterTask(Counter counter, boolean increment) {
this.counter = counter;
this.increment = increment;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
if (increment) {
counter.increment();
} else {
counter.decrement();
}
try {
Thread.sleep(100); // Simulate some work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Synchronized Instance Methods ===");
Counter counter = new Counter();
Thread t1 = new Thread(new CounterTask(counter, true), "Increment-Thread");
Thread t2 = new Thread(new CounterTask(counter, false), "Decrement-Thread");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + counter.getCount());
}
}
Static Method Synchronization
public class StaticSynchronization {
static class SharedResource {
private static int staticCounter = 0;
private int instanceCounter = 0;
// Synchronized static method - locks on Class object
public static synchronized void incrementStatic() {
staticCounter++;
System.out.println(Thread.currentThread().getName() +
" - Static counter: " + staticCounter);
try {
Thread.sleep(100); // Simulate work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// Synchronized instance method - locks on instance
public synchronized void incrementInstance() {
instanceCounter++;
System.out.println(Thread.currentThread().getName() +
" - Instance counter: " + instanceCounter);
try {
Thread.sleep(100); // Simulate work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Static vs Instance Synchronization ===");
SharedResource resource1 = new SharedResource();
SharedResource resource2 = new SharedResource();
// Threads using static synchronization (class-level lock)
Thread t1 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
SharedResource.incrementStatic();
}
}, "Static-Thread-1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
SharedResource.incrementStatic();
}
}, "Static-Thread-2");
// Threads using instance synchronization (instance-level lock)
Thread t3 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
resource1.incrementInstance();
}
}, "Instance-Thread-1");
Thread t4 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
resource2.incrementInstance();
}
}, "Instance-Thread-2");
t1.start();
t2.start();
t3.start();
t4.start();
t1.join();
t2.join();
t3.join();
t4.join();
System.out.println("All threads completed");
}
}
2. Synchronized Blocks
Instance Synchronized Blocks
public class SynchronizedBlocks {
static class BankAccount {
private double balance;
private final Object lock = new Object();
private String accountHolder;
public BankAccount(String accountHolder, double initialBalance) {
this.accountHolder = accountHolder;
this.balance = initialBalance;
}
// Using synchronized block with 'this'
public void deposit(double amount) {
synchronized (this) {
balance += amount;
System.out.println(Thread.currentThread().getName() +
" deposited: " + amount +
", Balance: " + balance);
// Simulate some processing time
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// Using synchronized block with custom lock object
public void withdraw(double amount) {
synchronized (lock) {
if (balance >= amount) {
balance -= amount;
System.out.println(Thread.currentThread().getName() +
" withdrew: " + amount +
", Balance: " + balance);
} else {
System.out.println(Thread.currentThread().getName() +
" - Insufficient funds for withdrawal: " + amount);
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// Different operations can use different locks
public void updateAccountHolder(String newHolder) {
synchronized (lock) { // Using custom lock
String oldHolder = this.accountHolder;
this.accountHolder = newHolder;
System.out.println(Thread.currentThread().getName() +
" updated holder from " + oldHolder +
" to " + newHolder);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public double getBalance() {
synchronized (this) { // Using 'this' lock
return balance;
}
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Synchronized Blocks ===");
BankAccount account = new BankAccount("John Doe", 1000);
// Create multiple threads performing different operations
Thread depositThread1 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
account.deposit(100);
}
}, "Depositor-1");
Thread depositThread2 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
account.deposit(50);
}
}, "Depositor-2");
Thread withdrawThread = new Thread(() -> {
for (int i = 0; i < 4; i++) {
account.withdraw(200);
}
}, "Withdrawer");
Thread updateThread = new Thread(() -> {
account.updateAccountHolder("Jane Smith");
}, "Updater");
depositThread1.start();
depositThread2.start();
withdrawThread.start();
updateThread.start();
depositThread1.join();
depositThread2.join();
withdrawThread.join();
updateThread.join();
System.out.println("Final balance: " + account.getBalance());
}
}
Static Synchronized Blocks
public class StaticSynchronizedBlocks {
static class ConfigurationManager {
private static Map<String, String> config = new HashMap<>();
private static final Object staticLock = new Object();
// Static synchronized block
public static void updateConfig(String key, String value) {
synchronized (staticLock) {
String oldValue = config.put(key, value);
System.out.println(Thread.currentThread().getName() +
" updated " + key +
" from '" + oldValue + "' to '" + value + "'");
try {
Thread.sleep(100); // Simulate work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// Another static method using class-level lock
public static void clearConfig() {
synchronized (ConfigurationManager.class) {
int size = config.size();
config.clear();
System.out.println(Thread.currentThread().getName() +
" cleared " + size + " config entries");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public static String getConfig(String key) {
synchronized (staticLock) {
return config.get(key);
}
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Static Synchronized Blocks ===");
// Multiple threads updating static configuration
Thread t1 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
ConfigurationManager.updateConfig("server" + i, "192.168.1." + i);
}
}, "Config-Updater-1");
Thread t2 = new Thread(() -> {
for (int i = 3; i < 6; i++) {
ConfigurationManager.updateConfig("server" + i, "192.168.1." + i);
}
}, "Config-Updater-2");
Thread t3 = new Thread(() -> {
ConfigurationManager.clearConfig();
}, "Config-Cleaner");
t1.start();
t2.start();
Thread.sleep(200); // Let some updates happen
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("Configuration management completed");
}
}
3. Producer-Consumer Problem
Classic Producer-Consumer with Synchronization
public class ProducerConsumer {
static class MessageQueue {
private final Queue<String> queue = new LinkedList<>();
private final int capacity;
public MessageQueue(int capacity) {
this.capacity = capacity;
}
public synchronized void produce(String message) throws InterruptedException {
while (queue.size() == capacity) {
System.out.println(Thread.currentThread().getName() +
" - Queue full, waiting...");
wait(); // Wait until consumer notifies
}
queue.add(message);
System.out.println(Thread.currentThread().getName() +
" produced: " + message +
" | Queue size: " + queue.size());
notifyAll(); // Notify waiting consumers
}
public synchronized String consume() throws InterruptedException {
while (queue.isEmpty()) {
System.out.println(Thread.currentThread().getName() +
" - Queue empty, waiting...");
wait(); // Wait until producer notifies
}
String message = queue.remove();
System.out.println(Thread.currentThread().getName() +
" consumed: " + message +
" | Queue size: " + queue.size());
notifyAll(); // Notify waiting producers
return message;
}
public synchronized int size() {
return queue.size();
}
}
static class Producer implements Runnable {
private MessageQueue queue;
private int messageCount;
public Producer(MessageQueue queue, int messageCount) {
this.queue = queue;
this.messageCount = messageCount;
}
@Override
public void run() {
try {
for (int i = 1; i <= messageCount; i++) {
String message = "Message-" + i + "-from-" + Thread.currentThread().getName();
queue.produce(message);
// Simulate production time
Thread.sleep((long) (Math.random() * 200));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
static class Consumer implements Runnable {
private MessageQueue queue;
private int messageCount;
public Consumer(MessageQueue queue, int messageCount) {
this.queue = queue;
this.messageCount = messageCount;
}
@Override
public void run() {
try {
for (int i = 1; i <= messageCount; i++) {
queue.consume();
// Simulate consumption time
Thread.sleep((long) (Math.random() * 300));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Producer-Consumer with Synchronization ===");
MessageQueue queue = new MessageQueue(3);
Thread producer1 = new Thread(new Producer(queue, 5), "Producer-1");
Thread producer2 = new Thread(new Producer(queue, 5), "Producer-2");
Thread consumer1 = new Thread(new Consumer(queue, 7), "Consumer-1");
Thread consumer2 = new Thread(new Consumer(queue, 3), "Consumer-2");
producer1.start();
producer2.start();
consumer1.start();
consumer2.start();
producer1.join();
producer2.join();
consumer1.join();
consumer2.join();
System.out.println("Final queue size: " + queue.size());
System.out.println("Producer-Consumer completed");
}
}
4. Deadlock Prevention
Deadlock Example and Prevention
public class DeadlockPrevention {
static class Account {
private final String id;
private double balance;
public Account(String id, double balance) {
this.id = id;
this.balance = balance;
}
public String getId() { return id; }
public synchronized void debit(double amount) {
balance -= amount;
}
public synchronized void credit(double amount) {
balance += amount;
}
public synchronized double getBalance() {
return balance;
}
// Potential deadlock method
public void transferWithDeadlock(Account to, double amount) {
synchronized (this) {
System.out.println(Thread.currentThread().getName() +
" acquired lock on " + this.id);
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (to) {
System.out.println(Thread.currentThread().getName() +
" acquired lock on " + to.id);
if (this.balance >= amount) {
this.debit(amount);
to.credit(amount);
System.out.println("Transferred " + amount +
" from " + this.id + " to " + to.id);
}
}
}
}
// Deadlock prevention using ordered locking
public void transferSafe(Account to, double amount) {
// Determine lock order based on account ID
Account firstLock = this.id.compareTo(to.id) < 0 ? this : to;
Account secondLock = this.id.compareTo(to.id) < 0 ? to : this;
synchronized (firstLock) {
System.out.println(Thread.currentThread().getName() +
" acquired lock on " + firstLock.id);
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (secondLock) {
System.out.println(Thread.currentThread().getName() +
" acquired lock on " + secondLock.id);
if (this.balance >= amount) {
this.debit(amount);
to.credit(amount);
System.out.println("Transferred " + amount +
" from " + this.id + " to " + to.id);
}
}
}
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Deadlock Prevention ===");
Account accountA = new Account("A", 1000);
Account accountB = new Account("B", 1000);
// This might cause deadlock
System.out.println("\n--- Potential Deadlock Scenario ---");
Thread t1 = new Thread(() -> {
accountA.transferWithDeadlock(accountB, 100);
}, "Thread-1-A-to-B");
Thread t2 = new Thread(() -> {
accountB.transferWithDeadlock(accountA, 50);
}, "Thread-2-B-to-A");
t1.start();
t2.start();
// Wait a bit to see if deadlock occurs
Thread.sleep(1000);
if (t1.isAlive() && t2.isAlive()) {
System.out.println("Deadlock detected! Stopping threads...");
t1.interrupt();
t2.interrupt();
}
t1.join();
t2.join();
// Safe transfer
System.out.println("\n--- Safe Transfer Scenario ---");
Thread t3 = new Thread(() -> {
accountA.transferSafe(accountB, 100);
}, "Thread-3-A-to-B");
Thread t4 = new Thread(() -> {
accountB.transferSafe(accountA, 50);
}, "Thread-4-B-to-A");
t3.start();
t4.start();
t3.join();
t4.join();
System.out.println("Account A balance: " + accountA.getBalance());
System.out.println("Account B balance: " + accountB.getBalance());
}
}
5. Read-Write Locks Pattern
Synchronized Read-Write Implementation
public class ReadWriteSynchronized {
static class SharedDictionary {
private final Map<String, String> dictionary = new HashMap<>();
private int readCount = 0;
private int writeCount = 0;
public synchronized void put(String key, String value) {
writeCount++;
System.out.println(Thread.currentThread().getName() +
" writing - Key: " + key + ", Value: " + value +
" (Write #" + writeCount + ")");
// Simulate write operation time
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
dictionary.put(key, value);
System.out.println(Thread.currentThread().getName() + " write completed");
}
public synchronized String get(String key) {
readCount++;
System.out.println(Thread.currentThread().getName() +
" reading - Key: " + key +
" (Read #" + readCount + ")");
// Simulate read operation time
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
String value = dictionary.get(key);
System.out.println(Thread.currentThread().getName() +
" read - Key: " + key + ", Value: " + value);
return value;
}
public synchronized int size() {
return dictionary.size();
}
}
static class Writer implements Runnable {
private SharedDictionary dictionary;
private int entries;
public Writer(SharedDictionary dictionary, int entries) {
this.dictionary = dictionary;
this.entries = entries;
}
@Override
public void run() {
for (int i = 0; i < entries; i++) {
String key = "key-" + Thread.currentThread().getName() + "-" + i;
String value = "value-" + i;
dictionary.put(key, value);
try {
Thread.sleep(150); // Time between writes
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
static class Reader implements Runnable {
private SharedDictionary dictionary;
private int reads;
public Reader(SharedDictionary dictionary, int reads) {
this.dictionary = dictionary;
this.reads = reads;
}
@Override
public void run() {
for (int i = 0; i < reads; i++) {
String key = "key-" + (i % 5); // Read from limited set of keys
dictionary.get(key);
try {
Thread.sleep(50); // Time between reads
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Read-Write with Synchronization ===");
SharedDictionary dictionary = new SharedDictionary();
// Create writers
Thread writer1 = new Thread(new Writer(dictionary, 3), "Writer-1");
Thread writer2 = new Thread(new Writer(dictionary, 3), "Writer-2");
// Create readers
Thread reader1 = new Thread(new Reader(dictionary, 5), "Reader-1");
Thread reader2 = new Thread(new Reader(dictionary, 5), "Reader-2");
Thread reader3 = new Thread(new Reader(dictionary, 5), "Reader-3");
writer1.start();
writer2.start();
reader1.start();
reader2.start();
reader3.start();
writer1.join();
writer2.join();
reader1.join();
reader2.join();
reader3.join();
System.out.println("Final dictionary size: " + dictionary.size());
}
}
6. Double-Checked Locking
Singleton Pattern with Synchronization
public class DoubleCheckedLocking {
// Singleton class with various synchronization approaches
static class Singleton {
private static Singleton instance;
private static final Object lock = new Object();
private int value;
private Singleton() {
// Private constructor
value = 42;
System.out.println("Singleton instance created");
}
// Method 1: Synchronized method (thread-safe but slow)
public static synchronized Singleton getInstanceSyncMethod() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
// Method 2: Synchronized block (better performance)
public static Singleton getInstanceSyncBlock() {
synchronized (lock) {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
// Method 3: Double-checked locking (best performance)
public static Singleton getInstanceDoubleChecked() {
if (instance == null) { // First check (no synchronization)
synchronized (lock) {
if (instance == null) { // Second check (with synchronization)
instance = new Singleton();
}
}
}
return instance;
}
// Method 4: Initialization-on-demand holder idiom (recommended)
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstanceHolder() {
return Holder.INSTANCE;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
static class SingletonAccessor implements Runnable {
private int methodType;
public SingletonAccessor(int methodType) {
this.methodType = methodType;
}
@Override
public void run() {
Singleton singleton = null;
switch (methodType) {
case 1:
singleton = Singleton.getInstanceSyncMethod();
break;
case 2:
singleton = Singleton.getInstanceSyncBlock();
break;
case 3:
singleton = Singleton.getInstanceDoubleChecked();
break;
case 4:
singleton = Singleton.getInstanceHolder();
break;
}
System.out.println(Thread.currentThread().getName() +
" got singleton with value: " + singleton.getValue());
// Modify value to verify we're using same instance
singleton.setValue(singleton.getValue() + 1);
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Singleton Synchronization Patterns ===");
// Test each synchronization method
for (int method = 1; method <= 4; method++) {
System.out.println("\n--- Testing Method " + method + " ---");
// Reset singleton for each test
try {
java.lang.reflect.Field field = Singleton.class.getDeclaredField("instance");
field.setAccessible(true);
field.set(null, null);
} catch (Exception e) {
e.printStackTrace();
}
Thread[] threads = new Thread[5];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new SingletonAccessor(method), "Thread-" + (i + 1));
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
// Verify final value
Singleton finalInstance = null;
switch (method) {
case 1: finalInstance = Singleton.getInstanceSyncMethod(); break;
case 2: finalInstance = Singleton.getInstanceSyncBlock(); break;
case 3: finalInstance = Singleton.getInstanceDoubleChecked(); break;
case 4: finalInstance = Singleton.getInstanceHolder(); break;
}
System.out.println("Final singleton value: " + finalInstance.getValue());
}
}
}
7. Best Practices and Performance
Synchronization Best Practices
public class SynchronizationBestPractices {
// 1. Use private final lock objects
static class PrivateLock {
private final Object lock = new Object();
private int data;
public void updateData() {
synchronized (lock) { // Good: using private lock
data++;
System.out.println("Data updated to: " + data);
}
}
}
// 2. Avoid synchronizing on non-final objects
static class NonFinalLock {
private Object lock = new Object(); // Bad: non-final
public void badMethod() {
synchronized (lock) {
// lock could be reassigned, breaking synchronization
}
}
}
// 3. Keep synchronized blocks small
static class SmallCriticalSections {
private int counter;
private final Object lock = new Object();
// Good: small synchronized block
public void efficientIncrement() {
// Non-critical work outside synchronized block
performValidation();
synchronized (lock) {
// Only critical section is synchronized
counter++;
}
// More non-critical work
performLogging();
}
// Bad: large synchronized block
public void inefficientIncrement() {
synchronized (lock) {
performValidation(); // This doesn't need synchronization
counter++;
performLogging(); // This doesn't need synchronization
}
}
private void performValidation() {
// Simulate validation work
try { Thread.sleep(10); } catch (InterruptedException e) {}
}
private void performLogging() {
// Simulate logging work
try { Thread.sleep(10); } catch (InterruptedException e) {}
}
}
// 4. Use different locks for independent operations
static class MultipleLocks {
private final Object readLock = new Object();
private final Object writeLock = new Object();
private int readCount = 0;
private int writeCount = 0;
public void readOperation() {
synchronized (readLock) {
readCount++;
System.out.println("Read operation #" + readCount);
}
}
public void writeOperation() {
synchronized (writeLock) {
writeCount++;
System.out.println("Write operation #" + writeCount);
}
}
}
// 5. Avoid nested synchronization (deadlock risk)
static class NestedSynchronization {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
// Dangerous: nested synchronization
public void dangerousMethod() {
synchronized (lock1) {
System.out.println("Acquired lock1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock2) { // Potential deadlock
System.out.println("Acquired lock2");
}
}
}
// Safe: use ordered locking
public void safeMethod(Object obj1, Object obj2) {
Object firstLock = System.identityHashCode(obj1) < System.identityHashCode(obj2) ? obj1 : obj2;
Object secondLock = System.identityHashCode(obj1) < System.identityHashCode(obj2) ? obj2 : obj1;
synchronized (firstLock) {
synchronized (secondLock) {
// Critical section
}
}
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Synchronization Best Practices ===");
// Test efficient vs inefficient synchronization
SmallCriticalSections example = new SmallCriticalSections();
System.out.println("Testing efficient synchronization...");
long start = System.currentTimeMillis();
Thread t1 = new Thread(() -> { for (int i = 0; i < 10; i++) example.efficientIncrement(); });
Thread t2 = new Thread(() -> { for (int i = 0; i < 10; i++) example.efficientIncrement(); });
t1.start();
t2.start();
t1.join();
t2.join();
long efficientTime = System.currentTimeMillis() - start;
System.out.println("Testing inefficient synchronization...");
start = System.currentTimeMillis();
Thread t3 = new Thread(() -> { for (int i = 0; i < 10; i++) example.inefficientIncrement(); });
Thread t4 = new Thread(() -> { for (int i = 0; i < 10; i++) example.inefficientIncrement(); });
t3.start();
t4.start();
t3.join();
t4.join();
long inefficientTime = System.currentTimeMillis() - start;
System.out.println("Efficient time: " + efficientTime + "ms");
System.out.println("Inefficient time: " + inefficientTime + "ms");
System.out.println("Improvement: " +
((inefficientTime - efficientTime) * 100.0 / inefficientTime) + "%");
}
}
Key Points Summary
- Synchronized Methods: Lock on
this(instance) orClassobject (static) - Synchronized Blocks: More flexible, can use any object as lock
- Intrinsic Locks: Every object has an intrinsic lock (monitor)
- Reentrant: Same thread can acquire the same lock multiple times
- Memory Visibility: Synchronization ensures visibility changes across threads
Best Practices
- Use private final lock objects instead of synchronizing on
this - Keep synchronized blocks small to minimize contention
- Use different locks for independent operations
- Avoid nested synchronization to prevent deadlocks
- Consider higher-level concurrency utilities (
java.util.concurrent) for complex scenarios
Synchronization is fundamental for thread safety in Java, but should be used judiciously to avoid performance issues and deadlocks.