ハイパラレルロックの最適化
9127 ワード
一、ロックの保持時間を減らす
ロックを使用して同時制御を行うアプリケーションでは、ロック競合中に、単一のスレッドがロックの保持時間とシステムパフォーマンスに直接関係します.1つのスレッドがロックを保持する時間が長い場合、ロックを待機するスレッドの数が増加し、システムのパフォーマンスに影響を及ぼすことは必然的です.
1つのより最適化された解決策は、必要に応じて同期を行うだけで、スレッドがロックを保持する時間を著しく減少させ、ロック衝突の可能性を低減し、システムの同時能力を向上させることである.
二、ロックの粒度を小さくする
ロックの粒度を小さくすることは,マルチスレッドロック競合を弱める有効な手段でもある.この技術の典型的な使用シーンはConcurrentHashMapクラスの実装である.
HashMapにとって最も重要な2つの方法はget()とput()である.1つの自然な考え方は、HashMap全体にロックをかけることで、スレッドの安全なHashMapが得られるに違いないが、ロック粒子が大きすぎるため、システム性能が低下する.
ConcurrentHashMapの内部には、セグメント(segment)と呼ばれるいくつかの小さなHashMapがさらに細分化されており、デフォルトではConcurrentHashMapの内部は16セグメントに細分化されています.
ConcurrentHashMapにテーブル・アイテムを追加する必要がある場合は、HashMap全体をロックする必要はありません.まず、hashcodeに基づいてテーブル・アイテムがどのセグメントに格納されるべきかを取得し、その後、セグメントをロックし、put()操作を完了します.マルチスレッド環境では、複数のスレッドがput()操作を同時に行う場合、追加されたテーブル項目が同じセグメントに格納されない限り、スレッド間で真の並列が可能になります.
デフォルトでは16セグメントがあるため、幸いなことに、ConcurrentHashMapは16スレッドの同時挿入(いずれも異なるセグメントに挿入)を同時に受け入れることができ、システムのスループットを大幅に向上させることができます.次はConcurrentHashMapソースコードのputメソッドです.
public V put(K key, V value) {
Segment s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
セグメントのシーケンス番号を取得してから、対応するセグメントにデータを挿入します.
しかし、ロックの粒度を減らすと、システムがグローバルロックを取得する必要がある場合に消費されるリソースが多いという新しい問題が導入されます.ConcurrentHashMapはputメソッドでロックをうまく分離していますが、グローバル情報にアクセスするには、すべてのセグメントのロックを取得する必要があります.例えばConcurrentHashMapのsize()メソッドを取得し、
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
final Segment[] segments = this.segments;
int size;
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
先ensureSegment(j).lock(); すべてロックしてfinallyを外します.
ロック粒度の低減とは、ロックのロック範囲を縮小し、ロック衝突の可能性を低減し、システムの同時能力を向上させることである.
三、独占ロックの代わりに読み書き分離
ReadWriteLock(ReadWriteLock)編で述べたように、ReadWriteLockを使用するとシステムのパフォーマンスが向上します.読み書きロックを使用することは、ロックの粒度を小さくする特別な場合です.
(二)で述べたロック粒度の低減は、データ構造を分割することによって実現され、読み書きロックはシステム機能点の分割によって実現される.読み書きが少ない場合、読み書き分離ロックを使用すると、システムの同時能力を効果的に向上させることができます.
四、ロック分離
読み書きロックの思想をさらに伸ばせば、ロックを分離することになる.読み書きロックは、読み書き操作機能によって、有効なロック分離が行われます.アプリケーションの機能特徴に基づいて,類似の分離思想を用いて,独立ロックを分離することもできる.典型的な例はLinkedBlockingQueueの実装です
LinkedBlockingQueueの実装ではtake()関数とput()関数がそれぞれ実装され,キューからデータを取り出す機能とキューにデータを追加する機能がある.両方の関数は、現在のキューに対して変更操作を行います.しかしLinkedBlockingQueueはチェーンテーブルであるため、2つの操作はそれぞれフロントエンドとテールに対する操作であり、理論的にはこの2つの操作は実際には衝突しない.
排他ロックを使用する場合は、take()とput()の操作時にキュー全体をロックする必要があります.このtake()とput()は同時に実行することはできません.実際の実行では、相手のロックを待つ必要があります.
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node node = new Node(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly(); // , put
try {
while (count.get() == capacity) { // ,
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal(); // ,
} finally {
putLock.unlock(); //
}
if (c == 0)
signalNotEmpty(); // take()
}
五、鎖粗化
通常、マルチスレッド間の効率的な同時性を保証するために、各スレッドがロックを保持する時間をできるだけ短くする必要があります.すなわち、共通リソースが使用された後、すぐにロックを解除します.このようにしてこそ、このロックに待機している他のスレッドは、リソース実行を早期に取得することができます.しかし、何事にも度があり、ロックが絶えず要求されたら、同期して、解放します.それ自体も多くの資源を消費します.逆にシステムの性能に不利です.
このため、仮想マシンは、1つのロックを連続的に要求し、解放する操作に遭遇すると、すべてのロック操作をロックに対する1回の要求に統合し、ロックに対する要求同期回数を減少させ、この操作をロックの粗化と呼ぶ.
実際の開発過程では,特にループ内でロックを要求する場合,合理的な場合にロックの粗化を行うことを意識すべきである.
for(inti = 0; i < CIRCLE; I ++){
synochronized(lock){
}
}
このようにすると、サイクルごとにロックを追加し、ロックを解放する必要があることを意味しますが、このような状況は明らかに必要ありません.より合理的な方法は、ループにロックを追加することです
synochronized(lock){
for(inti = 0; i < CIRCLE; I ++){
}
}
パフォーマンスの最適化は、実行時の実際の状況に基づいて、各リソースポイントを比較するプロセスです.ロック粗化の考え方は、ロックの持ち時間を減らすこととは逆ですが、場合によっては効果が異なるので、実際の状況に応じてバランスをとる必要があります.