Search code examples
javaalgorithmatomicnonblocking

Nonblocking algorithm to generate unique negative numbers


I recently refactored a piece of code used to generate unique negative numbers.
edit: Multiple threads obtain these ids and add as keys to a DB; numbers need to be negative to be easily identifiable -at the end of a test session they're removed from the DB.

My Java algorithm looks like this:

private final Set<Integer> seen = Collections.synchronizedSet(new HashSet<Integer>());
public Integer generateUniqueNegativeIds() {
    int result = 0;
    do {
        result = random.nextInt();
        if (result > 0) {
            result *= -1;
        }
    } while (!seen.add(result));
    return result;
}

The code structure above, with its speculative addition to the set and "retry" loop, makes me think there's an equivalent nonblocking algorithm that replaces the synchronized set with any of the atomic variables.

I made a few attempts to re-write using atomic variables but all failed the multithread attack test.

Is there an elegant nonblocking equivalent?

edit: for curiosity's sake here's a flawed attempt using an atomic integer as a guard

private final AtomicInteger atomi = new AtomicInteger(0);
public Integer generateUniqueNegativeIdsWithAtomicAlgo() {
    boolean added = false;
    int result = 0;
    do {
        result = random.nextInt();
        if (result > 0) {
            result *= -1;
        }
        if (atomi.compareAndSet(0, result)) {
            added = cache.add(result);
        }   
    } while (!added);
    return atomi.getAndSet(0);
}

edit: test harness below:

public static void main(String[] args) {
    final int NUMBER_OF_THREADS = 10000;
    final Set<Integer> uniques = Collections.synchronizedSet(new HashSet<Integer>());
    final List<Integer> positives = Collections.synchronizedList(new ArrayList<Integer>());
    final NegativeUniqueIdGenerator nuig = new NegativeUniqueIdGenerator();
    Thread[] workers = new Thread[NUMBER_OF_THREADS];
    long start = System.nanoTime();
    for (int i = 0; i < workers.length; i++) {
        Runnable runnable = new Runnable() {
            public void run() {
                int number = nuig.generateUniqueNegativeIds();
                if (number > 0) {
                    positives.add(number);
                }
                uniques.add(number);
            }
        };
        workers[i] = new Thread(runnable);
        workers[i].start();
    }
    for (int i = 0; i < workers.length; i++) {
        try {
            workers[i].join();
        } catch (InterruptedException ie) {}
    }
    long end = System.nanoTime();
    System.out.println(String.format("duration = %dns", (end - start)));
    System.out.println(String.format("#threads = %d", NUMBER_OF_THREADS));
    System.out.println(String.format("#uniques = %d", uniques.size()));
    System.out.println(String.format("#positives = %d", positives.size()));
    System.out.println(String.format("#duplicates = %d", NUMBER_OF_THREADS - uniques.size()));
    System.out.println(String.format("ratio = %f",
            ((double) NUMBER_OF_THREADS - uniques.size())
                    / NUMBER_OF_THREADS));
    assert uniques.size() == NUMBER_OF_THREADS;
}

Solution

  • From the requirements you've given, I would personally just use a medium-quality random number generator that you know won't produce duplicates within the number of unique numbers that you require. Unless you have an extra requirement you haven't mentioned, it seems overkill to keep the set of all the previously generated numbers.

    For example, using a 32-bit XORShift generator will produce all 2^31 negative 4-byte integers in "random" order before repeating the pattern. If you need more numbers than that, you probably don't want to be putting them in a hash set anyway. So something like this (warning: off-top-of-head untested code...):

    int seed = (int) System.nanoTime();
    final int origSeed = seed;
    
    public int nextUniqueNegativeNumber() {
      int n = seed;
      do {
        n ^= (n << 13);
        n ^= (n >>> 17);
        n ^= (n << 5);
        seed = n;
        if (n == origSeed) {
          throw new InternalError("Run out of numbers!");
        }
      } while (n > 0);
      return n;
    }
    

    I'll leave it up to the reader to convert "seed" to use an AtomicInteger if concurrency is necessary...

    Edit: actually, to optimise the concurrent case you maybe only want to write back to "seed" after getting the next negative number.

    OK, by popular demand, the atomic version would then be something like this:

      AtomicInteger seed = new AtomicInteger((int) System.nanoTime());
    
      public int nextUniqueNegativeNumber() {
        int oldVal, n;
        do {
          do {
            oldVal = seed.get();
            n = oldVal ^ (oldVal << 13); // Added correction
            n ^= (n >>> 17);
            n ^= (n << 5);
          } while (seed.getAndSet(n) != oldVal);
        } while (n > 0);
        return n;
      }