11

Have a scenario where multiple threads have race condition on comparison code.

private int volatile maxValue;
private AtomicInteger currentValue;

public void constructor() {
   this.current = new AtomicInteger(getNewValue());
}

public getNextValue() {
  while(true) {
     int latestValue = this.currentValue.get();
     int nextValue = latestValue + 1;
     if(latestValue == maxValue) {//Race condition 1 
       latestValue = getNewValue();
     }
    if(currentValue.compareAndSet(latestValue, nextValue) {//Race condition 2
      return latestValue;
    }
  }
}

private int getNewValue() {
    int newValue = getFromDb(); //not idempotent
    maxValue = newValue + 10;
    return newValue;
}

Questions :

The obvious way to fix this would be add synchronized block/method around the if condition. What are other performant way to fix this using concurrent api without using any kind of locks ?

How to get rid of the while loop so we can get the next value with no or less thread contention ?

Constraints :

The next db sequences will be in increasing order not necessarily evenly distributed. So it could be 1, 11, 31 where 21 may be have asked by other node. The requested next value will always be unique. Also need to make sure all the sequences are used and once we reach the max for previous range then only request to db for another starting sequence and so on.

Example :

for db next sequences 1,11,31 with 10 increment, the output next sequence should be 1-10, 11-20, 31-40 for 30 requests.

s7vr
  • 73,656
  • 11
  • 106
  • 127
  • 1
    Use a thread-safe collection like `ConcurrentLinkedQueue` or `LinkedBlockingQueue`? – Robert Harvey Jan 25 '21 at 16:31
  • Still not clear specifically what your race condition is - or what your two threads are doing. Each thread is calling `getNextValue`? One is calling `getNextValue` and the other `getNewValue`? There are loops involved? Some driver code would be nice. Plus, are you _sure_ you have a performance problem w.r.t. synchronization using blocks? Plus, maybe your sync block is in the wrong place: if it is _enclosing_ the test instead of just the database access that could very well be the wrong place. – davidbak Jan 25 '21 at 16:32
  • To add to my previous comment about wrong place: A lot depends on how frequently `getNextValue` is called vs how many times `getMaxValue` is called when the if condition is true vs _how often `getNewValue` actually gets a new changed value from the database_! – davidbak Jan 25 '21 at 16:37
  • @davidbak - Thanks - `getNextValue` can be called as many times you want by as many threads you can - Think of this as a in memory counter - Max value should be called after every 10 calls to get the new starting value. `getNewValue` gets a new value ( starting sequence ) every time it is called. Same threads entering `getNextValue` can only call `getNewValue` - so no other threads. – s7vr Jan 25 '21 at 16:40
  • @RobertHarvey - Thanks - This would be a in-memory counter so not sure if the use of collection is warranted here. – s7vr Jan 25 '21 at 16:48
  • Hmm. Since the increment is not shown the idea that you're calling the db only once in 10 times isn't clear. It really doesn't look like it is doing what you say it is doing. Is the code accurate or did the counter get conflated with the fetched database value when you simplified the code for this quesiton? – davidbak Jan 25 '21 at 16:52
  • Would `AtomicInteger` for maxValue work here? local values aren't susceptible to threads and `AtomicInteger` was made for use in threads. – WJS Jan 25 '21 at 16:53
  • 1
    How is `latestValue` calculated? It looks like this is already the first race condition, before you even get to `maxValue` – Holger Jan 25 '21 at 17:58
  • Thanks @Holger - Updated code. There should not be any race condition - also iterating in while loop so every thread gets unique value. Perhaps if you know how to also make this performant so there is less or no thread contention ? – s7vr Jan 25 '21 at 18:22
  • There are only two variables which could be shared between threads: `maxValue`, which is volatile, and `currentValue`, which is an object which represents atomic. Neither volatile accesses nor accesses to atomic variable could cause *race condition* **by definition**. Absence of race condition doesn't mean that the code is correct: the logic could be still broken in concurrent environment. But detecting broken logic requires to know much more than just a code of two methods without any comments and use cases. – Tsyvarev Jan 26 '21 at 00:23
  • @Tsyvarev you are oversimplifying this, if you look at what the OP wants to achieve and think about it, the race is obvious. At least for me. – Eugene Jan 26 '21 at 02:43
  • I am voting to re-open, but just notice that adding `synchronized` is not going to help you. the thread that waits to enter the synchronized block will override the value (it will still call `getNewValue`) that the previous one wrote. I hope I make sense here – Eugene Jan 26 '21 at 02:55
  • 1
    @Eugene there can be an arbitrary number of threads calling `getNewValue()` without knowing whether the subsequent `compareAndSet` will succeed, is that what you mean? I also voted to re-open, but mind that you can answer the question, as you can edit and undelete your already existing answer. – Holger Jan 26 '21 at 07:18
  • @Eugene: Well, there is actually no *data races*, but race condition could occur... up to the author of the code. But I still sure that the question **lacks for details**. "Synchronize" seems to be the only concurrent approach which doesn't require knowledge of data meaning and constraints. All other, lock-free, approaches highly depends on desired constraints. E.g. should return values of `getNextValue` be **all** unique? If so, whether `getFromDb` is guaranteed to return values in increase order? – Tsyvarev Jan 26 '21 at 09:00
  • @Tsyvarev I see your point now, thx for the follow-up. And I think the answer is "yes", to both of your last questions. I somehow infered that from the question and comments. – Eugene Jan 26 '21 at 12:23
  • @Holger I think so, yes. My point is that this is not only about exclusive access to `getNewValue`. If a certain thread calls that method, all others have to use the value that that thread wrote, at least for the next 10 values. So that means they cant call `getNewValue` themselves. It's difficult for me to find the proper words, I hope I make sense. I've spent a few hours yesterday thinking about this and I see no option then to make `getNextValue` synchronized. – Eugene Jan 26 '21 at 12:32
  • @Eugene - Thank you for your comments. Your comments are accurate and precisely the problem I'm trying to solve. – s7vr Jan 26 '21 at 15:12
  • @Tsyvarev - Thank you - Yes to both of your last questions. The next db sequences will be in increasing order not necessarily evenly apart. So it could be 1, 11, 31 where 21 may be have asked by other node. I dont think it is important for this part but for just for sake of completeness. Please let me know any other details I can add. – s7vr Jan 26 '21 at 15:17
  • 1
    @s7vr: Could you add these details to your question post? Needing to scan comments for this information.. is not the way how Stack Overflow works. – Tsyvarev Jan 26 '21 at 16:19
  • 1
    Just a braindump here ... may it work using a AtomicLong, where the 32 most sig bits represent the maxValue and the 32 least sig bits represent the currentValue ...? – Felix Jan 28 '21 at 20:03
  • @codeflush.dev I don't think that this matters, you would still need to do _two_ writes to it, in different code paths... unless I miss something from your suggestion. And if I did, you might want to present an answer? – Eugene Jan 29 '21 at 03:14
  • @s7vr in my opinion, you can create a service for fetching from db to a queue. Everytime a thread is trying to retrieve a item from the queue, it will notify the service. and when the queue is empty enough, the service will try to refill the queue. Therefor, the only time that thread have to wait to get next number is when the queue is being refill – Silver Feb 03 '21 at 08:18
  • Sorry all - I can't award the bounty to anyone. As I was looking for lock free sequence implementation and it would appear it is not possible to have one. There are multiple answers noting there is some form synchronization needed. – s7vr Feb 04 '21 at 15:14

10 Answers10

7

First of all: I would recommend thinking one more time about using synchronized, because:

  1. look at how simple such code is:
     private int maxValue;
     private int currentValue;
    
     public constructor() {
       requestNextValue();
     }
    
     public synchronized int getNextValue() {
       currentValue += 1;
       if (currentValue == maxValue) {
         requestNextValue();
       }
       return currentValue;
     }
    
     private void requestNextValue() {
       currentValue = getFromDb(); //not idempotent
       maxValue = currentValue + 10;
     }
    
  2. locks in java actually are pretty intelligent and have pretty good performance.
  3. you talk to DB in your code — the performance cost of that alone can be orders of magnitude higher than the performance cost of locks.

But in general, your race conditions happen because you update maxValue and currentValue independently.
You can combine these 2 values into a single immutable object and then work with the object atomically:

private final AtomicReference<State> stateHolder = new AtomicReference<>(newStateFromDb());

public int getNextValue() {
  while (true) {
    State oldState = stateHolder.get();
    State newState = (oldState.currentValue == oldState.maxValue)
        ? newStateFromDb()
        : new State(oldState.currentValue + 1, oldState.maxValue);
    if (stateHolder.compareAndSet(oldState, newState)) {
      return newState.currentValue;
    }
  }
}

private static State newStateFromDb() {
  int newValue = getFromDb(); // not idempotent
  return new State(newValue, newValue + 10);
}


private static class State {

  final int currentValue;
  final int maxValue;

  State(int currentValue, int maxValue) {
    this.currentValue = currentValue;
    this.maxValue = maxValue;
  }
}

After fixing that you will probably have to solve the following problems next:

  • how to prevent multiple parallel getFromDb(); (especially after taking into account that the method is idempotent)
  • when one thread performs getFromDb();, how to prevent other threads from busy spinning inside while(true) loop and consuming all available cpu time
  • more similar problems

Solving each of these problems will probably make your code more and more complicated.

So, IMHO it is almost never worth it — locks work fine and keep the code simple.

Community
  • 1
  • 1
user15102975
  • 204
  • 1
  • 1
  • the obvious and most painful problem would be that multiple threads can call `newStateFromDb` and thus alter the entire algorithm useless. Initially I thought about the same thing, but no, this is no good. And if you are suggesting a lock, a `ReentrantLock` is most probably going to perform better then `synchronized`:it inflates differently during GC and in latest JVM `BiasedLocking` in turned off by default, which means you might be in trouble a bit without realizing it. – Eugene Jan 29 '21 at 03:03
  • 3
    I don't get why anyone would upvote this. there are multiple threads that will call `newStateFromDb` without actually succeeding on this : `(stateHolder.compareAndSet(oldState, newState))`; which means this algorithm is broken. If any of the upvoters sees that I am wrong, please tag me in a comment below. – Eugene Jan 31 '21 at 02:41
  • Should be done in a `do ... while` loop, you are right. – Johannes Kuhn Feb 01 '21 at 19:14
  • 3
    @Eugene You are correct. The question has to make it absolutely clear that implementations should not waste any sequence(and so the range) fetched from db. `Also need to make sure all the sequences are used and once we reach the max for previous range then only request to db for another starting sequence and so on` Though this from question is nearly clear, an example will be really helpful. So, assuming there are 2 application servers, then it should be guaranteed that all the values from `1,2, ...infinite` will be returned as `id`(they can be intermixed, but can't be mixed) – Thiyanesh Feb 01 '21 at 23:33
2

You cannot completely avoid locking with the given constraints: since (1) every value returned by getFromDb() must be used and (2) calling getFromDb() is only allowed once maxValue has been reached, you need to ensure mutual exclusion for calls to getFromDb().

Without either of the constraints (1) or (2) you could resort to optimistic locking though:

  • Without (1) you could allow multiple threads calling getFromDb() concurrently and choose one of the results dropping all others.

  • Without (2) you could allow multiple threads calling getFromDb() concurrently and choose one of the results. The other results would be "saved for later".

michid
  • 10,536
  • 3
  • 32
  • 59
1

The obvious way to fix this would be add synchronized block around the if condition

That is not going to work. Let me try and explain.

When you hit the condition: if(latestValue == maxValue) { ... }, you want to update both maxValue and currentValue atomically. Something like this:

latestValue = getNewValue();
currentValue.set(latestValue);

getNewValue will get your next starting value from the DB and update maxValue, but at the same time, you want to set currentValue to that new starting one now. Suppose the case:

  • you first read 1 from the DB. As such maxValue = 11, currentValue = 1.

  • when you reach the condition if(latestValue == maxValue), you want to go to the DB to get the new starting position (let's say 21), but at the same time you want every thread to now start from 21. So you must also set currentValue.

Now the problem is that if you write to currentValue under a synchronized block, for example:

if(latestValue == maxValue) {
   synchronized (lock) {
       latestValue = getNewValue();
       currentValue.set(latestValue);
   }
}

you also need to read under the same lock, otherwise you have race. Initially I thought I can be a bit smarter and do something like:

if(latestValue == maxValue) {
    synchronized (lock) {
       if(latestValue == maxValue) {
           latestValue = getNewValue();
           currentValue.set(latestValue);
       } else {
          continue;
       }
    }
}

So that all threads that wait on a lock do not override the previously written value to maxValue when the lock is released. But that still is a race and will cause problems elsewhere, in a different case, rather trivially. For example:

  • ThreadA does latestValue = getNewValue();, thus maxValue == 21. Before it does currentValue.set(latestValue);

  • ThreadB reads int latestValue = this.currentValue.get();, sees 11 and of course this will be false : if(latestValue == maxValue) {, so it can write 12 (nextValue) to currentValue. Which breaks the entire algorithm.

I do not see any other way then to make getNextValue synchronized or somehow else protected by a mutex/spin-lock.

Eugene
  • 117,005
  • 15
  • 201
  • 306
  • You are invoking `getNewValue()` while holding the lock, even relying on it, so why acquiring the lock again in the method? – Holger Jan 25 '21 at 17:56
  • @Holger ah! missed the fact that `getNewValue` is private, good point. – Eugene Jan 25 '21 at 18:06
  • @Eugene Thank you so much. I really appreciate your time and you've summed up both the race conditions perfectly in your answer. I initially missed the second race condition but later I updated the question. I'm truly looking for a lock free sequence counter implementation. I'm still hopeful there is one. I'm going to add bounty to the question once I can. If I may just had one question - Do we need an extra if check outside of the lock when the maxValue is volatile type ? – s7vr Jan 27 '21 at 15:27
  • @s7vr it is hard for me to understand what you mean here, can you explain more broadly? You can always do a spin lock on the entire `getNextValue`, but that will most probably perform worse than a lock. – Eugene Jan 27 '21 at 21:31
  • Sorry - If you are referring to my question in the previous comment - I meant to ask why do we need multiple if checks in your last code snippet - `getNewValue` updates the max value so the outside if check is redundant - so any thread entering the synchronized block would see the updated max value and check will only succeed for single thread. – s7vr Jan 27 '21 at 23:44
  • 1
    @s7vr right, that acts as an optimization, just like "double check locking" for singletons. Since I write to a volatile field, threads might not need to enter the synchronized block at all, they might see the value that the previous thread wrote. So no need to lock at all. – Eugene Jan 28 '21 at 00:30
1

I don't really see a way around synchonizing the DB call - unless calling the DB multiple times is not an issue (i.e. retrieving several "new values").

To remove the need to synchronize the getNextValue method, you could use a BlockingQueue which will remove the need to atomically update 2 variables. And if you really don't want to use the synchronize keyword, you can use a flag to only let one thread call the DB.

It could look like this (looks ok, but not tested):

private final BlockingQueue<Integer> nextValues = new ArrayBlockingQueue<>(10);
private final AtomicBoolean updating = new AtomicBoolean();

public int getNextValue() {
  while (true) {
    Integer nextValue = nextValues.poll();
    if (nextValue != null) return nextValue;
    else getNewValues();
  }
}

private void getNewValues() {
  if (updating.compareAndSet(false, true)) {
    //we hold the "lock" to run the update
    if (!nextValues.isEmpty()) {
      updating.set(false);
      throw new IllegalStateException("nextValues should be empty here");
    }
    try {
      int newValue = getFromDb(); //not idempotent
      for (int i = 0; i < 10; i++) {
        nextValues.add(newValue + i);
      }
    } finally {
      updating.set(false);
    }
  }
}

But as mentioned in other comments, there is a high chance that the most costly operation here is the DB call, which remains synchronized, so you may as well synchronize everything and keep it simple, with very little difference performance wise.

assylias
  • 321,522
  • 82
  • 660
  • 783
  • 2
    smart. let's replace the `synchronized` block with a busy spin :) afaik, busy spins are only good when there is little contention. – Eugene Jan 30 '21 at 04:00
1

As getFromDb hits the database you really want some locking - the other threads should block not also go for the database or spin. Really, if you are doing that every 10 iterations, you can probably synchronize the lot. However, that is no fun.

Any reasonable, non-microcontroller platform should support AtomicLong as lock-free. So we can conveniently pack the two ints into one atomic.

private final AtomicLong combinedValue;

public getNextValue() {
    for (;;) {
        long combined = combinedValue.get();
        int latestValue = (int)combined;
        int maxValue = (int)(combined>>32);

        int nextValue = latestValue + 1;

        long nextCombined = (newValue&0xffffffff) | (maxValue<<32)

        if (latestValue == maxValue) { 
            nextValue();
        } else if (currentValue.compareAndSet(combined, nextCombined)) {
            return latestValue;
        }
    }
}

private synchronized void nextValue() {
    // Yup, we need to double check with this locking.
    long combined = combinedValue.get();
    int latestValue = (int)combined;
    int maxValue = (int)(combined>>32);

    if (latestValue == maxValue) {
        int newValue = getFromDb(); //not idempotent
        int maxValue = newValue + 10;

        long nextCombined = (newValue&0xffffffff) | (maxValue<<32)

        combinedValue.set(nextCombined);
    }
}

An alternative with memory allocation would be to lump both values into one object and use AtomicReference. However, we can observe that the value changes more frequently than the maximum, so we can use a slow changing object and a fast offset.

private static record Segment(
    int maxValue, AtomicInteger currentValue
) {
}
private volatile Segment segment;

public getNextValue() {
    for (;;) {
        Segment segment = this.segment;
        int latestValue = segment.currentValue().get();
        int nextValue = latestValue + 1;

        if (latestValue == segment.maxValue()) { 
            nextValue();
        } else if (segment.currentValue().compareAndSet(
            latestValue, nextValue
        )) {
            return latestValue;
        }
    }
}

private synchronized void nextValue() {
    // Yup, we need to double check with this locking.
    Segment segment = this.segment;
    int latestValue = segment.currentValue().get();

    if (latestValue == segment.maxValue()) {
        int newValue = getFromDb(); //not idempotent
        int maxValue = newValue + 10;
        segment = new Segment(maxValue, new AtomicInteger(newValue));
    }
}

(Standard disclaimer: Code not so much as compiled, tested or thought about much. records require a quite new at time of writing JDK. Constructors elided.)

Tom Hawtin - tackline
  • 145,806
  • 30
  • 211
  • 305
  • `volatile Segment segment` and `int maxValue`: Does volatile on reference variable guarantee visibility of members within the referenced object? How does the reorder safe property ensure freshness across cores? – Thiyanesh Feb 02 '21 at 20:12
  • 1
    @Horse There is a *happens-before* from a write to the `segment` field and a read from it, so the `AtomicInteger` object is always seen correctly. The reference and `maxValue` don't change so they are okay with just `segment` being `volatile`, but are `final` anyway. – Tom Hawtin - tackline Feb 02 '21 at 20:49
0

What an interesting question. As others have said you get round with your problem by using synchronized keyword.

public synchronized int getNextValue() { ... }

But because you didn't want to use that keyword and at the same time want to avoid race condition, this probably helps. No guarantee though. And please don't ask for explanations, I'll throw you with OutOfBrainException.

private volatile int maxValue;
private volatile boolean locked = false; //For clarity.
private AtomicInteger currentValue;
    
public int getNextValue() {
    int latestValue = this.currentValue.get();
    int nextValue = latestValue + 1;
        
    if(!locked && latestValue == maxValue) {
        locked = true; //Only one thread per time.
        latestValue = getNewValue();
        currentValue.set(latestValue);
        locked = false;
    }
    while(locked) { latestValue = 0; } //If a thread running in the previous if statement, we need this to buy some time.
    //We also need to reset "latestValue" so that when this thread runs the next loop, 
    //it will guarantee to call AtomicInteger.get() for the updated value.
    while(!currentValue.compareAndSet(latestValue, nextValue)) {
        latestValue = this.currentValue.get();
        nextValue = latestValue + 1;
    }
    return nextValue;
}

Or you can use Atomic to fight Atomic.

private AtomicBoolean locked = new AtomicBoolean(false);
        
public int getNextValue() {
...
if(locked.compareAndSet(false, true)) { //Only one thread per time.
    if(latestValue == maxValue) {
        latestValue = getNewValue();
        currentValue.set(latestValue);
    }
    locked.set(false);
}
...
Darkman
  • 2,941
  • 2
  • 9
  • 14
  • 1
    this is not correct. besides the fact that `volatile` is not about atomicity (which you imply), there is an obvious path that renders this incorrect. Two threads `ThreadA` and `ThreadB` see that `locked == false` and `latestValue == maxValue`. They each enter the if statement and obviously everything fails now, they both do the sequence of `latestValue = getNewValue(); currentValue.set(latestValue);`, thus overriding each others value. So, this is broken at this point in time. – Eugene Jan 29 '21 at 03:12
  • Yes, you're right. If multiple threads are checking ``if(!locked`` or even one thread is checking ``latestValue == maxValue`` and another thread is checking ``if(!locked``, this method will fail miserably. – Darkman Jan 29 '21 at 03:45
0

I can't think of a way to remove all locking since the underlying problem is accessing a mutable value from several threads. However there several improvements that can be done to the code you provided, basically taking advantage of the fact that when data is read by multiple threads, there is no need to lock the reads unless a write has to be done, so using Read/Write locks will reduce the contention. Only 1/10 times there will be a "full" write lock

So the code could be rewritten like this (leaving bugs aside):

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Counter {

    private final ReentrantReadWriteLock reentrantLock = new ReentrantReadWriteLock(true);
    private final ReentrantReadWriteLock.ReadLock readLock = reentrantLock.readLock();
    private final ReentrantReadWriteLock.WriteLock writeLock = reentrantLock.writeLock();
    private AtomicInteger currentValue;
    private AtomicInteger maxValue;

    public Counter() {
        int initialValue = getFromDb();
        this.currentValue = new AtomicInteger(initialValue);
        this.maxValue = new AtomicInteger(initialValue + 10);
    }

    public int getNextValue() {
        readLock.lock();
        while (true){
            int nextValue = currentValue.getAndIncrement();
            if(nextValue<maxValue.get()){
                readLock.unlock();
                return nextValue;
            }
            else {
                readLock.unlock();
                writeLock.lock();
                reload();
                readLock.lock();
                writeLock.unlock();
            }
        }
    }

    private void reload(){
        int newValue = getFromDb();
        if(newValue>maxValue.get()) {
            this.currentValue.set(newValue);
            this.maxValue.set(newValue + 10);
        }
    }

    private int getFromDb(){
        // your implementation
    }

}
Tomas Fornara
  • 1,280
  • 8
  • 12
0

What is the business use case you are trying to solve? Can the next scenario work for you:

  1. Create SQL sequence (based your database) with counter requirements in the database;
  2. Fetch counters from the database as a batch like 50-100 ids
  3. Once 50-100 are used on the app level, fetch 100 values more from db ...

?

lazylead
  • 1,453
  • 1
  • 14
  • 26
0

Slightly modified version of Tom Hawtin - tackline's answer and also the suggestion by codeflush.dev in the comments of the question

Code

I have added a working version of code and simulated a basic multithreaded environment.

Disclaimer: Use with your own discretion

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

class Seed {
    private static final int MSB = 32;
    private final int start;
    private final int end;
    private final long window;

    public Seed(int start, int end) {
        this.start = start;
        this.end = end;
        this.window = (((long) end) << MSB) | start;
    }

    public Seed(long window) {
        this.start = (int) window;
        this.end = (int) (window >> MSB);
        this.window = window;
    }

    public int getStart() {
        return start;
    }

    public int getEnd() {
        return end;
    }

    public long getWindow() {
        return window;
    }

    // this will not update the state, will only return the computed value
    public long computeNextInWindow() {
        return window + 1;
    }
}

// a mock external seed service to abstract the seed generation and window logic
class SeedService {

    private static final int SEED_INIT = 1;
    private static final AtomicInteger SEED = new AtomicInteger(SEED_INIT);
    private static final int SEQ_LENGTH = 10;

    private static final int JITTER_FACTOR = 5;
    private final boolean canAddRandomJitterToSeed;
    private final Random random;

    public SeedService(boolean canJitterSeed) {
        this.canAddRandomJitterToSeed = canJitterSeed;
        this.random = new Random();
    }

    public int getSeqLengthForTest() {
        return SEQ_LENGTH;
    }

    public Seed getDefaultWindow() {
        return new Seed(1, 1);
    }

    public Seed getNextWindow() {
        int offset = SEQ_LENGTH;

        // trying to simulate multiple machines with interleaved start seed
        if (canAddRandomJitterToSeed) {
            offset += random.nextInt(JITTER_FACTOR) * SEQ_LENGTH;
        }

        final int start = SEED.getAndAdd(offset);
        return new Seed(start, start + SEQ_LENGTH);
    }

    // helper to validate generated ids
    public boolean validate(List<Integer> ids) {
        Collections.sort(ids);
        // unique check
        if (ids.size() != new HashSet<>(ids).size()) {
            return false;
        }

        for (int startIndex = 0; startIndex < ids.size(); startIndex += SEQ_LENGTH) {
            if (!checkSequence(ids, startIndex)) {
                return false;
            }
        }
        return true;
    }

    // checks a sequence
    // relies on 'main' methods usage of SEQ_LENGTH
    protected boolean checkSequence(List<Integer> ids, int startIndex) {
        final int startRange = ids.get(startIndex);
        return IntStream.range(startRange, startRange + SEQ_LENGTH).boxed()
            .collect(Collectors.toList())
            .containsAll(ids.subList(startIndex, startIndex + SEQ_LENGTH));
    }

    public void shutdown() {
        SEED.set(SEED_INIT);
        System.out.println("See you soon!!!");
    }
}

class SequenceGenerator {

    private final SeedService seedService;
    private final AtomicLong currentWindow;

    public SequenceGenerator(SeedService seedService) {
        this.seedService = seedService;

        // initialize currentWindow using seedService
        // best to initialize to an old window so that every instance of SequenceGenerator
        // will lazy load from seedService during the first getNext() call
        currentWindow = new AtomicLong(seedService.getDefaultWindow().getWindow());
    }

    public synchronized boolean requestSeed() {
        Seed seed = new Seed(currentWindow.get());
        if (seed.getStart() == seed.getEnd()) {
            final Seed nextSeed = seedService.getNextWindow();
            currentWindow.set(nextSeed.getWindow());
            return true;
        }
        return false;
    }

    public int getNext() {
        while (true) {
            // get current window
            Seed seed = new Seed(currentWindow.get());

            // exhausted and need to seed again
            if (seed.getStart() == seed.getEnd()) {
                // this will loop at least one more time to return value
                requestSeed();
            } else if (currentWindow.compareAndSet(seed.getWindow(), seed.computeNextInWindow())) {
                // successfully incremented value for next call. so return current value

                return seed.getStart();
            }
        }
    }
}

public class SequenceGeneratorTest {

    public static void test(boolean canJitterSeed) throws Exception {
        // just some random multithreaded invocation

        final int EXECUTOR_THREAD_COUNT = 10;
        final Random random = new Random();
        final int INSTANCES = 500;
        final SeedService seedService = new SeedService(canJitterSeed);
        final int randomRps = 500;
        final int seqLength = seedService.getSeqLengthForTest();

        ExecutorService executorService = Executors.newFixedThreadPool(EXECUTOR_THREAD_COUNT);
        Callable<List<Integer>> callable = () -> {
            final SequenceGenerator generator = new SequenceGenerator(seedService);
            int rps = (1 + random.nextInt(randomRps)) * seqLength;
            return IntStream.range(0, rps).parallel().mapToObj(i -> generator.getNext())
                .collect(Collectors.toList());
        };

        List<Future<List<Integer>>> futures = IntStream.range(0, INSTANCES).parallel()
            .mapToObj(i -> executorService.submit(callable))
            .collect(Collectors.toList());

        List<Integer> ids = new ArrayList<>();
        for (Future<List<Integer>> f : futures) {
            ids.addAll(f.get());
        }

        executorService.shutdown();

        // validate generated ids for correctness
        if (!seedService.validate(ids)) {
            throw new IllegalStateException();
        }

        seedService.shutdown();

        // summary
        System.out.println("count: " + ids.size() + ", unique count: " + new HashSet<>(ids).size());
        Collections.sort(ids);
        System.out.println("min id: " + ids.get(0) + ", max id: " + ids.get(ids.size() - 1));
    }

    public static void main(String[] args) throws Exception {
        test(true);
        System.out.println("Note: ids can be interleaved. if continuous sequence is needed, initialize SeedService with canJitterSeed=false");
        final String ruler = Collections.nCopies( 50, "-" ).stream().collect( Collectors.joining());
        System.out.println(ruler);

        test(false);
        System.out.println("Thank you!!!");
        System.out.println(ruler);
    }
}

Thiyanesh
  • 2,360
  • 1
  • 4
  • 11
0

Slightly modified version of user15102975's answer with no while-loop and getFromDb() mock impl.

/**
 * Lock free sequence counter implementation
 */
public class LockFreeSequenceCounter {

    private static final int BATCH_SIZE = 10;
    private final AtomicReference<Sequence> currentSequence;
    private final ConcurrentLinkedQueue<Integer> databaseSequenceQueue;

    public LockFreeSequenceCounter() {
        this.currentSequence = new AtomicReference<>(new Sequence(0,0));
        this.databaseSequenceQueue = new ConcurrentLinkedQueue<>();
    }

    /**
     * Get next unique id (threadsafe)
     */
    public int getNextValue() {
        return currentSequence.updateAndGet((old) -> old.next(this)).currentValue;
    }

    /**
     * Immutable class to handle current and max value
     */
    private static final class Sequence {
        private final int currentValue;
        private final int maxValue;

        public Sequence(int currentValue, int maxValue) {
            this.currentValue = currentValue;
            this.maxValue = maxValue;
        }

        public Sequence next(LockFreeSequenceCounter counter){
            return isMaxReached() ? fetchDB(counter) : inc();
        }

        private boolean isMaxReached(){
            return currentValue == maxValue;
        }

        private Sequence inc(){
            return new Sequence(this.currentValue + 1, this.maxValue);
        }

        private Sequence fetchDB(LockFreeSequenceCounter counter){
            counter.databaseSequenceQueue.add(counter.getFromDb());
            int newValue = counter.databaseSequenceQueue.poll();
            int maxValue = newValue + BATCH_SIZE -1;
            return new Sequence(newValue, maxValue);
        }
    }

    /**
     * Get unique id from db (mocked)
     * return on call #1: 1
     * return on call #2: 11
     * return on call #3: 31
     * Note: this function is not idempotent
     */
    private int getFromDb() {
        if (dbSequencer.get() == 21){
            return dbSequencer.addAndGet(BATCH_SIZE);
        } else{
            return dbSequencer.getAndAdd(BATCH_SIZE);
        }
    }
    private final AtomicInteger dbSequencer = new AtomicInteger(1);
}
Oliver
  • 332
  • 1
  • 11