In most practical multithreaded applications, two or more threads need to share access to the same data, What
happens if two threads have access to the same object and each calls a method that modifies the state of the
object? As you might imagine, the threads can step on each other's toes. Depending on the order in which the
data were accessed, corrupted objects can result. Such a situation is often called a "Race Condition".
I recommend that readers first consider the following program. It is best to practice this simple program; and in the next paragraph, I will explain the "Race Condition".
The simple program simulates funds transfering from one account to another in a bank. We simulated an array of accounts transfering funds to each other randomly.
package ConcurrentTest;
import java.util.Arrays;
/**
* Created by lenovo on 2018/6/13.
*/
public class Bank {
private final double[] accounts;
public Bank(int n, double initialBalance) {
accounts = new double[n];
Arrays.fill(accounts, initialBalance);
}
public void transfer(int from, int to, double amount) {
if (accounts[from] < amount) {
return;
}
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f\n", getTotalBalance());
}
private double getTotalBalance() {
double sum = 0;
for(double account : accounts) {
sum += account;
}
return sum;
}
public int size() {
return accounts.length;
}
}
package ConcurrentTest;
/**
* Created by lenovo on 2018/6/13.
*/
public class UnsynchBankTest {
public static final int NACCOUNTS = 100;
public static final double INITIAL_BALANCE = 1000;
public static final double MAX_AMOUNT = 1000;
public static final int DELAY = 10;
public static void main(String[] args) {
Bank bank = new Bank(NACCOUNTS, INITIAL_BALANCE);
for (int i = 0; i < bank.size(); i++) {
int fromAccount = i;
Runnable r = () -> {
try {
while (true) {
int toAccount = (int) (bank.size() * Math.random());
double amount = MAX_AMOUNT * Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((int) (DELAY * Math.random()));
}
} catch (Throwable e) {
Thread.currentThread().interrupt();
}
};
new Thread(r).start();
}
}
}
About the previous program, the "Race Condition" occurs when two threads are simultaneously trying to update an account. Support two threads simultaneously carry out the instruction:
accounts[to] += amount;
The problem is that these are not atomic operations. The instruction might be processed as follows:
1. Load "account[to]" into a register.
2. Add "amount".
3. Move the result back to "account[to]".
Now, suppose the first thread executes Steps 1 and 2, and then it is preempted.
Suppose the second thread awakens and updates the same entry in the "account" array. Then, the first thread awakens and completes its Step 3.That action wipes out the modification of the other thread. As a result, the total is no longer correct.
NOTE: You can actually peek at the virtual machine bytecodes that execute each statement in our class. Run the command:
javap -c -v Bank
to decompile the "Bank.class" file. For example, the line
accounts[to] += amount;
is translated into the following bytecodes:
aload_0
getfield #2; //Field accounts:[D
iload_2
dup2
daload
dload_3
dadd
dastore
What these codes mean does not matter.
The point is that the increment command is made up of several instructions, and the thread executing them can be interrupted at any instruction.