2

私は最近、一意の負の数を生成するために使用されるコードをリファクタリングしました。
編集:複数のスレッドがこれらの ID を取得し、キーとして DB に追加します。簡単に識別できるようにするには、数値を負にする必要があります。テスト セッションの終了時に、数値は DB から削除されます。

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

上記のコード構造は、セットと「再試行」ループに投機的に追加されているため、同期されたセットをアトミック変数のいずれかに置き換える、同等のノンブロッキング アルゴリズムがあると思われます。

アトミック変数を使用して書き直そうと何度か試みましたが、すべてマルチスレッド攻撃テストに失敗しました。

エレガントなノンブロッキングの同等物はありますか?

編集:好奇心のために、アトミック整数をガードとして使用する欠陥のある試みを次に示します

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

8 に答える 8

9

ランダム性を気にしない場合は、次のようにカウンターを減らすことができます。

private final AtomicInteger ai=new AtomicInteger(0);

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

編集:

乱数の場合、ソリューションを使用して、たとえば次のように使用できます。synchronizedSet の代わりに ConcurrentHashMap または ConcurrentSkipListSet。異なるスレッドがランダム ジェネレーターの異なるインスタンスを使用し、これらのジェネレーターが相関していないことを確認する必要があります。

于 2009-02-24T00:06:40.833 に答える
6

カウンターの使用を提案する他の回答は優れていますが、予測不可能性(または少なくとも自明でない予測可能性)重要な場合は、元のアルゴリズムで問題ありません。

なんで?

基本的に、繰り返し整数を取得する確率は非常に (非常に) (非常に) 小さく、おおよそ 1 をまだ見たことのない整数の数で割った値です。すでにN数値を生成している場合、アルゴリズムの予想される実行時間は係数 1/2^32 でほぼ線形にNなります。つまり、予想される実行時間を 2 回以上反復するには、10 億を超える数値を生成する必要があります。ループの!実際には、セットに特定の数値が存在するかどうかを確認することは、ループを繰り返す可能性よりもアルゴリズムの実行時間を大幅に伸ばすことができます (まあ、多分を使用していない限りHashSet- その漸近的な実行時間が何であるかを忘れてしまいました) )。

価値があるのは、ループ反復の正確な予想回数は次のとおりです。

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

100 万個の数字を生成した後、これは 1.00047 になります。つまり、たとえば、1,000,001 番目から 1,002,000 番目の数字を生成するには、これらすべての呼び出しでおそらく1 つの繰り返し数字totalを取得することになります。

于 2009-02-24T00:38:07.747 に答える
3

リストされているすべての要件に対するエレガントな解決策は、私が知る限り、-1 から始まる値を減らすことです。ただし、すべての要件をリストしていないと思われます。

于 2009-02-24T00:03:30.747 に答える
2

OPの回答とjpalecekの回答を組み合わせて、次のようにします。

private final AtomicInteger ai=new AtomicInteger(0);

public int nextID() {
    return ai.addAndGet(-1 - random.nextInt(1000));
}
于 2009-02-25T03:49:41.990 に答える
2

高スケール ライブラリには、使用できる NonBlockingHashSet があります。セットのインスタンスを NonBlockingHashSet のインスタンスに置き換えるだけで、設定は完了です。

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

于 2009-02-25T04:26:15.477 に答える
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 を使用するように変換するのは読者に任せます...

編集:実際には、同時発生のケースを最適化するには、次の負の数を取得した後に「シード」に書き戻したいだけかもしれません。

OK、一般的な要求により、アトミック バージョンは次のようになります。

  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;
  }
于 2009-02-24T05:24:24.253 に答える
1

あなたが意味するのは、ノンブロッキングでリエントラントだと思います。

編集:(これははるかに優れているため、オリジナルを置き換えます)

実際には非常にパフォーマンスの高いスレッドベースのオプションが頭に浮かびました (少なくともオリジナルよりもパフォーマンスが高い)。スレッドオブジェクトを「キー」として使用して弱いハッシュマップを作成し、「値」として一連の、たとえば特定の範囲から1000個の数値を作成する機能を持つオブジェクトを作成した場合。

そうすれば、各スレッドに割り当て元の 1000 の数値範囲を割り当てることができます。オブジェクトが数値を使い果たしたら、無効な数値 (0?) を返すようにします。そのオブジェクトに新しい範囲を割り当てる必要があることがわかります。

どこにも同期はありません (編集: おっと、少し間違っていました。以下を参照してください)。弱いハッシュ マップは、破棄されたスレッドを自動的に解放し (特別なメンテナンスは不要)、最も遅い部分はスレッドの単一のハッシュ ルックアップになります。これは実際には非常に高速です。

現在実行中のスレッドを取得するには:

Thread currThread=Thread.getCurrentThread();

また、私が間違っている可能性があり、メソッドを同期させるだけでよい場合、これは機能します。

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

私は先に進んでそれを書きました(時々、このことは私がそれをするまで私の頭の中で立ち往生しています. 未テストですが、箱から出してすぐに動作しない場合でも、近いはずです。一意の負の数を取得するために呼び出す静的メソッドが 1 つあるクラスは 1 つだけです。(ああ、同期が必要でしたが、それは 0.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;
    }
}

1 つの大きな同期ブロックの代わりに、「+= カウント」用と .put() 用の 1 つずつ、異なるオブジェクトで同期された 2 つの非常に小さなブロックに置き換えることができることに気付きました。衝突によってまだ速度が低下している場合は、それが役立つ可能性があります (ただし、衝突によってまだ速度が低下している場合 (本当に???)、単にカウントを上げるだけのほうがよいでしょう)。

于 2009-02-24T00:29:28.720 に答える