私は標準のJavaシステムに取り組んでいます。プロデューサーにとって重要なタイミング要件があります(1/100ミリ秒が重要)。
私はプロデューサーがブロッキングキューにデータを配置し、単一のコンシューマーが後でそのデータを取得してファイルにダンプしています。データが利用できない場合、コンシューマはブロックします。
明らかに、ブロッキングキューが適切なインターフェイスですが、必要な場合は実際にどの実装を選択すればよいですかプロデューサーへのコストを最小限に抑える?キューにデータを入れるときは、ロックや割り当てなどのことをできるだけ少なくしたいのですが、消費者がもっと長く待たなければならないか、もっと一生懸命働かなくてもかまいません。
私は単一のコンシューマーと単一のプロデューサーしか持っていないため、より高速になる可能性のある実装はありますか?
まあ、実際にはあまり多くのオプションはありません。 リストされているサブクラス を見てみましょう:
DelayQueue
、 LinkedBlockingDeque
、 PriorityBlockingQueue
、および SynchronousQueue
はすべて、特別な機能が必要な特別な場合のために作られています。このシナリオでは意味がありません。
ArrayBlockingQueue
と LinkedBlockingQueue
のみが残ります。 ArrayList
とLinkedList
のどちらが必要かを判断する方法がわかっている場合は、おそらく自分で答えることができます。
LinkedBlockingQueue
では、「リンクされたノードは挿入ごとに動的に作成される」ことに注意してください。これはあなたをArrayBlockingQueue
に向かわせる傾向があります。
待ち時間の影響を受けやすいシステムはJavaで作成できますが、メモリ割り当て、スレッド間で受け渡しされる情報、およびロックに注意する必要があります。これらのコストがどれだけかかるかを確認するには、いくつかの簡単なテストを作成する必要がありますサーバー(OSおよびメモリアーキテクチャによって異なります)
これを行うと、最大99.99%の時間で動作の安定したタイミングを得ることができます。これは適切なリアルタイムではありませんが、十分に近い場合があります。トレーディングシステムでは、Cコストの代わりにJavaを使用すると、1日あたり100ポンドかかる場合があります。ただし、Javaではなく、C/C++で開発する場合のコストはこれよりもはるかに高くなります。たとえば、柔軟性と保存されるバグの数の点で、.
同じことをしているCプログラムで見られるのと同じ量のジッターをかなり接近させることができます。これを「Cのような」Javaと呼ぶ人もいます。
残念ながら、私が作業しているサーバーのArrayBlockingQueueを介してJavaでスレッド間でオブジェクトを渡すのに約8マイクロ秒かかります。サーバーでこれをテストすることをお勧めします。簡単なテストは、スレッド間のSystem.nanoTime()とその所要時間を確認します。
ArrayBlockingQueueには、追加された各要素にオブジェクトを作成する「機能」があります(そうする正当な理由は多くありません)。これを行わない標準実装を見つけた場合は、お知らせください。
LinkedBlockingQueue
にはO(1)メモリ割り当ての遅延がない限り挿入コストがあります。非常に大きい ArrayBlockingQueue
にはO(1)挿入コスト。ただし、容量がいっぱいになると挿入時にブロックされます。
並行ガベージコレクションを使用しても、リアルタイムシステムをマネージ言語で作成する必要があるかどうかはわかりません。
Javaでこのような時間に制約のあるシステムを実装することは誤りである可能性があるというAndrew Duffyの発言に同意します。いずれにしても、JVMと結婚していて、このキューがフルに実行されている(つまり、コンシューマーが負荷を処理できる)場合、ArrayBlockingQueueに似ていますが、単一のプロデューサー/コンシューマーシナリオに合わせて削減/最適化されたカスタム実装が最適です。特に、ブロックするのではなく、スペースを待つためにプロデューサー側でスピンするという概念。
ガイダンスとしてJava.concurrent sources を参照し、このアルゴリズムを作成しました...
それは私にはかなりよく見えますが、それは完全にテストされておらず、実際には速くないかもしれません(おそらく革命的ではありませんが、私はそれを自分で調整しました:-)。とにかく楽しかった...欠陥を見つけられますか?
疑似コード:
private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition();
private final E[] buffer;
private int head = 0
private int tail = 0;
public void put(E e) {
if (e == null) throw NullPointerException();
while (buffer[tail] != null) {
// spin, wait for space... hurry up, consumer!
// open Q: would a tight/empty loop be superior here?
Thread.sleep(1);
}
buffer[tail] = e;
tail = (tail + 1) % buffer.length;
if (count.getAndIncrement() == 0) {
sync(takeLock) { // this is pseudocode -- should be lock/try/finally/unlock
notEmpty.signal();
}
}
}
public E take() {
sync(takeLock) {
while (count.get() == 0) notEmpty.await();
}
E item = buffer[head];
buffer[head] = null;
count.decrement();
head = (head + 1) % buffer.length;
return item;
}
タイミング要件がそれほど厳しくない場合は、最適なものを決定するために、まったく同じハードウェアで広範なベンチマークを行う必要があるでしょう。
推測するとしたら、配列ベースのコレクションはニースの参照の局所性を持つ傾向があるため、 ArrayBlockingQueue
を使用します。
「Slackを使用したデュアルキュー」を実装し、プロデューサーとコンシューマーのマッチングに優れている LinkedTransferQueue を使用して調べることができます。詳細については、以下を参照してください Java 7 TransferQueue 、 Dual Synchronous Queues(.pdf) およびLinkedTransferQueue source
挿入時にリンクノードを作成する必要がないため、ArrayBlockingQueueはおそらくより高速になります。ただし、ArrayBlockingQueueは固定長であり、キューを(少なくともInteger.MAX_INTまで)任意に大きくしたい場合は、LinkedBlockingQueueを使用する必要があります(Integer.MAX_INT要素の配列を割り当てる場合を除く)。 。