How Synchronization Works in Java (Part 1)
This introductory primer to Java synchronization will discuss how synchronization solves problems and introduces locks.
Join the DZone community and get the full member experience.
Join For FreeIf you've decided to learn synchronization in Java, then let's start without wasting time. This is the first part of a series of articles on synchronization.
Definition
Synchronization means to control the access of multiple threads to a shared resource.
How Is It Done?
For practice purposes, I would recommend having Eclipse installed on your PC/laptop. This will help you to understand the threads and their states more easily.
Open your Eclipse
Go to File -> New -> Java Project
Give it a name and click finish.
A project will be created in your workspace
Now in your project--> src folder, right click on it
Select New -> Package
Give it a name and click finish. The package will be created inside your src folder
Right-click on your package, select New -> Class
Enter class name as "Main"
Tick the checkbox "public static void main"
Click finish
The main class has been created!
Now we will create a basic banking application in which 2 customers (Threads) will try to deposit/withdraw an amount from a single bank account
Create the class Bank as follows:
public class Bank {
private static Bank instance = new Bank();
private HashMap<Integer, BankAccount> accountNumberVsAccount;
private Bank() {
accountNumberVsAccount = new HashMap<Integer, BankAccount>();
accountNumberVsAccount.put(123456, new BankAccount(123456));
}
public static Bank getInstance(){
return instance;
}
public BankAccount getAccount(Integer accountNumber) {
return accountNumberVsAccount.get(accountNumber);
}
}
The Bank
class contains a map of accountNumberVsAccount
.
It has a method getAccount
, which simply returns the BankAccount instance of the account number.
Now we will create the BankAccount Class:
public class BankAccount {
private Integer balance;
private Integer accountNumber;
public BankAccount(Integer accountNumber, Integer initialBalance) {
this.accountNumber = accountNumber;
balance = initialBalance;
}
public BankAccount(Integer accountNumber) {
this(accountNumber, 0);
}
public Integer getBalance() {
return balance;
}
public Integer getAccountNumber() {
return accountNumber;
}
public void deposit(Integer amount) {
balance = balance + amount;
System.out.println(Thread.currentThread().getName() + " depositing the amount "+amount+" updated balance = " + balance);
}
public Integer withdraw(Integer amount) {
System.out.println(Thread.currentThread().getName() + " trying to withdraw " + amount + " from the account " + accountNumber);
if (balance < amount) {
System.out.println("OOPS, NO BALANCE LEFT TO WITHDRAW FOR "+Thread.currentThread().getName());
return 0;
}
balance = balance - amount;
System.out.println(Thread.currentThread().getName() + " successfully withdrow the amount. balance left = " + balance);
return amount;
}
}
Let's understand this class.
The BankAccount
contains its accountNumber
and a balance
, and two methods (withdraw
and deposit
) are there for updating the balance of the account. Also, if the balance is not enough, then the value 0
is returned with an error message.
Now let's create the Customer
class (Runnable
):
public class Customer implements Runnable {
@Override
public void run() {
Bank bank = Bank.getInstance();
BankAccount account = bank.getAccount(123456);
account.deposit(100);
account.withdraw(200);
}
}
Let's see what the customer is doing. When it runs:
It gets the
Bank
instanceIt gets the
BankAccount
Object from thebank
It deposits
100
bucks to the accountAnd finally, it withdraws
200
bucks from the account
Now, what can happen if 2 customers (2 instances of the Customer class) run at the same time? Both will get the SAME
BankAccount
Object from the Bank.Then both will deposit 100 bucks to the account. The balance should be 200.
Now both customers will try to withdraw 200, but only one customer should be able to withdraw 200 (think it as in a real-world scenario).
Because no balance is left in that account, another customer should not be able to withdraw the amount
Now we will write the code for main class and produce the same scenario stated above
public class Main {
public static void main(String[] args) {
Customer customer1 = new Customer();
Customer customer2 = new Customer();
Thread t1 = new Thread(customer1);
Thread t2 = new Thread(customer2);
t1.setName("Customer-1");
t2.setName("Customer-2");
t1.start();
t2.start();
}
}
Now Run the main class 3-4 times and analyze the inconsistent output. You should see that the behavior is not consistent. Sometimes, one customer is able to withdraw, and sometimes, both customers are not able to withdraw. Ideally, every time, one customer should be able to withdraw the amount from the account. But that is not what is happening. This is the problem, and that is where synchronization
comes into the picture.
To make this behavior consistent and to make it work as expected, we need to synchronize the BankAccount
object before doing any operations (deposit/withdraw) on it.
To synchronize an object, we need to write:
synchronized (object) {
// do some operations on obj
}
So in our customer class, we will make the following changes:
@Override
public void run() {
Bank bank = Bank.getInstance();
BankAccount account = bank.getAccount(123456);
// obtain a lock on the account before performing operations
synchronized (account) {
account.deposit(100);
account.withdraw(200);
}
}
This means that the current thread (Customer) needs to obtain a lock on the account object before doing any operations on it.
Try running the application now: Every time, the output will be correct — one customer will be able to withdraw the amount each time.
How it Happened
For a deeper understanding it, let's debug the application in Eclipse. Put the breakpoint in the Customer class as shown:
Now, right click on the main class and select debug as -> Java application
The application will start in debug mode, and the Debug perspective will be opened as below:
See the debug view in the top left corner. This shows the current running threads in our application. (See the highlighted lines below):
You can select a thread by clicking on it, and then resume it, stop it etc.
Suppose I select thread customer-1 and press F6. The thread will go into a synchronized block. See the debug view now:
A new line appeared that says:
owns: BankAccount (id=29)
This means that this thread (Customer-1) has obtained a lock on that object of BankAccount, so if another thread tries to obtain the lock on that object, the thread will get blocked. Let's try it.
Select the thread Customer-2 (click on it) and then press F6. Did it go into the synchronized block? See the debug view:
The answer is no, it did not enter into a synchronized block (still on line 9). Its state became Stepping
instead of Suspended
, which means it is blocked now.
Now let's get customer-1 out of the synchronized block (by selecting it and pressing F6 3 times). It will leave the synchronized block — notice now that the Customer-2 thread instantly moved into the suspended state and obtained the lock (see in the debug view):
That means the Customer-2 thread entered into the synchronized block instantly after Customer-1 got out of the block.
REMEMBER: Only ONE thread at a time can execute a synchronized block on the SAME object.
Questions are welcome.
Opinions expressed by DZone contributors are their own.
Comments