问题
In the following code (copied from Java Concurrency in Practice Chapter 2, section 2.5, Listing 2.8):
@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(); // questionable line here
}
}
if (factors == null) {
factors = factor(i);
synchronized (this) {
lastNumber = i;
lastFactors = factors.clone(); // and here
}
}
encodeIntoResponse(resp, factors);
}
}
why the factors, lastFactors arrays are cloned? Can't it be simply written as factors = lastFactors; and lastFactors = factors;? Just because the factors is a local variable and it is then passed to encodeIntoResponse, which can modify it?
Hope the question is clear. Thanks.
回答1:
This is called defensive copying. Arrays are objects as any other, so
factors = lastFactors
would assing a reference of lastFactos to factors and vice versa. So anyone can overwrite your state outside your control. As an example:
private void filterAndRemove(BigInteger[] arr);
private void encodeIntoResponse(..., BigInteger[] factors) {
filterAndRemove(factors);
}
With our theoretical assignment filterAndRemove would also affect the original lastFactorials.
回答2:
If you change factors = lastFactors.clone(); to factors = lastFactors;, both factors and lastFactors point to the same object, factors is no longer a local variable, it becomes a shared mutable state.
Imagine there are three requests, request A, B, C. The number sent by Request A and B is 10, but the number sent by request C is 20. Things can go wrong if the below execution order happens and you change factors = lastFactors.clone(); to factors = lastFactors;.
- servlet server receives request A, the entire
servicemethod is executed, nowlastNumberis10,lastFactorsis[1, 2, 5, 10]. - servlet server receives both request B and C, request B is handled at first, but after exiting the first
synchronizedblock (now for request B,factorsis[1, 2, 5, 10], which is correct), request C is handled. - for request C, the entire
servicemethod is executed, it changelastFactorsfrom[1, 2, 5, 10]to[1, 2, 4, 5, 10, 20], because bothfactorslastFactorspoint to the same object,factorsis now[1, 2, 4, 5, 10, 20]too. The response of request B is supposed to be[1, 2, 5, 10], but is[1, 2, 4, 5, 10, 20]right now.
回答3:
Answer guessed from basics : You need to clone if you are planning to modify the Object, and you don't want to modify orginal object, in your case factors = lastFactors.clone(); is done because you donnot want lastFactors to be modified instead you clone it and send it to encodeIntoResponse(resp, factors); which may contain code to modify it.
回答4:
The only reason to clone arrays is to block (concurrent in this case) modification of the array elements. However, that doesn't look possible in this case assuming no other methods modify the array referenced by lastFactors which makes sense given the example. The arrays stored in factors and lastFactors are all created and returned in a complete state by factor, and their references are assigned inside synchronized blocks which will cause them to be safely published.
Unless encodeIntoResponse modifies its factors argument's elements, it looks to me like the calls to clone are unnecessary.
回答5:
I agree that that section of the book could have been better explained by the author of the book.
It is true that, in order to implement thread safety properly, you have to synchronize both read and write operations using the same lock; in the code above, in order to minimize the amount of synchronization, the author decided to perform the encodeIntoResponse(...) without synchronization: since the encodeIntoResponse(...) method reads the content of the array referenced by factors then the author cloned it into a new array.
Note: while it is true that factors is a local variable, in general, it would still require to be cloned because the same array is read by synchronized and not synchronized code, which could happen if we pass the reference (without cloning) to lastFactors and encodeIntoResponse(...).
However, as correctly pointed out by @khachik in the question and by @david-harkness in a response, in this specific case the clone calls are unnecessary because lastFactors is safely published, and it is not modified after its publication.
来源:https://stackoverflow.com/questions/12034370/java-concurrency-in-practice-cached-thread-safe-number-factorizer-listing-2