次の例を考えてみましょう。メールボックスを担当するサービスがあります。 新しいメッセージを追加するためのメソッドと、メッセージを読み取るためのメソッドがあります。 もちろん、マルチスレッド環境でサービスを開始します-互いに無数のメッセージを送信する何千人ものユーザーがいます。 メールボックスをデータ破損から保護するために、複数のスレッドによる1つのメールボックスの同時変更を許可しないでください。
public void sendMessage(UserId from, UserId to, Message message) { Mailbox sendersBox = getMailbox(from); Mailbox recipientsBox = getMailbox(to); min(sendersBox,recipientsBox).lock(); max(sendersBox, recipientsBox).lock(); sendersBox.addIncomingMessage(message); recipientsBox.addSentMessage(message); max(sendersBox, recipientsBox).lock(); min(sendersBox,recipientsBox).lock(); }
このコードは、メールボックスに対して、常に明確な結果を返すmin / max関数があることを前提としています。たとえば、ハッシュコード、ホストIDなどを比較します。 これは、2つのボックスを変更するときに常にロックを同じ順序(最初に小さい)にして、デッドロックを防ぐために必要です。 それを念頭に置いて、コードは比較的安全に見えますよね?
実際、このコードのセキュリティは、 getMailbox()メソッドが実際にどのように実装されているかによって異なります。 彼が同じオブジェクト(さらに、「同じ」ではなく「同じ」)の返却を保証できれば、安全です。 残念ながら、そのような保証を実装することはほとんど不可能です。 一方、もちろんsendMessage()メソッド全体にsynchronizedを設定することもできますが、使用できるスレッドとプロセッサの数に関係なく、一度に1つのメッセージしか送信できないため、芽の生産性が低下します。
getMailbox()を正しく実装する方法の例を次に示します。
private Mailbox getMailbox(UserId id) { Mailbox mailbox = cache.get(id); if (mailbox == null) { mailbox = new Mailbox(); cache.put(id, mailbox); } return mailbox; }
明らかに、この実装は安全ではありません。本質的に同じメールボックス(つまり、同じユーザーに属する)を異なるスレッドから同時に作成できます。 これは、システムのある時点で1人のユーザーの2つの異なるボックスが表示され、クトゥルフだけがどちらが生き残るかを知ることを意味します。 新しいメールボックスの作成をグローバルにブロックすることでこの問題を解決できますが、これもパフォーマンスのあるレーキです。 ConcurrentMapとあらゆる種類のputIfAbsentをお楽しみください 。 しかし、新しいメールボックスを作成するだけでなく、データベースから既存のメールボックスもロードするとします。 そうすると、正しく同期するのがはるかに困難になります(そして、データベースへの不要なクエリを防ぐのが良いでしょう)。
幸いなことに、IdBasedLockingはまさにこの問題を解決します。 特定の 「メールボックス」 オブジェクトをブロックする代わりに、ユーザーごとに1つのメールボックスがあるという事実に基づいて、 メールボックスの概念をブロックします。
IdBasedLockManager<UserId> manager = new SafeIdBasedLockManager<UserId>(); public void sendMessageSafe(UserId from, UserId to, Message message) { IdBasedLock<UserId> minLock = manager.obtainLock(min(from, to)); IdBasedLock<UserId> maxLock = manager.obtainLock(max(from, to)); minLock.lock(); maxLock.lock(); try { Mailbox sendersBox = getMailbox(from); Mailbox recipientsBox = getMailbox(to); sendersBox.addIncomingMessage(message); recipientsBox.addSentMessage(message); } finally { maxLock.unlock(); minLock.unlock(); } }
2つのオブジェクトを同時に変更するため、例は人為的に複雑に見えるかもしれません。 次のカウンター例が示すように、IdBasedLockingは単一のオブジェクトで正常に機能します。
まず、壊れたバージョン:
public void increaseCounterUnsafe(String id) { Counter c = counterCache.get(id); if (c == null) { c = new Counter(); counterCache.put(id, c); } c.increase(); }
そして今、安全なバージョン:
public void increaseCounterSafely(String id) { IdBasedLock<String> lock = lockManager.obtainLock(id); lock.lock(); try{ Counter c = counterCache.get(id); if (c == null) { c = new Counter(); counterCache.put(id, c); } c.increase(); } finally { lock.unlock(); } }
githubの IdBasedLockingプロジェクト。
maven、gradle、ivyを使用して、または単にcentralから使用できます。 または、 githubで自分でビルドします。
ご清聴ありがとうございました。