高負荷マルチスレッドアプリケーションでは、パフォーマンスが非常に重要です。パフォーマンスを向上させるために、開発者は並行性の重要性を認識する必要があります。並行性を使用する必要がある場合、2つ以上のスレッドで共有する必要のあるリソースがよくあります。
この場合、競合状態があります。つまり、1つのスレッドがロックを取得でき(ロックは特定のリソースにバインドされています)、ロックを取得したい他のスレッドはブロックされます。この同期メカニズムの実装にはコストがかかりますが、便利な同期モデルを提供するために、JVMとオペレーティングシステムの両方がリソースを消費します。並行実装が多くのリソースを消費する3つの最も重要な要因があります。
- コンテキストスイッチ
- メモリ同期
- ブロッキング同期用に最適化されたコードを作成するには、これら3つの要因とそれらを削減する方法を認識する必要があります。そのようなコードを書くときに注意しなければならないことがたくさんあります。この記事では、これらの要因を減らすためにロックの粒度を減らす手法を紹介します。
基本的な原則から始めましょう:不要なロックを長時間保持しないでください。
ロックを取得する前に必要なことをすべて行い、同期が必要なリソースに対してのみロックを使用し、使用後すぐにロックを解放します。簡単な例を見てみましょう:
public class HelloSync {
private Map dictionary = new HashMap();
public synchronized void borringDeveloper(String key, String value) {
long startTime = (new java.util.Date()).getTime();
value = value + "_"+startTime;
dictionary.put(key, value);
System.out.println("I did this in "+
((new java.util.Date()).getTime() - startTime)+" miliseconds");
}
}
この例では、System.out.println()と呼ばれる2つのDateオブジェクトを作成し、多数の文字列連結操作を実行したため、基本的な原則に違反しましたが、必要な同期操作は「辞書」のみでした。 put(key、value); "。コードを変更して、同期メソッドをこの文のみを含む同期ブロックに変更し、次のより最適化されたコードを取得します。
public class HelloSync {
private Map dictionary = new HashMap();
public void borringDeveloper(String key, String value) {
long startTime = (new java.util.Date()).getTime();
value = value + "_"+startTime;
synchronized (dictionary) {
dictionary.put(key, value);
}
System.out.println("I did this in "+
((new java.util.Date()).getTime() - startTime)+" miliseconds");
}
}
上記のコードはさらに最適化できますが、このアイデアを伝えたいだけです。さらに最適化する方法に興味がある場合は、java.util.concurrent.ConcurrentHashMapを参照してください。
それでは、ロックの粒度をどのように減らすのでしょうか?簡単に言えば、できるだけ少ないロックを要求することです。基本的な考え方は、クラスドメイン全体に対して1つのロックのみを使用するのではなく、同じクラス内の複数の独立した状態変数を保護するために異なるロックを使用することです。多くのアプリケーションで見た次の簡単な例を見てみましょう。
public class Grocery {
private final ArrayList fruits = new ArrayList();
private final ArrayList vegetables = new ArrayList();
public synchronized void addFruit(int index, String fruit) {
fruits.add(index, fruit);
}
public synchronized void removeFruit(int index) {
fruits.remove(index);
}
public synchronized void addVegetable(int index, String vegetable) {
vegetables.add(index, vegetable);
}
public synchronized void removeVegetable(int index) {
vegetables.remove(index);
}
}
食料品店のオーナーは、自分の食料品店で野菜や果物を追加/削除できます。上記の食料品店の実装では、メソッドドメインで同期が行われるため、基本的な食料品ロックを使用して果物と野菜を保護しています。実際、この大規模なロックを使用する代わりに、各リソース(果物と野菜)にロックを使用できます。改善されたコードを見てください:
public class Grocery {
private final ArrayList fruits = new ArrayList();
private final ArrayList vegetables = new ArrayList();
public void addFruit(int index, String fruit) {
synchronized(fruits) fruits.add(index, fruit);
}
public void removeFruit(int index) {
synchronized(fruits) {fruits.remove(index);}
}
public void addVegetable(int index, String vegetable) {
synchronized(vegetables) vegetables.add(index, vegetable);
}
public void removeVegetable(int index) {
synchronized(vegetables) vegetables.remove(index);
}
}
2つのロックを使用した後(ロックを分離)、以前にロック全体を使用した場合よりも、ロックのブロック状況が少なくなります。この手法をロックの競合が中程度のロックに適用すると、最適化の改善がより明確になります。この方法をロックの競合がわずかなロックに適用すると、改善は比較的小さくなりますが、依然として効果的です。しかし、ロックの競合が激しいロックで使用する場合、結果が常に良いとは限らないことを理解する必要があります。
この手法を選択的に使用してください。ロックが激しい競合ロックであると思われる場合は、次の方法を使用して、上記の手法を使用するかどうかを確認してください。
- 製品のスクランブルの量を確認し、スクランブルを3〜5倍にします(フールプルーフにする場合は10倍にします)。
- この競合に基づいて適切なテストを行う
- 2つのプログラムのテスト結果を比較し、最適なプログラムを選択します。
同期のパフォーマンスを向上させるための手法はたくさんありますが、すべての手法について、基本的な原則は1つだけです:不要なロックを長時間保持しないでください。
この基本原理は、先ほど説明したように「できるだけ少ないロックを要求する」と理解できますし、他の説明(実装方法)もあり、後の記事でさらに紹介します。