Reputation: 26587
Are these methods getNewId()
& fetchIdsInReserve()
thread safe ?
public final class IdManager {
private static final int NO_OF_USERIDS_TO_KEEP_IN_RESERVE = 200;
private static final AtomicInteger regstrdUserIdsCount_Cached = new AtomicInteger(100);
private static int noOfUserIdsInReserveCurrently = 0;
public static int getNewId(){
synchronized(IdManager.class){
if (noOfUserIdsInReserveCurrently <= 20)
fetchIdsInReserve();
noOfUserIdsInReserveCurrently--;
}
return regstrdUserIdsCount_Cached.incrementAndGet();
}
private static synchronized void fetchIdsInReserve(){
int reservedInDBTill = DBCountersReader.readCounterFromDB(....); // read column from DB
if (noOfUserIdsInReserveCurrently + regstrdUserIdsCount_Cached.get() != reservedInDBTill) throw new Exception("Unreserved ids alloted by app before reserving from DB");
if (DBUpdater.incrementCounter(....)) //if write back to DB is successful
noOfUserIdsInReserveCurrently += NO_OF_USERIDS_TO_KEEP_IN_RESERVE;
}
}
Upvotes: 2
Views: 154
Reputation: 13139
You the field noOfUserIdsInReserveCurrently
is not accessed from anywhere else, then yes - access to it is thread-safe.
P.S. if fetchIdsInReserve
is called only from inside the synchronized
block in the getNewId
method, then you don't have to make the method synchronized.
UPDATE: as long as the question was edited, now it is not thread-safe. You have to have the return statement in the first method INSIDE the synchronized block. And it doesn't have to be an AtomicInteger, it can be just a simple int
in this case.
Upvotes: 1
Reputation: 120496
No.
If 21 threads comes in here
synchronized(IdManager.class){
if (noOfUserIdsInReserveCurrently <= 20)
fetchIdsInReserve();
noOfUserIdsInReserveCurrently--;
}
and wait while another 180 threads proceed through the top and through the line below, then by the time the 21st thread reaches the line below, there will be no user ids in reserve when the 21st thread from the first group calls
return regstrdUserIdsCount_Cached.incrementAndGet();
EDIT:
Here's the initial state on class load:
regstrdUserIdsCount_Cached = 100
noOfUserIdsInReserveCurrently = 0
Let's assume that the write back to the DB is always successful. If it isn't, this code is clearly broken, because it still allocates an ID in that case.
The first thread comes through, and calls fetch because there are no ids in reserve.
regstrdUserIdsCount_Cached = 100
noOfUserIdsInReserveCurrently = 0
assuming the DB returns 100 as the initial ID, after the method completes without contention
regstrdUserIdsCount_Cached = 101
noOfUserIdsInReserveCurrently = 199
Now, let's assume 178 more threads go through without contention
regstrdUserIdsCount_Cached = 279
noOfUserIdsInReserveCurrently = 21
if that thread is preempted by another that comes through after it exits the synchronized
block but before it decrements the atomic int
, the preempting thread will trigger a fetch.
Since noOfUserIdsInReserveCurrently
has not been decremented by the thread that was pre-empted,
(noOfUserIdsInReserveCurrently + regstrdUserIdsCount_Cached.get() != reservedInDBTill)
will be false.
Assuming that exception indicates a failure mode, we have a failure during one interleaving that is not thrown during other-interleavings. Therefore, the code is not thread-safe.
The solution is to consistently access regstrdUserIdsCount_Cached
inside the critical section. In that case, it need not be an atomic int, but can simply be a non-final
int.
Upvotes: 1
Reputation: 7706
Here is an example test to show it is thread safe. Replace the AtomicInteger with an int and get rid of the syncs and the test should fail.
public static void main(String... arg) throws Exception {
final int loops = 1042;
for (int l = 0; l != loops; l++) {
reset(); // add this method to your class to reset the counters
final int maxThreads = 113;
final ArrayList<String> checker = new ArrayList<String>();
final CountDownLatch cl1 = new CountDownLatch(maxThreads);
final CountDownLatch cl2 = new CountDownLatch(maxThreads);
for (int x = 0; x != maxThreads; x++) {
Thread thread = new Thread(new Runnable() {
public void run() {
cl1.countDown();
try {
cl1.await(); // stack all threads here
} catch (InterruptedException e) {
e.printStackTrace();
}
int id = getNewId();
synchronized(checker) {
checker.add("" + id);
}
cl2.countDown();
}
});
thread.start();
}
cl2.await();
for (int x = 0; x != maxThreads; x++) {
String key = "" + (101 + x); // 1st ID value
if (!checker.contains(key)) {
System.out.println("Checker 1 FAIL - missing id=" + key);
} else {
checker.remove(key);
if (checker.contains(key)) {
System.out.println("Checker 2 FAIL - extra id=" + key);
}
}
}
for (int x = 0; x != checker.size(); x++) {
String key = "" + (101 + x);
System.out.println("Checker 3 FAIL - extra id=" + key);
}
}
}
Upvotes: 0