问题
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
service
method is executed, nowlastNumber
is10
,lastFactors
is[1, 2, 5, 10]
. - servlet server receives both request B and C, request B is handled at first, but after exiting the first
synchronized
block (now for request B,factors
is[1, 2, 5, 10]
, which is correct), request C is handled. - for request C, the entire
service
method is executed, it changelastFactors
from[1, 2, 5, 10]
to[1, 2, 4, 5, 10, 20]
, because bothfactors
lastFactors
point to the same object,factors
is 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