Sunday, October 31, 2010

Atomicity

What happens when we add one element of state to what was a stateless object? Suppose we want to add a "hit counter" that measures the number of requests processed. The obvious approach is to add a long field to the servlet and increment it on each request.

public class UnsafeCountingFactorizer implements Servlet {

private long count = 0;

public long getCount() {

return count;

}

public void service(ServletRequest req, ServletResponse resp) {

BigInteger i = extractFromRequest(req);

BigInteger[] factors = factor(i);

++count;

encodeIntoResponse(resp, factors);

}

}

Unfortunately, UnsafeCountingFactorizer is not thread-safe, even though  it would work just fine in a single-threaded environment. It is susceptible to lost updates. While the increment operation,  ++count, may look like a single action because of its compact syntax,  it is not atomic. shows what can happen if two threads try to increment a counter simultaneously  without synchronization. If the counter is initially 9, with some unlucky timing  each thread could read the value, see that it is 9, add one to it, and each set  the counter to 10. This is clearly not what is supposed to happen; an increment  got lost along the way, and the hit counter is now permanently off by one. This is known is Race Condition.

UnsafeCountingFactorizer has several race conditions that make its results unreliable. A race condition occurs when the correctness of a computation depends on the relative timing or interleaving of multiple threads by the runtime; in other words, when getting the right answer relies on lucky timing.

Race Condition can be avoided by lazy initialization

public class LazyInitRace {

private ExpensiveObject instance = null;

public ExpensiveObject getInstance() {

if (instance ==null){

instance = new ExpensiveObject();

}

return instance;

}

}

LazyInitRace has race conditions that can undermine its correctness. Say that threads A and B execute getInstance at the same time. A sees that instance is null, and instantiates a new ExpensiveObject. B also checks if instance is null. Whether instance is null at this point depends unpredictably on timing, including the vagaries of scheduling and how long A takes to instantiate the ExpensiveObject and set the instance field. If instance is null when B examines it, the two callers to getInstance may receive two different results, even though getInstance is always supposed to return the same instance.

If the increment operation in UnsafeSequence were atomic, the race conditioncould not occur, and each execution of the increment operation would have the desired effect of incrementing the counter by exactly one. To ensure thread safety, check-then-act operations (like lazy initialization) and read-modify-write operations (like increment) must always be atomic. Atomic state can be created by the syncronization or the free atomic package in new Java 1.5

public class CountingFactorizer implements Servlet { 
    private final AtomicLong count = new AtomicLong(0); 
     public long getCount() {
       return count.get(); 
     } 
 public void service(ServletRequest req, ServletResponse resp) {                                
         BigInteger i = extractFromRequest(req);    
         BigInteger[] factors = factor(i);
         count.incrementAndGet();
         encodeIntoResponse(resp, factors);
     }
 } 


The java.util.concurrent.atomic package contains atomic variable classes for effecting atomic state transitions on numbers and object references. By replacing the long counter with an AtomicLong, we ensure that all actions that access the counter state are atomic. Because the state of the servlet is the state of the counter and the counter is thread-safe, our servlet is once again thread-safe.We were able to add a counter to our factoring servlet and maintain thread safety by using an existing thread-safe class to manage the counter state, AtomicLong. When a single element of state is added to a stateless class, the resulting class will be thread-safe if the state is entirely managed by a thread-safe object. But, as we'll see in the next section, going from one state variable to more than one is not necessarily as simple as going from zero to one.

Where practical, use existing thread-safe objects, like AtomicLong, to manage your class's state. It is simpler to reason about the possible states and state transitions for existing thread-safe objects than it is for arbitrary state variables, and this makes it easier to maintain and verify thread safety.

No comments:

Post a Comment