In Java, a shared object is typically an object that can be accessed by multiple threads simultaneously. To handle such situations, you often need to synchronize access to ensure thread safety.
Shared Objects in Java
-
 A shared object refers to an instance of a class that can be accessed and modified by multiple threads concurrently. When multiple threads interact with the same object, proper synchronization is required to avoid data inconsistency or corruption.
-
Synchronization: Synchronization ensures that only one thread can access a shared object’s critical section (like a method or block of code) at a time. This is necessary to ensure thread safety when multiple threads are reading or writing to shared data.
-
Synchronized Methods: A method can be synchronized using the
synchronized
keyword to prevent concurrent access by multiple threads. -
Synchronized Blocks: You can synchronize smaller portions of code within a method to improve performance by limiting the scope of synchronization.
-
-
volatile
Keyword: Thevolatile
keyword is used to ensure that updates to a variable by one thread are immediately visible to other threads. It is mainly used with simple types likeint
orboolean
in shared objects to ensure visibility across threads without full synchronization. -
Reentrant Locks: Instead of using the
synchronized
keyword, you can useReentrantLock
fromjava.util.concurrent.locks
. This gives you more control over locking and unlocking shared resources, allowing for features like try-lock or timed lock acquisition. -
Atomic Variables: Java provides atomic classes (like
AtomicInteger
,AtomicLong
, etc.) injava.util.concurrent.atomic
package to perform thread-safe operations on primitive data types. These classes offer methods likeget()
,set()
,incrementAndGet()
, which perform atomic operations without needing explicit synchronization.
Program Implementation
A bank account where multiple threads can deposit and withdraw money. We’ll use synchronization to ensure that the balance is correctly updated even when multiple threads try to modify it at the same time.
Here using synchronized methods
//BankAccount.java public class BankAccount { private double balance; public BankAccount(double initialBalance) { this.balance = initialBalance; } public synchronized void deposit(double amount) { if (amount > 0) { balance += amount; System.out.println(Thread.currentThread().getName() + " deposited " + amount + ". New balance: " + balance); } } public synchronized void withdraw(double amount) { if (amount > 0 && balance >= amount) { balance -= amount; System.out.println(Thread.currentThread().getName() + " withdrew " + amount + ". New balance: " + balance); } else { System.out.println(Thread.currentThread().getName() + " tried to withdraw " + amount + " but insufficient balance. Current balance: " + balance); } } public double getBalance() { return balance; } public static void main(String[] args) { BankAccount account = new BankAccount(1000); Runnable depositor = () -> { for (int i = 0; i < 5; i++) { account.deposit(100); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } }; Runnable withdrawer = () -> { for (int i = 0; i < 5; i++) { account.withdraw(50); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } }; Thread depositorThread1 = new Thread(depositor, "DepositorThread1"); Thread depositorThread2 = new Thread(depositor, "DepositorThread2"); Thread withdrawerThread1 = new Thread(withdrawer, "WithdrawerThread1"); Thread withdrawerThread2 = new Thread(withdrawer, "WithdrawerThread2"); depositorThread1.start(); depositorThread2.start(); withdrawerThread1.start(); withdrawerThread2.start(); try { depositorThread1.join(); depositorThread2.join(); withdrawerThread1.join(); withdrawerThread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Final balance: " + account.getBalance()); } } /* C:\>javac BankAccount.java C:\>java BankAccount DepositorThread1 deposited 100.0. New balance: 1100.0 WithdrawerThread2 withdrew 50.0. New balance: 1050.0 WithdrawerThread1 withdrew 50.0. New balance: 1000.0 DepositorThread2 deposited 100.0. New balance: 1100.0 DepositorThread1 deposited 100.0. New balance: 1200.0 WithdrawerThread1 withdrew 50.0. New balance: 1150.0 WithdrawerThread2 withdrew 50.0. New balance: 1100.0 DepositorThread2 deposited 100.0. New balance: 1200.0 WithdrawerThread1 withdrew 50.0. New balance: 1150.0 DepositorThread2 deposited 100.0. New balance: 1250.0 DepositorThread1 deposited 100.0. New balance: 1350.0 WithdrawerThread2 withdrew 50.0. New balance: 1300.0 DepositorThread2 deposited 100.0. New balance: 1400.0 WithdrawerThread1 withdrew 50.0. New balance: 1350.0 WithdrawerThread2 withdrew 50.0. New balance: 1300.0 DepositorThread1 deposited 100.0. New balance: 1400.0 WithdrawerThread1 withdrew 50.0. New balance: 1350.0 WithdrawerThread2 withdrew 50.0. New balance: 1300.0 DepositorThread1 deposited 100.0. New balance: 1400.0 DepositorThread2 deposited 100.0. New balance: 1500.0 Final balance: 1500.0 */
In Java, managing shared objects correctly is crucial for developing reliable and thread-safe applications. Without proper synchronization, multiple threads accessing the same object can lead to unpredictable behavior and data inconsistency.
Choosing the right synchronization approach depends on the complexity and performance needs of the application. Correctly synchronizing access to shared objects ensures data integrity, avoids race conditions, and leads to robust multi-threaded programs.