並行プログラムテスト
1:34.98 + 55.5 + 9.71 100.19
2:55.5 +9.71 65.21
3:9.716
並行プログラムテストの落とし穴
- ガベージコレクション
- 予測できないガベージコレクション操作
- 解決:
- テストの実行中、GCは1回実行されません
- テストの実行中は、GCが複数回実行され、十分に長い時間(数分)かかります[より良い]
- 動的コンパイル
- JVMは、解釈の実行とコンパイルの実行を組み合わせたものです。クラスが初めて実行されるとき、JVMはバイトコードを解釈してクラスを実行します。ある時点で、メソッドが十分な回数実行されると、動的コンパイラはそれをマシンコードにコンパイルするため、コードは次のようになります。コンパイルの実行(コンパイルの実行が速いため)。さらに、コードは逆コンパイルおよび再コンパイルされる可能性があり、コンパイルプロセスにも時間がかかります。
- 解決:
- コンパイル時間と解釈実行時間を無視できるように、十分な時間実行します。
- テストを開始する前に一定期間コードを実行して、テスト前にコードが完全にコンパイルされていることを確認します
- プログラムの実行時に、コマンドラインオプション-xx:+ PrintCompilationを使用すると、動的コンパイルの実行時にメッセージが出力されます。このメッセージを使用して、テストを実行する前に動的コンパイルが実行されていることを確認できます。
- 無駄なコード除去
- -serverモードでは、最適化によって不要なコードを簡単に削除できるため、-serverモードの方が実行が向上します。テストするコードが不要なコードとして削除された場合、テストは無意味になります。
- 解決策:オブジェクトフィールドのハッシュ値を計算し、
System.nanoTime()
比較しますif (foo.x.hashCode() == System.nanoTime()) System.out.print(" ");
- コードパスの非現実的なサンプリング
- 実行時に、コンパイラは収集した情報に従ってコンパイルされたコードを最適化します。つまり、特定のプログラムのメソッドMをコンパイルするときに生成されるコードは、別のプログラムのメソッドMをコンパイルすることによって生成されるコードとは異なる場合があります。テストギャップを作成します。
- 非現実的な競争
マルチプロデューサー、マルチコンシューマーモデルの正確性を確認する方法
- 2つを維持する
AtomicInteger
:putSum
とtakeSum
- 番号をキューに入れて、に追加します
putSum
- キューから番号を取り出して、に追加します
takeSum
- 試験:
putSum == takeSum
CyclicBarrier
並列スレッド保証
public class PutTakeTest extends TestCase {
protected static final ExecutorService pool = Executors.newCachedThreadPool();
protected CyclicBarrier barrier;
protected final SemaphoreBoundedBuffer<Integer> bb;
protected final int nTrials, nPairs;
protected final AtomicInteger putSum = new AtomicInteger(0);
protected final AtomicInteger takeSum = new AtomicInteger(0);
public static void main(String[] args) throws Exception {
new PutTakeTest(10, 10, 100000).test(); // sample parameters
pool.shutdown();
}
public PutTakeTest(int capacity, int npairs, int ntrials) {
this.bb = new SemaphoreBoundedBuffer<Integer>(capacity);
this.nTrials = ntrials;
this.nPairs = npairs;
// 使用CyclicBarrier保证生产者和消费者线程的并发执行
this.barrier = new CyclicBarrier(npairs * 2 + 1);
}
void test() {
try {
for (int i = 0; i < nPairs; i++) {
pool.execute(new Producer());
pool.execute(new Consumer());
}
barrier.await(); // 阻塞主线程,等待所有线程就绪
barrier.await(); // 等待所有线程执行完成,
// 即生产者和消费者线程中的第二个await执行完
// 相当于循环使用了两次CyclicBarrier
assertEquals(putSum.get(), takeSum.get());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 测试用的随机数生成代码
// 使用方法:通过前一个seed生成后一个seed
// int seed = (this.hashCode() ^ (int) System.nanoTime());
// seed = xorShift(seed);
static int xorShift(int y) {
y ^= (y << 6);
y ^= (y >>> 21);
y ^= (y << 7);
return y;
}
class Producer implements Runnable {
public void run() {
try {
int seed = (this.hashCode() ^ (int) System.nanoTime());
int sum = 0;
barrier.await(); // 通知主线程生产者线程开始执行准备就绪
for (int i = nTrials; i > 0; --i) {
bb.put(seed);
sum += seed;
seed = xorShift(seed);
}
putSum.getAndAdd(sum);
barrier.await(); // 通知主线程生产者线程执行结束
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
class Consumer implements Runnable {
public void run() {
try {
barrier.await(); // 通知主线程消费者线程开始执行准备就绪
int sum = 0;
for (int i = nTrials; i > 0; --i) {
sum += bb.take();
}
takeSum.getAndAdd(sum);
barrier.await(); // 通知主线程消费者线程执行结束
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
ブロッキングキューのパフォーマンス比較
LinkedBlockingQueue
スケーラビリティはより優れていArrayBlockingQueue
ます。- 常識に反して、要素を挿入するときにリンクリスト要素にリンクリストノードオブジェクトを割り当てる必要があるため、理論的にはより時間がかかるはずです。
- 理由:一部の最適化されたリンクリストキューは、キューのヘッドノードの更新操作をテールノードの更新操作から分離できるため、配列ベースのキューと比較して、リンクリストキューのputメソッドとtakeメソッドはより高いアクセスをサポートします。