1. Introduction to Synchronized Methods
What are Synchronized Methods?
Synchronized methods are methods that can be executed by only one thread at a time for a given object instance. They provide thread safety by preventing multiple threads from accessing critical sections simultaneously.
Why Use Synchronized Methods?
- Prevent race conditions
- Ensure data consistency
- Maintain thread safety
- Avoid corrupted state
- Provide mutual exclusion
The Problem Without Synchronization
class UnsafeCounter {
private int count = 0;
// NOT synchronized - DANGEROUS!
public void increment() {
count++; // This is NOT atomic!
}
public int getCount() {
return count;
}
}
public class WithoutSynchronization {
public static void main(String[] args) throws InterruptedException {
UnsafeCounter counter = new UnsafeCounter();
// Create multiple threads that increment the 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();
// Expected: 2000, but often gets less due to race condition
System.out.println("Final count: " + counter.getCount());
System.out.println("Race condition occurred! Expected: 2000, Got: " + counter.getCount());
}
}
2. synchronized Keyword Syntax
Instance Synchronized Method
public synchronized void methodName() {
// Critical section - only one thread can execute this at a time per instance
}
Static Synchronized Method
public static synchronized void methodName() {
// Critical section - only one thread can execute this at a time per class
}
3. Complete Code Examples
Example 1: Basic Synchronized Counter
class SynchronizedCounter {
private int count = 0;
// Synchronized instance method
public synchronized void increment() {
count++; // Now this is thread-safe!
}
// Synchronized getter
public synchronized int getCount() {
return count;
}
// Synchronized method with complex logic
public synchronized void complexOperation() {
System.out.println(Thread.currentThread().getName() + " entered complexOperation");
try {
// Simulate some work
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
count += 5;
System.out.println(Thread.currentThread().getName() + " completed complexOperation");
}
}
public class BasicSynchronizedExample {
public static void main(String[] args) throws InterruptedException {
SynchronizedCounter counter = new SynchronizedCounter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
}, "Thread-1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
}, "Thread-2");
Thread t3 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
counter.complexOperation();
}
}, "Thread-3");
System.out.println("Starting all threads...");
long startTime = System.currentTimeMillis();
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
long endTime = System.currentTimeMillis();
System.out.println("Final count: " + counter.getCount()); // Always 2000 + 25
System.out.println("Time taken: " + (endTime - startTime) + "ms");
System.out.println("No race condition! Perfect synchronization.");
}
}
Example 2: Static Synchronized Methods
class Bank {
private static double totalBankBalance = 100000;
private String accountHolder;
private double balance;
public Bank(String accountHolder, double initialBalance) {
this.accountHolder = accountHolder;
this.balance = initialBalance;
}
// Static synchronized method - locks on Bank.class
public static synchronized void updateTotalBalance(double amount) {
System.out.println(Thread.currentThread().getName() + " updating total balance by: " + amount);
try {
Thread.sleep(100); // Simulate processing time
} catch (InterruptedException e) {
e.printStackTrace();
}
totalBankBalance += amount;
System.out.println("Total bank balance updated to: " + totalBankBalance);
}
// Instance synchronized method - locks on this instance
public synchronized void transfer(Bank recipient, double amount) {
System.out.println(Thread.currentThread().getName() + " transferring " + amount +
" from " + accountHolder + " to " + recipient.accountHolder);
if (balance >= amount) {
balance -= amount;
recipient.balance += amount;
// Update total bank balance (static synchronized)
updateTotalBalance(0); // Just for demonstration
System.out.println("Transfer successful!");
System.out.println(accountHolder + " new balance: " + balance);
System.out.println(recipient.accountHolder + " new balance: " + recipient.balance);
} else {
System.out.println("Insufficient funds!");
}
}
public synchronized double getBalance() {
return balance;
}
public static synchronized double getTotalBankBalance() {
return totalBankBalance;
}
}
public class StaticSynchronizedExample {
public static void main(String[] args) throws InterruptedException {
Bank alice = new Bank("Alice", 5000);
Bank bob = new Bank("Bob", 3000);
Bank charlie = new Bank("Charlie", 7000);
// Multiple transfers happening concurrently
Thread t1 = new Thread(() -> {
alice.transfer(bob, 1000);
}, "Transfer-1");
Thread t2 = new Thread(() -> {
bob.transfer(charlie, 500);
}, "Transfer-2");
Thread t3 = new Thread(() -> {
charlie.transfer(alice, 2000);
}, "Transfer-3");
Thread t4 = new Thread(() -> {
Bank.updateTotalBalance(5000); // Deposit to bank
}, "Bank-Update");
System.out.println("Initial total bank balance: " + Bank.getTotalBankBalance());
t1.start();
t2.start();
t3.start();
t4.start();
t1.join();
t2.join();
t3.join();
t4.join();
System.out.println("\nFinal Balances:");
System.out.println("Alice: " + alice.getBalance());
System.out.println("Bob: " + bob.getBalance());
System.out.println("Charlie: " + charlie.getBalance());
System.out.println("Total Bank Balance: " + Bank.getTotalBankBalance());
}
}
Example 3: Producer-Consumer with Synchronized Methods
class MessageQueue {
private String message;
private boolean hasMessage = false;
// Producer method - synchronized
public synchronized void put(String message) throws InterruptedException {
// Wait until the message has been consumed
while (hasMessage) {
System.out.println(Thread.currentThread().getName() + " waiting to put message...");
wait(); // Releases lock and waits
}
// Produce new message
this.message = message;
hasMessage = true;
System.out.println(Thread.currentThread().getName() + " produced: " + message);
notifyAll(); // Notify all waiting consumers
}
// Consumer method - synchronized
public synchronized String take() throws InterruptedException {
// Wait until a message is available
while (!hasMessage) {
System.out.println(Thread.currentThread().getName() + " waiting for message...");
wait(); // Releases lock and waits
}
// Consume the message
hasMessage = false;
System.out.println(Thread.currentThread().getName() + " consumed: " + message);
notifyAll(); // Notify all waiting producers
return message;
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
MessageQueue queue = new MessageQueue();
// Producer thread
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
queue.put("Message-" + i);
Thread.sleep(1000); // Simulate production time
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Producer");
// Consumer thread
Thread consumer = new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
String message = queue.take();
Thread.sleep(1500); // Simulate consumption time
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Consumer");
System.out.println("Starting Producer-Consumer...");
producer.start();
consumer.start();
try {
producer.join();
consumer.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Producer-Consumer completed!");
}
}
4. Real-World Example: Bank Account Management
class BankAccount {
private final String accountNumber;
private String accountHolder;
private double balance;
public BankAccount(String accountNumber, String accountHolder, double initialBalance) {
this.accountNumber = accountNumber;
this.accountHolder = accountHolder;
this.balance = initialBalance;
}
// Synchronized deposit method
public synchronized void deposit(double amount) {
if (amount > 0) {
double oldBalance = balance;
balance += amount;
System.out.println(Thread.currentThread().getName() +
" deposited: " + amount +
" | Balance: " + oldBalance + " -> " + balance);
} else {
System.out.println("Invalid deposit amount!");
}
}
// Synchronized withdraw method
public synchronized void withdraw(double amount) throws InsufficientFundsException {
if (amount <= 0) {
System.out.println("Invalid withdrawal amount!");
return;
}
if (balance >= amount) {
double oldBalance = balance;
balance -= amount;
System.out.println(Thread.currentThread().getName() +
" withdrew: " + amount +
" | Balance: " + oldBalance + " -> " + balance);
} else {
throw new InsufficientFundsException("Insufficient funds! Available: " + balance + ", Required: " + amount);
}
}
// Synchronized transfer method
public synchronized void transferTo(BankAccount targetAccount, double amount) throws InsufficientFundsException {
if (amount <= 0) {
System.out.println("Invalid transfer amount!");
return;
}
System.out.println(Thread.currentThread().getName() +
" initiating transfer: " + amount +
" from " + accountHolder + " to " + targetAccount.accountHolder);
if (balance >= amount) {
// Withdraw from this account
balance -= amount;
// Need to acquire target account lock to avoid deadlock
// This demonstrates the limitation of synchronized methods
synchronized(targetAccount) {
targetAccount.balance += amount;
}
System.out.println("Transfer successful!");
System.out.println(accountHolder + " new balance: " + balance);
System.out.println(targetAccount.accountHolder + " new balance: " + targetAccount.balance);
} else {
throw new InsufficientFundsException("Transfer failed! Insufficient funds.");
}
}
// Synchronized getters
public synchronized double getBalance() {
return balance;
}
public synchronized String getAccountInfo() {
return "Account: " + accountNumber + " | Holder: " + accountHolder + " | Balance: " + balance;
}
}
class InsufficientFundsException extends Exception {
public InsufficientFundsException(String message) {
super(message);
}
}
public class BankAccountExample {
public static void main(String[] args) throws InterruptedException {
BankAccount aliceAccount = new BankAccount("ACC001", "Alice", 5000);
BankAccount bobAccount = new BankAccount("ACC002", "Bob", 3000);
System.out.println("Initial state:");
System.out.println(aliceAccount.getAccountInfo());
System.out.println(bobAccount.getAccountInfo());
System.out.println();
// Multiple concurrent operations
Thread depositThread1 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
aliceAccount.deposit(100);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Deposit-Thread-1");
Thread depositThread2 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
bobAccount.deposit(50);
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Deposit-Thread-2");
Thread withdrawThread = new Thread(() -> {
for (int i = 0; i < 2; i++) {
try {
aliceAccount.withdraw(200);
Thread.sleep(300);
} catch (InterruptedException | InsufficientFundsException e) {
System.out.println("Withdrawal failed: " + e.getMessage());
}
}
}, "Withdraw-Thread");
Thread transferThread = new Thread(() -> {
try {
aliceAccount.transferTo(bobAccount, 1000);
Thread.sleep(500);
} catch (InterruptedException | InsufficientFundsException e) {
System.out.println("Transfer failed: " + e.getMessage());
}
}, "Transfer-Thread");
// Start all threads
depositThread1.start();
depositThread2.start();
withdrawThread.start();
transferThread.start();
// Wait for all threads to complete
depositThread1.join();
depositThread2.join();
withdrawThread.join();
transferThread.join();
System.out.println("\nFinal state:");
System.out.println(aliceAccount.getAccountInfo());
System.out.println(bobAccount.getAccountInfo());
}
}
5. Synchronized Methods vs Synchronized Blocks
class ComparisonExample {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
private int count1 = 0;
private int count2 = 0;
// Synchronized method - locks on 'this'
public synchronized void incrementWithMethod() {
count1++;
try {
Thread.sleep(100); // Simulate work
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// Synchronized block with custom lock
public void incrementWithBlock() {
synchronized(lock1) {
count2++;
try {
Thread.sleep(100); // Simulate work
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// Different synchronized blocks for different resources
public void processResource1() {
synchronized(lock1) {
System.out.println(Thread.currentThread().getName() + " processing resource 1");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void processResource2() {
synchronized(lock2) {
System.out.println(Thread.currentThread().getName() + " processing resource 2");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized int getCount1() {
return count1;
}
public int getCount2() {
synchronized(lock1) {
return count2;
}
}
}
public class MethodVsBlockComparison {
public static void main(String[] args) throws InterruptedException {
ComparisonExample example = new ComparisonExample();
System.out.println("=== Synchronized Method vs Block ===");
// Test synchronized method
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.incrementWithMethod();
}
}, "Method-Thread-1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.incrementWithMethod();
}
}, "Method-Thread-2");
// Test synchronized block
Thread t3 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.incrementWithBlock();
}
}, "Block-Thread-1");
Thread t4 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.incrementWithBlock();
}
}, "Block-Thread-2");
// Test fine-grained locking
Thread t5 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
example.processResource1();
example.processResource2();
}
}, "FineGrained-1");
Thread t6 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
example.processResource1();
example.processResource2();
}
}, "FineGrained-2");
long startTime = System.currentTimeMillis();
t1.start(); t2.start(); t3.start(); t4.start(); t5.start(); t6.start();
t1.join(); t2.join(); t3.join(); t4.join(); t5.join(); t6.join();
long endTime = System.currentTimeMillis();
System.out.println("Count1 (synchronized method): " + example.getCount1());
System.out.println("Count2 (synchronized block): " + example.getCount2());
System.out.println("Total time: " + (endTime - startTime) + "ms");
}
}
6. Reentrant Synchronization
class ReentrantExample {
private int count = 0;
public synchronized void outerMethod() {
System.out.println(Thread.currentThread().getName() + " in outerMethod");
count++;
// This can call another synchronized method (reentrant)
innerMethod();
System.out.println(Thread.currentThread().getName() + " exiting outerMethod");
}
public synchronized void innerMethod() {
System.out.println(Thread.currentThread().getName() + " in innerMethod");
count++;
// Can call yet another synchronized method
anotherSynchronizedMethod();
}
public synchronized void anotherSynchronizedMethod() {
System.out.println(Thread.currentThread().getName() + " in anotherSynchronizedMethod");
count++;
}
public synchronized int getCount() {
return count;
}
}
public class ReentrantSynchronization {
public static void main(String[] args) throws InterruptedException {
ReentrantExample example = new ReentrantExample();
System.out.println("=== Reentrant Synchronization Demo ===");
Thread t1 = new Thread(() -> {
example.outerMethod();
}, "Thread-1");
Thread t2 = new Thread(() -> {
example.outerMethod();
}, "Thread-2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + example.getCount()); // Should be 6
System.out.println("Each thread increments count 3 times in nested synchronized calls");
}
}
7. Common Pitfalls and Best Practices
class CommonPitfalls {
// PITFALL 1: Synchronizing on different objects
private final Object correctLock = new Object();
private String data1 = "Data1";
private String data2 = "Data2";
// WRONG - synchronizing on different objects for related operations
public void updateDataWrong(String newData1, String newData2) {
synchronized(data1) {
data1 = newData1;
}
synchronized(data2) {
data2 = newData2;
}
}
// CORRECT - use same lock for related operations
public void updateDataCorrect(String newData1, String newData2) {
synchronized(correctLock) {
data1 = newData1;
data2 = newData2;
}
}
// PITFALL 2: Synchronized method in constructor (usually not needed)
public CommonPitfalls() {
// synchronized(this) { } // Generally not necessary
}
// PITFALL 3: Returning internal mutable objects
private List<String> internalList = new ArrayList<>();
// WRONG - returning internal mutable object
public synchronized List<String> getListWrong() {
return internalList; // Caller can modify without synchronization!
}
// CORRECT - return defensive copy or unmodifiable view
public synchronized List<String> getListCorrect() {
return new ArrayList<>(internalList); // Defensive copy
}
// OR return unmodifiable view
public synchronized List<String> getListUnmodifiable() {
return Collections.unmodifiableList(internalList);
}
}
class BestPractices {
// PRACTICE 1: Use private final lock objects
private final Object lock = new Object();
private int value = 0;
// Better than synchronized method for flexibility
public void increment() {
synchronized(lock) {
value++;
}
}
// PRACTICE 2: Keep synchronized blocks small
public void processData(String data) {
// Do non-critical work outside synchronized block
String processedData = data.trim().toUpperCase();
// Only synchronize the critical section
synchronized(lock) {
value += processedData.length();
// Keep synchronized section minimal
}
// More non-critical work
System.out.println("Processed: " + processedData);
}
// PRACTICE 3: Use synchronized wrappers for collections
private List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
public void addToSyncList(String item) {
synchronizedList.add(item); // Thread-safe
}
// But still need synchronization for compound actions!
public boolean addIfAbsent(String item) {
synchronized(synchronizedList) {
if (!synchronizedList.contains(item)) {
synchronizedList.add(item);
return true;
}
return false;
}
}
}
public class PitfallsAndPractices {
public static void main(String[] args) {
System.out.println("=== Common Pitfalls and Best Practices ===");
BestPractices practices = new BestPractices();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
practices.increment();
practices.addToSyncList("Item-" + i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
practices.increment();
practices.processData(" data " + i);
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("All operations completed successfully!");
}
}
8. Performance Considerations
class PerformanceExample {
private int fastCount = 0;
private int slowCount = 0;
private final Object fastLock = new Object();
// Fast: Minimal work in synchronized block
public void fastIncrement() {
// Non-critical work outside synchronized block
String threadName = Thread.currentThread().getName();
synchronized(fastLock) {
fastCount++; // Only critical operation synchronized
}
// More non-critical work
if (fastCount % 1000 == 0) {
System.out.println(threadName + " reached " + fastCount);
}
}
// Slow: Too much work in synchronized method
public synchronized void slowIncrement() {
slowCount++;
// Simulate expensive operation in synchronized context (BAD!)
try {
Thread.sleep(1); // This blocks all other threads!
} catch (InterruptedException e) {
e.printStackTrace();
}
String threadName = Thread.currentThread().getName();
if (slowCount % 1000 == 0) {
System.out.println(threadName + " reached " + slowCount);
}
}
public int getFastCount() {
synchronized(fastLock) {
return fastCount;
}
}
public synchronized int getSlowCount() {
return slowCount;
}
}
public class PerformanceComparison {
public static void main(String[] args) throws InterruptedException {
PerformanceExample example = new PerformanceExample();
System.out.println("=== Performance Comparison ===");
// Test fast approach
Thread[] fastThreads = new Thread[5];
long fastStart = System.currentTimeMillis();
for (int i = 0; i < fastThreads.length; i++) {
fastThreads[i] = new Thread(() -> {
for (int j = 0; j < 100; j++) {
example.fastIncrement();
}
}, "Fast-Thread-" + i);
fastThreads[i].start();
}
for (Thread t : fastThreads) {
t.join();
}
long fastEnd = System.currentTimeMillis();
// Test slow approach
Thread[] slowThreads = new Thread[5];
long slowStart = System.currentTimeMillis();
for (int i = 0; i < slowThreads.length; i++) {
slowThreads[i] = new Thread(() -> {
for (int j = 0; j < 100; j++) {
example.slowIncrement();
}
}, "Slow-Thread-" + i);
slowThreads[i].start();
}
for (Thread t : slowThreads) {
t.join();
}
long slowEnd = System.currentTimeMillis();
System.out.println("\nResults:");
System.out.println("Fast approach - Count: " + example.getFastCount() +
", Time: " + (fastEnd - fastStart) + "ms");
System.out.println("Slow approach - Count: " + example.getSlowCount() +
", Time: " + (slowEnd - slowStart) + "ms");
System.out.println("Performance difference: " +
((slowEnd - slowStart) - (fastEnd - fastStart)) + "ms");
}
}
9. Advanced Example: Thread-Safe Cache
import java.util.*;
import java.util.concurrent.*;
class ThreadSafeCache {
private final Map<String, String> cache = new HashMap<>();
private final Map<String, Long> accessTimes = new HashMap<>();
private final int maxSize;
public ThreadSafeCache(int maxSize) {
this.maxSize = maxSize;
}
// Synchronized put method
public synchronized void put(String key, String value) {
System.out.println(Thread.currentThread().getName() + " putting key: " + key);
// If cache is full, remove least recently used item
if (cache.size() >= maxSize && !cache.containsKey(key)) {
removeLeastRecentlyUsed();
}
cache.put(key, value);
accessTimes.put(key, System.currentTimeMillis());
System.out.println("Cache size: " + cache.size());
}
// Synchronized get method
public synchronized String get(String key) {
System.out.println(Thread.currentThread().getName() + " getting key: " + key);
String value = cache.get(key);
if (value != null) {
// Update access time
accessTimes.put(key, System.currentTimeMillis());
}
return value;
}
// Synchronized remove method
public synchronized void remove(String key) {
System.out.println(Thread.currentThread().getName() + " removing key: " + key);
cache.remove(key);
accessTimes.remove(key);
}
// Synchronized size method
public synchronized int size() {
return cache.size();
}
// Synchronized clear method
public synchronized void clear() {
cache.clear();
accessTimes.clear();
System.out.println("Cache cleared");
}
// Private helper method - already in synchronized context
private void removeLeastRecentlyUsed() {
String lruKey = null;
long oldestTime = Long.MAX_VALUE;
for (Map.Entry<String, Long> entry : accessTimes.entrySet()) {
if (entry.getValue() < oldestTime) {
oldestTime = entry.getValue();
lruKey = entry.getKey();
}
}
if (lruKey != null) {
System.out.println("Removing LRU key: " + lruKey);
cache.remove(lruKey);
accessTimes.remove(lruKey);
}
}
// Synchronized method to get cache stats
public synchronized Map<String, Object> getStats() {
Map<String, Object> stats = new HashMap<>();
stats.put("size", cache.size());
stats.put("keys", new ArrayList<>(cache.keySet()));
return stats;
}
}
public class CacheExample {
public static void main(String[] args) throws InterruptedException {
ThreadSafeCache cache = new ThreadSafeCache(3);
System.out.println("=== Thread-Safe Cache Example ===");
// Multiple threads accessing cache concurrently
Thread writer1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
cache.put("key-" + i, "value-" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Writer-1");
Thread writer2 = new Thread(() -> {
for (int i = 5; i < 10; i++) {
cache.put("key-" + i, "value-" + i);
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Writer-2");
Thread reader = new Thread(() -> {
for (int i = 0; i < 10; i++) {
String value = cache.get("key-" + (i % 5));
System.out.println("Read key-" + (i % 5) + ": " + value);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Reader");
Thread statsThread = new Thread(() -> {
for (int i = 0; i < 3; i++) {
Map<String, Object> stats = cache.getStats();
System.out.println("Cache Stats: " + stats);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Stats-Thread");
writer1.start();
writer2.start();
reader.start();
statsThread.start();
writer1.join();
writer2.join();
reader.join();
statsThread.join();
System.out.println("\nFinal cache size: " + cache.size());
System.out.println("Final cache stats: " + cache.getStats());
}
}
10. Conclusion
Key Takeaways:
- Thread Safety: Synchronized methods prevent race conditions
- Object Lock: Instance methods lock on
this, static methods lock onClassobject - Reentrant: A thread can acquire the same lock multiple times
- Automatic: synchronized keyword handles lock acquisition/release automatically
When to Use Synchronized Methods:
- ✅ Simple critical sections
- ✅ When the entire method needs synchronization
- ✅ Readability and maintainability are priorities
- ✅ For getters/setters of shared state
When to Use Synchronized Blocks Instead:
- ✅ Fine-grained locking needed
- ✅ Different locks for different resources
- ✅ Only part of method needs synchronization
- ✅ Better performance requirements
Best Practices:
- Keep synchronized sections as small as possible
- Use private final objects for explicit locking
- Be careful with nested synchronization to avoid deadlocks
- Consider higher-level concurrency utilities for complex scenarios
Common Mistakes to Avoid:
- ❌ Synchronizing on non-final objects
- ❌ Returning internal mutable state without protection
- ❌ Performing I/O or expensive operations in synchronized blocks
- ❌ Synchronizing on different objects for related operations
Final Thoughts:
Synchronized methods are the simplest way to achieve thread safety in Java, but they come with performance costs. Use them judiciously and consider alternatives like ReentrantLock, concurrent collections, or other java.util.concurrent utilities for more complex scenarios.
Master synchronized methods as they form the foundation of thread safety in Java, but always be mindful of performance implications and potential deadlocks!