The title sounds like there is a lot of problems ahead. Here's my specific case:
This is a travel tickets sales system. Each route has a limited quantity of tickets, and so purchasing the last ticket for a given route shouldn't be accessible to two people (a standard scenario). However, there is the "return ticket" option.. So, I'm using the unique route ID (database-provided) to do the following:
synchronized(bothRoutesUniqueString.intern()) {
synchronized (routeId.intern()) {
if (returnRouteId != null) {
synchronized (returnRouteId.intern()) {
return doPurchase(selectedRoute, selectedReturnRoute);
}
}
return doPurchase(selectedRoute, selectedReturnRoute);
}
}
The two inner synchronized
blocks are in order to make threads stop there only if a ticket for this particular route is being purchased by two people at the same time, not if tickets for two distinct routes are purchased at the same time. The second synchronization is if course due to the fact, that someone may be attempting to purchase the retuern route as a outbound route at the same time.
The outer-most synchronized
block is to account for the scenario, when two people purchase the same combination of tickets, reversed. For example one orders London-Manchester, and the other orders Manchester-London. If there isn't an outer synchronized block, this situation could lead to a deadlock.
(The doPurchase()
method either returns a Ticket
object, or throws an exception, if there are no more tickets available)
Now, I'm perfectly aware this is a very awkward solution, but, if it works as it is expected to, it gives:
- 10 lines to handle the whole complex scenario (and with proper comments, it won't be that hard to understand)
- no unnecessary locking - everything blocks only if it has to block.
- database agnosticism
I'm also aware that such scenarios are handled by either pessimistic or optimistic database locks, and since I'm using Hibernate, these won't be hard to implement either.
I think horizontal scaling can be achieved with the above code using VM clustering. According to Teracotta documentation, it allows turning a single-node multithreaded app to a multi-node, and:
Terracotta tracks String.intern() calls and guarantees reference equality for these explicitly interned strings. Since all references to an interned String object point to the canonical value, reference equality checks will work as expected even for distributed applications.
So, now onto the questions themselves:
- do you spot any drawbacks to the above code (apart from its awkwardness)?
- is there an applicable class from the
java.util.concurrent
API to help in this scenario? - why would a database locking be preferable to this?
Update:
Since most of the answers are concerned with OutOfMemoryError
, I crated a benchmark for intern()
, and the memory hasn't been eaten up. Perhaps the strings table is being cleared, but this wouldn't matter in my case, since I need the objects to be equal in race conditions, and clearing of the most recent Strings should not happen at the point:
System.out.println(Runtime.getRuntime().freeMemory());
for (int i = 0; i < 10000000; i ++) {
String.valueOf(i).intern();
}
System.out.println(Runtime.getRuntime().freeMemory());
P.S. The environment is JRE 1.6