Last Updated on 13 August 2019   |   Print Email
In this Java tutorial series about synchronization, we will help you grasp the concepts and practical experience about synchronization in the context of concurrent programming. In this first part, let’s see how multiple threads updating the same data can cause problems.In a multi-threaded application, several threads can access the same data concurrently, which may leave the data in inconsistent state (corrupted or inaccurate). Let’s find out how multi-thread access can be a source of problems by going through an example that demonstrates the processing of transactions in a bank.Suppose that we have a class that represents an account in the bank as follows:
/**
* Account.java
* This class represents an account in the bank
* @author www.codejava.net
*/
public class Account {
private int balance = 0;
public Account(int balance) {
this.balance = balance;
}
public void withdraw(int amount) {
this.balance -= amount;
}
public void deposit(int amount) {
this.balance += amount;
}
public int getBalance() {
return this.balance;
}
}
The balance of an account can be changed frequently due to the transactions of deposit and withdrawal.The following code represents a bank that manages some accounts:
/**
* Bank.java
* This class represents a bank that manages accounts and provides
* money transfer function.
* @author www.codejava.net
*/
public class Bank {
public static final int MAX_ACCOUNT = 10;
public static final int MAX_AMOUNT = 10;
public static final int INITIAL_BALANCE = 100;
private Account[] accounts = new Account[MAX_ACCOUNT];
public Bank() {
for (int i = 0; i < accounts.length; i++) {
accounts[i] = new Account(INITIAL_BALANCE);
}
}
public void transfer(int from, int to, int amount) {
if (amount <= accounts[from].getBalance()) {
accounts[from].withdraw(amount);
accounts[to].deposit(amount);
String message = "%s transfered %d from %s to %s. Total balance: %d\n";
String threadName = Thread.currentThread().getName();
System.out.printf(message, threadName, amount, from, to, getTotalBalance());
}
}
public int getTotalBalance() {
int total = 0;
for (int i = 0; i < accounts.length; i++) {
total += accounts[i].getBalance();
}
return total;
}
}
As you can see, this bank consists of 10 accounts for each is initialized with a balance amount of 100. So the total balance of these 10 accounts is 10 x 100 = 1000.The transfer() method withdraws a specified amount from an account and deposit that amount to the target account. The transfer will be processed if and only if the source account has enough balance. And after the transfer has been done, a log message is printed to let us know the transaction details.The getTotalBalance() method returns the total amount of all accounts, which must be always 1000. We check this number after every transaction to make sure that the program runs correctly.As the bank allows many transactions to happen at the same time, the following class represents a transaction:
/**
* Transaction.java
* This class represents a transaction task that can be executed by a thread.
* @author www.codejava.net
*/
public class Transaction implements Runnable {
private Bank bank;
private int fromAccount;
public Transaction(Bank bank, int fromAccount) {
this.bank = bank;
this.fromAccount = fromAccount;
}
public void run() {
while (true) {
int toAccount = (int) (Math.random() * Bank.MAX_ACCOUNT);
if (toAccount == fromAccount) continue;
int amount = (int) (Math.random() * Bank.MAX_AMOUNT);
if (amount == 0) continue;
bank.transfer(fromAccount, toAccount, amount);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
As you can see, this Transaction class implements the Runnable interface so the code in its run() method can be executed by a separate thread.The source account is passed from the constructor and the target account is chosen randomly, and both source and target cannot be the same. Also the amount to be transferred is chosen randomly but always less than 10. After the transaction has been done, the current thread goes to sleep for a very short time (50 milliseconds), and then it continues doing the same steps repeatedly until the thread is terminated.And here’s the test program:
/**
* TransactionTest.java
* This is a test program that creates threads to process
* many transactions concurrently.
* @author www.codejava.net
*/
public class TransactionTest {
public static void main(String[] args) {
Bank bank = new Bank();
for (int i = 0; i < Bank.MAX_ACCOUNT; i++) {
Thread t = new Thread(new Transaction(bank, i));
t.start();
}
}
}
As you can see, a Bank instance is created and shared among the threads that perform the transactions. For each account, a new thread is created to transfer money from that account to other randomly chosen accounts. That means there are total 10 threads sharing one instance of Bank class. These threads will run forever until the program is terminated by pressing Ctrl + C.Remember this rule: No matter how many transactions are processed, the total balance of all accounts must remain unchanged. In other words, the program must consistently report this number as 1000.Now, let’s compile and run the TransactionTest program and observe the output. Initially you should see some output like this:The total balance is reported as 1000 consistently.But wait! Let the program continues running longer, you quickly see a problem happens:Ouch! Somehow the total balance is getting changed. It doesn’t remain at 1000 anymore. It’s getting smaller and smaller over time. Why did this happen?There must be something wrong with the program. Let’s analyze the code to find out why.Look at the Transaction class, you see multiple threads execute the transfer() method of the shared instance of the Bank class:
bank.transfer(fromAccount, toAccount, amount);
This method is implemented as follows:
public void transfer(int from, int to, int amount) {
if (amount <= accounts[from].getBalance()) {
accounts[from].withdraw(amount);
accounts[to].deposit(amount);
// code to print the log message…
}
}
Suppose that the account #1 has balance of 5 after some transactions. The thread #1 is executing the if statement to verify that account has sufficient fund to transfer and amount of 3. Since the account’s balance is 5, the thread #1 enters the body of the if block.But just before the thread #1 executes the statement to withdraw:
accounts[from].withdraw(amount);
another thread (say thread #2) has performed a transaction that withdraws an amount of 4 from the account #1. Now the thread #1 executes the withdraw operation and at this time the balance is 5 - 4 = 1, which is no longer seen as 5 by the thread #1. Hence the balance of account #1 is now 1 - 3 = -2. The balance is negative, so that’s why when the program calculates the total balance again, it gets decreased!If you keep running the program longer and longer, you will see the total balance can get smaller and smaller:That means the shared data may get corrupted when it is updated by multiple threads concurrently.A similar problem can happen with the deposit operation. Suppose that the thread #3 is about to add an amount of 8 to the account #3. Before adding, the thread #3 sees the balance of this account is 10. But just before the thread #3 updates the balance, another thread (say thread #4) performs a withdrawal of an amount of 5 on this account, so its balance is 10 - 5 = 5.In the mean time, the thread #3 still sees the balance is 10 so it adds 8 to 10 which results the balance of the account #3 is 18. But an amount of 5 has been added to another account, which means the total balance gets increased by 5. That’s why you may also see that the total balance gets increased over time when the program keeps running, as shown in the screenshot below:Let run the test program several times and observe the output yourself. The output is unpredicted: sometimes you see the total balance gets increased, sometimes it gets decreased, and sometimes it goes up and goes down, whatever!Also try to change the sleep time in the Transaction class. The longer time, the total balance gets changed slower. And the shorter time, the total balance gets changed faster.So what should we do to fix this problem?We need a mechanism that is able to guarantee that code in the transfer() method is executed by only one thread at a time. In other words, we need to synchronize access to shared data.Now you understand what kind of problem may happen with unsynchronized code. I will show you the first solution in the next part 2 of this tutorial series.NEXT: Java Synchronization Tutorial Part 2 - Using Lock and Condition Objects
Nam Ha Minh is certified Java programmer (SCJP and SCWCD). He began programming with Java back in the days of Java 1.4 and has been passionate about it ever since. You can connect with him on Facebook and watch his Java videos on YouTube.
Comments
You can change the values, test and measure.