Hugo ʕ•ᴥ•ʔ Bear Blog

1. Synchronization

Java Synchronization

In Java, concepts of synchronization are based on lock of objects.

Basically, you should acquire lock on the object if its states are accessed and modified by different threads.

There are typically two types of synchronization

The need of operation atomicity.

In some cases, there could have a synchronization issue even if you are using only synchronized objects, such as atomic objects which Java provided. Take a look at the following example, where a http server serving an endpoint, that return a factor values of a number. The endpoint also supports a cache which return the latest factors in case the next request ask factors of the similar value with the last request.

@ThreadSafe
public class SynchronizedFactorizer implements Servlet {
    private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
    private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();
    public void service(ServletRequest req, ServletResponse resp) {
      BigInteger i = extractFromRequest(req);
      BigInteger i = extractFromRequest(req);
      if (i.equals(lastNumber.get()))
        encodeIntoResponse(resp, lastFactors.get()); 
      else {
        BigInteger[] factors = factor(i); 
        lastNumber.set(i); 
        lastFactors.set(factors);
        encodeIntoResponse(resp, factors);
      }
    }
}

Supposed we already cache factors for a number, the following sequence of actions of two threads, thread 1 requesting factors for a different value, and thread 2 requesting for the same value, would cause the issue.

  1. The thread 1, ask if equal to its value, turn out not -> proceed to update the cache.
  2. The thread 2, passing equal condition, proceed to return the factors.
  3. Thread 1 update the factors.
  4. Thread 2 return the factors which was updated by thread 1.

The synchronization issue occurs as thread 2 doesn’t return the correct factors. Such an issue often occurs when you need to coordinate states of an object, in this case lastFactors need to be the factors of lastNumbers. To address this, acquire a lock for the composition of check and return operation. Following is an example

@ThreadSafe
public class CachedFactorizer implements Servlet {
    @GuardedBy("this") private BigInteger lastNumber;
    @GuardedBy("this") private BigInteger[] lastFactors;
    @GuardedBy("this") private long hits;
    @GuardedBy("this") private long cacheHits;
    public synchronized long getHits() { return hits; }
    public synchronized double getCacheHitRatio() {
        return (double) cacheHits / (double) hits;
    }
    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = null;
        synchronized (this) {
            ++hits;
            if (i.equals(lastNumber)) {
                ++cacheHits;
                factors = lastFactors.clone();
            }
        }
        if (factors == null) {
            factors = factor(i);
            synchronized (this)  {
                lastNumber = i;
                lastFactors = factors.clone();
            }
        }
        encodeIntoResponse(resp, factors);
    }
}

Depending on the way you acquire lock, the performance of the system might vary. The above code block is actually a good approach, as it separate the calling of long-processing method factors = factor(i); out of the synchronized block, allowing thread can execute the code concurrently.

Class synchronization documentation

As you can synchronize on any locks, this lock, fields’ lock of an instance. The documentation for synchronization approaches are essential, because:

  1. If a user want to extend your class, they would meet an issue if they lock on this, while your class locks on the private fields, leading to states could be accessible by multiple threads.
  2. The same occurs if a user of your method needs to apply client-side synchronization.

#Markdown #Syntax