非阻塞算法生成唯一的负数

时间:2009-02-23 23:58:49

标签: java algorithm atomic nonblocking

我最近重构了一段用于生成唯一负数的代码 编辑:多个线程获取这些ID并将其作为密钥添加到数据库中;数字必须是负数才能轻易识别 - 在测试会话结束时,它们将从数据库中删除。

我的Java算法如下所示:

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;
}

上面的代码结构,以及对set和“retry”循环的推测性添加,让我觉得有一个等效的非阻塞算法用任何atomic variables替换同步集。

我曾尝试使用原子变量重写,但所有尝试都未通过多线程攻击测试。

是否有优雅的非阻塞等效?

为了好奇,

编辑这是一个使用原子整数作为警卫的有缺陷的尝试

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);
}

编辑:下面的测试工具:

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;
}

8 个答案:

答案 0 :(得分:9)

如果你不关心随机性,你可以减少一个计数器,如下:

private final AtomicInteger ai=new AtomicInteger(0);

public int nextID() {
  return ai.addAndGet(-1);
}

编辑:

对于随机数,您可以使用您的解决方案并使用例如。 ConcurrentHashMap或ConcurrentSkipListSet而不是synchronizedSet。您必须确保不同的线程使用随机生成器的不同实例,并且这些生成器不相关。

答案 1 :(得分:6)

建议使用计数器的其他答案非常好,但如果非预测性(或至少是非平凡的可预测性) 非常重要,那么原始算法应该没问题。

为什么?

基本上,你得到一个重复整数的概率是非常(非常)(非常)小,大约1除以你还没有看到的整数数。如果您已经生成了N个数字,则算法的预期运行时间在N中近似为线性,系数为1/2 ^ 32,这意味着您必须生成超过十亿个数字只是为了让预期的运行时间超过循环的2次迭代!在实践中,检查集合是否存在某个数字将会延长算法的运行时间,而不是重复循环的可能性(好吧,除非您使用的是HashSet,否则我会忘记它的渐近运行时是什么。)

对于它的价值,确切的预期循环迭代次数是

2^64/(2^32 - N)^2

在您生成了一百万个数字之后,这可以达到1.00047 - 这意味着,例如,生成第1,000,001到1,002,000个数字,您可能会得到一个重复数字,<在所有这些电话中,em>总计。

答案 2 :(得分:3)

据我所知,所有列出的要求的优雅解决方案只是递减从-1开始的值。但是,我怀疑你没有列出所有要求。

答案 3 :(得分:2)

根据您提出的要求,我个人只会使用一个中等质量的随机数生成器,您知道它不会在您需要的唯一数字数量内产生重复数据。除非你没有提到额外的要求,否则保留所有先前生成的数字的集合似乎有点过分了。

例如,在重复模式之前,使用32位XORShift生成器将以“随机”顺序生成所有2 ^ 31个负4字节整数。如果你需要更多的数字,你可能不希望将它们放在哈希集中。所以这样的事情(警告:头顶未经测试的代码......):

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;
}

如果需要并发性,我会让读者将“种子”转换为使用AtomicInteger ...

编辑:实际上,为了优化并发案例,您可能只想在获得下一个否定号码后回写“种子”。

好的,按照大众的需求,原子版本会是这样的:

  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;
  }

答案 4 :(得分:2)

答案 5 :(得分:2)

我会将OP的回答与jpalecek结合起来给出:

private final AtomicInteger ai=new AtomicInteger(0);

public int nextID() {
    return ai.addAndGet(-1 - random.nextInt(1000));
}

答案 6 :(得分:2)

高级lib具有可以使用的NonBlockingHashSet。只需用NonBlockingHashSet实例替换你的set实例,你就可以了。

http://sourceforge.net/projects/high-scale-lib

答案 7 :(得分:1)

我认为你的意思是非阻塞和可重入。

编辑(替换原版,因为这样做要好得多)

实际上非常高效的基于线程的选项只是想到了(至少比原来更高效)。如果您创建了一个带有线程对象的弱哈希映射作为“密钥”,并且为“值”创建了一个能够从特定范围制造一系列(例如1000)数字的对象。

这样你就可以为每个线程分配自己的1000号码来分配。当对象用完数字时,让它返回一个无效的数字(0?),你就会知道你必须为该对象分配一个新的范围。

任何地方都没有同步(编辑:哎呀,有点不对。见下文),弱哈希映射会自动释放被破坏的线程(没有特殊维护),最慢的部分将是单个哈希查找实际上非常快的线程。

获取当前正在运行的线程:

Thread currThread=Thread.getCurrentThread();

另外我可能是错的,您可能只需要使方法同步,然后这将起作用:

int n=-1;
synchronized int getNegativeNumber() {
    return n--;
}

我继续写下它(有时候这些东西会卡在我脑海里直到我这样做,而且只要我这样做,我不妨发布它)。未经测试的所有,但我很确定它应该是关闭,如果不开箱即用。只有一个类使用一个静态方法来调用以获得唯一的负数。 (哦,我确实需要一些同步,但它只会被使用.001%的时间。)

希望有一种方法可以创建一个链接代码块,而不是像这样内联而不会离开网站 - 对不起长度。

package test;

import java.util.WeakHashMap;

public class GenNumber {
    // Static implementation goes first.
    private static int next = -1;
    private static final int range = 1000;

    private static WeakHashMap<Thread, GenNumber> threads = new WeakHashMap<Thread, GenNumber>();

    /**
     * Generate a unique random number quickly without blocking
     * 
     * @return the random number < 0
     */
    public static int getUniqueNumber() {
        Thread current = Thread.currentThread();
        int next = 0;

        // Have to synchronize some, but let's get the very
        // common scenario out of the way first without any
        // synchronization. This will be very fast, and will
        // be the case 99.9% of the time (as long as range=1000)
        GenNumber gn = threads.get(current);
        if (gn != null) {
            next = gn.getNext();
            if (next != 0)
                return next;
        }

        // Either the thread wasn't found, or the range was
        // used up. Do the rest in a synchronized block.
        // The three lines tagged with the comment "*" have
        // the potential to collide if this wasn't synchronized.
        synchronized (threads) {
            if (gn == null) {
                gn = new GenNumber(next -= range); // *
                threads.put(current, gn); // *
                return gn.getNext(); // can't fail this time
            }
            // now we know the range has run out

            gn.setStart(next -= range); // *
            return gn.getNext();
        }
    }

    // Instance implementation (all private, nobody needs to see this)
    private int start;
    private int count;

    private GenNumber(int start) {
        setStart(start);
    }

    private int getNext() {
        if (count < range)
            return start - count;
        return 0;
    }

    private GenNumber setStart(int start) {
        this.start = start;
        return this;
    }
}

让我感到震惊的是,不是一个大的同步块可以被在不同对象上同步的2个非常小的块替换,一个用于“+ = count”,一个用于.put()。如果碰撞仍在减慢你的速度,那可能会有所帮助(虽然如果碰撞仍在减慢你的速度(真的???)你会更好地提高计数。