Why is Java Concurrency in Practice code considered safe?

I am reading the book Java Concurrency in Practice and one example of "safe" code from Chapter 3 alerted me. The book says that using volatile variables in conjunction with immutable objects can provide a weak form of atomicity, and gives an example with the factorization of a number. Given a class-holder that contains a number and its factors, it should serve as a"cache".

@Immutable
class OneValueCache {
     private final BigInteger lastNumber;
     private final BigInteger[] lastFactors;
     public OneValueCache(BigInteger i, BigInteger[] factors) {
         lastNumber = i;
         lastFactors = Arrays.copyOf(factors, factors.length);
     }
     public BigInteger[] getFactors(BigInteger i) {
         if (lastNumber == null || !lastNumber.equals(i))
             return null;
         else
             return Arrays.copyOf(lastFactors, lastFactors.length);
     }
}

Also given is a servlet that receives a number as input and must give the factors of this number, and if the input is the servlet receives the same number twice in a row, then the first time it caches the number itself and its factors, and the second time it simply takes it out of the cache. The code below.

@ThreadSafe
public class VolatileCachedFactorizer implements Servlet {
     private volatile OneValueCache cache = new OneValueCache(null, null);
     public void service(ServletRequest req, ServletResponse resp) {
         BigInteger i = extractFromRequest(req);
         BigInteger[] factors = cache.getFactors(i);
         if (factors == null) {
             factors = factor(i);
             cache = new OneValueCache(i, factors);
         }
         encodeIntoResponse(resp, factors);
     }
}

The thing is, I can't understand why such code is considered safe? Let's say thread A accepts the number 8, writes lastNumber = 8 in the constructor of the OneValueCache class; and hangs (conditionally). At the same time, thread B also gets the number 8, tries to get the factors of the number 8 in the line BigInteger[] factors = cache.getFactors(i) of the VolatileCachedFactorizer class and, executing this method, sees that the if condition (lastNumber == null || !lastNumber.equals(i)) is not respected and therefore returns return Arrays. copyOf(lastFactors, lastFactors.length); But thread A is still hanging and it did not have time to write the factors of the number 8, so lastFactors is still null, hence the call to Arrays.copyOf(lastFactors, lastFactors.length) hello to NPA. Please correct me, where did I go wrong?

Author: m_luk, 2020-07-01