English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
javaでロックのパフォーマンスを向上させる方法
私たちは自分たちの製品が直面する問題に対して解決策を考えていますが、この記事では、デッドロックを検出するツールを使用せずに、分離ロック、並行データ構造、データをコードではなく保護し、ロックの範囲を狭めるなどのいくつかの一般的な技術を共有します。
問題の根源はロックではなく、ロック間の競争です
通常、マルチスレッドのコードでパフォーマンスの問題に直面した場合、一般的にはロックの問題を不満に思います。なぜなら、ロックはプログラムの実行速度を低下させ、低い拡張性が広く知られているからです。したがって、この「常識」を持ってコードの最適化を始めると、その後、不快な並行問題が発生する可能性が高いです。
したがって、競争ロックと非競争ロックの違いを理解することが非常に重要です。あるスレッドが別のスレッドが実行している同期ブロックまたはメソッドに入ろうとすると、ロック競争が発生します。そのスレッドは強制的に待機状態に入り、最初のスレッドが同期ブロックを実行し、監視器を解放するまで待ちます。同一時間に1つのスレッドしか同期のコードエリアを実行しようとしない場合、ロックは非競争状態を保ちます。
実際には、競争がなく、多くのアプリケーションで、JVMは既に同期を最適化しています。競争が無いロックは実行中に追加のコストを引き起こしません。したがって、パフォーマンス問題についてロックを不満に思うべきではなく、ロックの競争を不満に思うべきです。この認識を得た後、競争の可能性を低減したり、競争の持続時間を短縮するためには何ができるかを見てみましょう。
データを保護するのではなく、コードを保護する
スレッドセキュリティ問題を解決する速やかな方法の一つは、メソッドのアクセス全体にロックをかけるとされています。例えば、以下の例では、この方法を使用してオンラインパーカーゲームサーバーを構築しようと試みています:
class GameServer { public Map<String, List<Player>> tables = new HashMap<String, List<Player>>(); public synchronized void join(Player player, Table table) { if (player.getAccountBalance() > table.getLimit()) { List<Player> tablePlayers = tables.get(table.getId()); if (tablePlayers.size() < 9) { tablePlayers.add(player); } } } public synchronized void leave(Player player, Table table) {/*簡略化のため本文省略*/} public synchronized void createTable() {/*簡略化のため本文省略*/} public synchronized void destroyTable(Table table) {/*簡略化のため本文省略*/} }
著者の意図は良いもので、新しいプレイヤーがテーブルに参加したとき、テーブル上のプレイヤーの数がテーブルが収容できる最大プレイヤー数を超えないことを確保する必要があります。9.
しかし、この解決策は実際にはいつでもプレイヤーのテーブルへの参加を制御する必要があります——特にサーバーのアクセスが少ない時でも、ロックを待つスレッドはシステムの競合イベントを頻繁に引き起こします。アカウント残高とテーブル制限の確認を含むロックブロックは、呼び出し操作のコストを大幅に増加させ、競合の可能性と持続時間を増加させる可能性があります。
最初の解決策は、データを保護する必要があることを確かめることです。メソッド宣言からメソッド体に移動した同期声明が、上記のシンプルな例ではあまり影響を与えないかもしれません。しかし、ゲームサービスの全体のインターフェースに立って考える必要があります——単なるjoin()メソッドではなく。
class GameServer { public Map<String, List<Player>> tables = new HashMap<String, List<Player>>(); public void join(Player player, Table table) { synchronized (tables) { if (player.getAccountBalance() > table.getLimit()) { List<Player> tablePlayers = tables.get(table.getId()); if (tablePlayers.size() < 9) { tablePlayers.add(player); } } } } public void leave(Player player, Table table) {/* 簡略化のため本文省略 */} public void createTable() {/* 簡略化のため本文省略 */} public void destroyTable(Table table) {/* 簡略化のため本文省略 */} }
最初は小さな変更に過ぎなかったかもしれませんが、影響を受けるのは整个クラスの動作スタイルです。プレイヤーがいつでもテーブルに参加すると、先の同期メソッドはGameServerインスタンス全体にロックをかけ、同時にテーブルから退室しようとするプレイヤーとの競合が発生します。ロックをメソッド宣言からメソッド体に移動することで、ロックのロードを遅らせ、ロック競合の可能性を減少させます。
ロックの範囲を狭める
今やデータを保護する必要があることを確信した後、必要な場所でのみロックを確保する必要があります——例えば、上記のコードがリファクタリングされた後:
public class GameServer { public Map<String, List<Player>> tables = new HashMap<String, List<Player>>(); public void join(Player player, Table table) { if (player.getAccountBalance() > table.getLimit()) { synchronized (tables) { List<Player> tablePlayers = tables.get(table.getId()); if (tablePlayers.size() < 9) { tablePlayers.add(player); } } } } //省略して説明省略 }
プレイヤーのアカウント残高の確認(IO操作が発生する可能性がある)が時間がかかる可能性のある操作を引き起こす可能性のあるコードが、ロック制御の範囲外に移されました。注意してください、現在のロックはプレイヤーの数がテーブルが収容できる人数を超えないようにするためにのみ使用されており、アカウント残高の確認はこの保護手段の一部ではありません。
分離ロック
上記の例の最後の行コードからは明らかに、整个データ構造は同じロックで保護されています。このデータ構造では、数以千計のテーブルが存在し、各テーブルの人数が容量を超えないように保護する必要があるため、競合が発生するリスクが非常に高いことを考慮すると、
このことについて簡単な解決策は、各テーブルに分離ロックを導入することです。以下の例を参照してください:
public class GameServer { public Map<String, List<Player>> tables = new HashMap<String, List<Player>>(); public void join(Player player, Table table) { if (player.getAccountBalance() > table.getLimit()) { List<Player> tablePlayers = tables.get(table.getId()); synchronized (tablePlayers) { if (tablePlayers.size() < 9) { tablePlayers.add(player); } } } } //省略して説明省略 }
今では、すべてのテーブルではなく、単一のテーブルのアクセスのみをシンクロナイズしています。これにより、ロック競合が発生する可能性が大幅に低下します。具体的な例を挙げると、データ構造には以下のようなものがあります:100個のテーブルのインスタンスがあれば、今までよりも競合が発生する可能性は低くなります100倍。
スレッドセーフなデータ構造の使用
もう一つ改善できる点は、伝統的な単一スレッドデータ構造を捨てて、明確にスレッドセーフに設計されたデータ構造を使用することです。例えば、あなたのテーブルインスタンスをConcurrentHashMapで保存する場合、コードは以下のようになります:
public class GameServer { public Map<String, List<Player>> tables = new ConcurrentHashMap<String, List<Player>>(); public synchronized void join(Player player, Table table) {/*省略して方法の本体をスキップします。*/} public synchronized void leave(Player player, Table table) {/*省略して方法の本体をスキップします。*/} public synchronized void createTable() { Table table = new Table(); tables.put(table.getId(), table); } public synchronized void destroyTable(Table table) { tables.remove(table.getId()); } }
join()とleave()メソッド内のシンクロナイズブロックは、前の例と同様に維持されます。なぜなら、単一のテーブルデータの整合性を保証する必要があるからです。ConcurrentHashMapはこの点で何の助けにもなりません。しかし、increateTable()とdestoryTable()メソッドでは、新しいテーブルの作成と破棄にConcurrentHashMapを使用し、これらの操作はConcurrentHashMapに対して完全にシンクロナイズされています。これにより、並列でテーブルの数を追加または減少させることができます。
他の提案と技術
ロックの可視性を低下させます。上記の例では、ロックがpublicとして宣言されている(外部から見える)ため、他の悪意のある人々があなたの慎重に設計された監視器にロックをかけ、あなたの作業を破壊することができるかもしれません。
java.util.concurrent.locksのAPIを確認して、他に既に実装されているロック戦略があれば、それを利用して上記のソリューションを改善します。
アトミック操作を使用します。現在使用中のシンプルなインクリメントカウンタは、実際にはロックが必要ではありません。上記の例では、IntegerではなくAtomicIntegerを使用する方が適しています。
最後に、Plumberの自動デッドロック検出ソリューションを使用しているかどうかに関わらず、または手動でスレッドダンプから解決策の情報を得ているかどうかに関わらず、この記事があなたのロック競合問題を解決するのに役立つことを願っています。
ご覧いただきありがとうございます。皆様のこのサイトへのサポートに感謝します。