Reputation: 31
I was recently trying to wrap my head around some Java multi-threading concepts and was writing a small piece of code to help me understand memory visibility and getting synchronization as correct as possible. Based on what I've read, it seems that the smaller the amount of code we hold a lock around, the more efficient our program will be (in general). I've written a small class to help me understand some of the synchronization issues I might run into:
public class BankAccount {
private int balance_;
public BankAccount(int initialBalance) {
if (initialBalance < 300) {
throw new IllegalArgumentException("Balance needs to be at least 300");
}
balance_ = initialBalance;
}
public void deposit(int amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Deposit has to be positive");
}
// should be atomic assignment
// copy should also be non-shared as it's on each thread's stack
int copy = balance_;
// do the work on the thread-local copy of the balance. This work should
// not be visible to other threads till below synchronization
copy += amount;
synchronized(this) {
balance_ = copy; // make the new balance visible to other threads
}
}
public void withdraw(int amount) {
// should be atomic assignment
// copy should also be non-shared as it's on each thread's stack
int copy = balance_;
if (amount > copy) {
throw new IllegalArgumentException("Withdrawal has to be <= current balance");
}
copy -= amount;
synchronized (this) {
balance_ = copy; // update the balance and make it visible to other threads.
}
}
public synchronized getBalance() {
return balance_;
}
}
Please ignore the fact that balance_ should be a double instead of an integer. I know that primitive type reads/assignments are atomic with the exception of doubles and longs so I opted for ints for simplicity
I've tried to write comments inside of the functions to describe my thinking. This class was written to get correct synchronization as well as minimize the amount of code that's under locking. Here's my questions:
Upvotes: 3
Views: 301
Reputation: 3863
Any code that is not inside the synchronized block can be concurrenlty executed by multiple threads, your solution is creating the new balance outside of a synchronized block so it won't work properly. Let's see an example:
int copy = balance_; // 1
copy += amount; //2
synchronized(this) {
balance_ = copy; // 3
}
At the end the BankAccount has 20 but it should be 35
This is the correct way to make it:
public class BankAccount {
private int balance_;
public BankAccount(int initialBalance) {
if (initialBalance < 300) {
throw new IllegalArgumentException("Balance needs to be at least 300");
}
balance_ = initialBalance;
}
public void deposit(int amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Deposit has to be positive");
}
synchronized(this) {
balance_ += amount;
}
}
public void withdraw(int amount) {
synchronized (this) {
if (amount > balance_) {
throw new IllegalArgumentException("Withdrawal has to be <= current balance");
}
balance_ -= amount;
}
}
public synchronized int getBalance() {
return balance_;
}
}
Upvotes: 2
Reputation: 43391
This code is prone to race conditions.
Consider this part:
int copy = balance_;
copy += amount;
// here!
synchronized(this) {
balance_ = copy; // make the new balance visible to other threads
}
What happens if someone calls withdraw
or deposit
during the "here" section? That second method will change _balance
, but that change won't be reflected in your local copy
. When you then write copy
to the shared variable, it will simply overwrite the value.
The way to handle this would be to do the whole operation — the read, modification, and write — under the exclusive lock. Alternatively, you could use an AtomicInteger, which provides an atomic incrementAndGet
method. This can often compile down to hardware primitives called "compare and swap" and is thus very efficient. The disadvantage is that it only provides atomicity for that one operation; if you need some other operation to also be atomic (maybe you also want to increment a depositCounts
field?), then AtomicInteger won't work.
Upvotes: 1