-
Notifications
You must be signed in to change notification settings - Fork 38.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SimpleIdGenerator is not thread safe, neither it is documented to be #25485
Comments
Can I fix this . |
Why would that be? The value of incrementAndGet is saved in a local variable. |
@rstoyanchev I should have had my coffee before posting this. you're right and I am wrong. Either way there is a race here.
it sets |
The result of |
@wind57 I agree. |
I prepared a unit test to show the issue: class SimpleIdGeneratorTest {
private final AtomicLong mostSigBits = new AtomicLong(0);
private final AtomicLong leastSigBits = new AtomicLong(-1);
Set<UUID> ids = new HashSet<>();
@Test
void generateIdTest() throws InterruptedException {
ids.add(new UUID(0, 1));
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.execute(() -> ids.add(generateId()));
executorService.execute(() -> ids.add(generateId()));
Thread.sleep(200);
assertTrue(ids.contains(new UUID(0, 1)));
assertTrue(ids.contains(new UUID(1, 0)));
// This UUID is missing, instead UUID(mostSigBits = 0, leastSigBits = 1) got created twice
assertFalse(ids.contains(new UUID(1, 1)));
}
public UUID generateId() {
long leastSigBits = this.leastSigBits.incrementAndGet();
if (leastSigBits == 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.mostSigBits.incrementAndGet();
}
return new UUID(this.mostSigBits.get(), leastSigBits);
}
} I guess, to be truly thread safe, we would need to introduce a synchronized block as shown below. Please correct me if I'm wrong. class SimpleIdGeneratorTest {
private final AtomicLong mostSigBits = new AtomicLong(0);
private final AtomicLong leastSigBits = new AtomicLong(-1);
Set<UUID> ids = new HashSet<>();
@Test
void generateIdTest() throws InterruptedException {
ids.add(new UUID(0, 1));
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.execute(() -> ids.add(generateId()));
executorService.execute(() -> ids.add(generateId()));
Thread.sleep(200);
assertTrue(ids.contains(new UUID(0, 1)));
assertTrue(ids.contains(new UUID(1, 0)));
assertTrue(ids.contains(new UUID(1, 1)));
}
public UUID generateId() {
long leastSigBits = this.leastSigBits.incrementAndGet();
synchronized (this) {
if (leastSigBits == 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.mostSigBits.incrementAndGet();
}
return new UUID(this.mostSigBits.get(), leastSigBits);
}
}
} |
@puhlerblet usually a unit test proves very little in case of thread safety, though, I admit, in this case it is fairly trivial to prove the point. Otherwise, you could also look into this tool, that I usually use. It does instruction re-ordering that can prove things, see more here for example. Also both But the problem is a bit more subtle, it seems to me. Keep in mind that for this problem to be observed, we need to go through the entire range of positive and negative values of a long. Let's take the easy case:
So there are
take And if I now look at the documentation of the class itself:
I could say that there is no need for two |
@wind57 since i am quite inexperienced in dealing with concurrency I really appreciate your input. Your assumption, that no one will ever hit so many IDs sounds reasonable. |
@rstoyanchev what do you think about this one? |
@wind57 indeed every time I think it's simply not worth optimizing and we can just update the docs to say that uniqueness is not fully guaranteed and that on occasion, after every Thoughts? /cc @garyrussell @artembilan |
This is by the way one alternative I thought of but again I'm not sure it's necessary: public class SimpleIdGenerator implements IdGenerator {
private final AtomicLong mostSigBits = new AtomicLong(0);
private final AtomicLong leastSigBits = new AtomicLong(0);
private final AtomicLong lastMostSigBits = new AtomicLong(-1);
@Override
public UUID generateId() {
long leastSigBits = this.leastSigBits.incrementAndGet();
if (leastSigBits == 0) {
this.mostSigBits.incrementAndGet();
}
else if (leastSigBits == Long.MAX_VALUE - 10000) {
this.lastMostSigBits.incrementAndGet();
}
else if (leastSigBits < 10000) {
while (true) {
if (!this.mostSigBits.equals(this.lastMostSigBits)) {
break;
}
}
}
return new UUID(this.mostSigBits.get(), leastSigBits);
}
} |
Bear in mind that this generator is pretty much useless in any environment that persists the ID in a DB and/or a multi-instance application. Not only does the problem (potentially) occur after This generator was provided long ago for a very specific use case where the IDs were not persisted and So, I would say documentation should be enough. If performance is really an issue, consider using the |
Thanks @garyrussell those will be good clarifications. For same JVM, not persisted, and reasonably close to unique. If those trade-offs are okay you'll probably get better performance than |
|
That's an option I guess, make it more official that after Long.MAX_VALUE it won't be fully unique. If that is an issue to begin with then it's the wrong generator to use one way or another. @garyrussell you agree with that? |
Yes, I agree; users are always able to define their own implementation if they are not happy with this one. |
I have a small miss-understanding about
SimpleIdGenerator
, that is a fairly trivial class:The presence of
AtomicLong
hints into the fact that this is a thread-safe continuous incremented UUID, but it's not the case:For the sake of the discussion let's suppose that currently
leastSigBits
holds a-1
(it has been incremented quite a lot, yes).ThreadA
doeslong leastSigBits = this.leastSigBits.incrementAndGet();
, so it puts the value into0
(-1 + 1 = 0
); but before it does the checkif (leastSigBits == 0)
,ThreadB
didlong leastSigBits = this.leastSigBits.incrementAndGet();
too, now on a value that is0
, so it put the value in1
.ThreadA
does the check and sees a value of1
, thatif
statement is not entered and a such a duplicateUUID
.This is very far stretched and I have doubts it has ever impacted any users as for this to happen they would need to generate all the
long
range of IDs, which is highly highly improbable. Still, this code is wrong.If this is suppose to provide thread-safe variant :
if this isn't supposed to be thread safe, simply dropping the un-necessary
AtomicLong
(with it'svolatile
overhead) is going to be a big performance gain.Either way, I would be more than glad to fix this, if someone tells me the path I should be taking. Thank you.
The text was updated successfully, but these errors were encountered: