1. 概要

Javaでの同期は、マルチスレッドの問題を取り除くのに非常に役立ちます。 ただし、同期の原則は、それらを慎重に使用しないと、多くの問題を引き起こす可能性があります。

このチュートリアルでは、同期に関連するいくつかの悪い習慣と、各ユースケースのより良いアプローチについて説明します。

2. 同期の原理

原則として、外部コードがロックしないことが確実なオブジェクトでのみ同期する必要があります

つまり、同期にプールされたオブジェクトまたは再利用可能なオブジェクトを使用することは悪い習慣です。 その理由は、プールされた/再利用可能なオブジェクトはJVM内の他のプロセスにアクセス可能であり、外部/信頼できないコードによるそのようなオブジェクトへの変更は、デッドロックと非決定的な動作を引き起こす可能性があるためです。

次に、 String Boolean Integer Objectなどの特定のタイプに基づく同期の原則について説明します。

3. 文字列リテラル

3.1. 悪い習慣

文字列リテラルはプールされ、Javaで再利用されることがよくあります。 したがって、同期synchronizedキーワードでStringタイプを使用することはお勧めしません。

public void stringBadPractice1() {
    String stringLock = "LOCK_STRING";
    synchronized (stringLock) {
        // ...
    }
}

同様に、 private final String リテラルを使用する場合でも、定数プールから参照されます。

private final String stringLock = "LOCK_STRING";
public void stringBadPractice2() {
    synchronized (stringLock) {
        // ...
    }
}

さらに、同期のためにインターン文字列を使用することは悪い習慣と見なされます。

private final String internedStringLock = new String("LOCK_STRING").intern();
public void stringBadPractice3() {
  synchronized (internedStringLock) {
      // ...
  }
}

Javadocs に従って、 intern メソッドは、Stringオブジェクトの正規表現を取得します。 つまり、 intern メソッドは、プールから String を返し、プールがない場合は、このStringと同じ内容のStringをプールに明示的に追加します。

したがって、再利用可能なオブジェクトでの同期の問題は、インターンされたStringオブジェクトでも発生します。

注:すべての文字列リテラルと文字列値の定数式は自動的にインターンされます

3.2. 解決

String リテラルでの同期に関する不正行為を回避するための推奨事項は、新しいキーワードを使用してStringの新しいインスタンスを作成することです。

すでに説明したコードの問題を修正しましょう。 最初に、新しい String オブジェクトを作成して、(再利用を避けるために)一意の参照と、同期に役立つ独自の組み込みロックを設定します。

次に、オブジェクトprivatefinalを保持して、外部/信頼できないコードがオブジェクトにアクセスするのを防ぎます。

private final String stringLock = new String("LOCK_STRING");
public void stringSolution() {
    synchronized (stringLock) {
        // ...
    }
}

4. ブール値リテラル

truefalseの2つの値を持つBooleanタイプは、ロックの目的には適していません。 JVMのStringリテラルと同様に、 boolean リテラル値も、Booleanクラスの一意のインスタンスを共有します。

Booleanロックオブジェクトで同期する悪いコード例を見てみましょう。

private final Boolean booleanLock = Boolean.FALSE;
public void booleanBadPractice() {
    synchronized (booleanLock) {
        // ...
    }
}

ここで、外部コードも同じ値の Boolean リテラルで同期すると、システムが応答しなくなったり、デッドロック状態になる可能性があります。

したがって、 ブール値 同期ロックとしてのオブジェクト。

5. 箱入りプリミティブ

5.1. 悪い習慣

boolean リテラルと同様に、ボックス化された型は、一部の値に対してインスタンスを再利用する場合があります。 その理由は、JVMがバイトとして表すことができる値をキャッシュして共有するためです。

たとえば、ボックス化されたタイプIntegerで同期する悪いコード例を書いてみましょう。

private int count = 0;
private final Integer intLock = count; 
public void boxedPrimitiveBadPractice() { 
    synchronized (intLock) {
        count++;
        // ... 
    } 
}

5.2. 解決

ただし、 boolean リテラルとは異なり、ボックス化されたプリミティブでの同期の解決策は、新しいインスタンスを作成することです。

String オブジェクトと同様に、 new キーワードを使用して、独自の組み込みロックを持つ Integer オブジェクトの一意のインスタンスを作成し、それを保持する必要があります privateおよびfinal

private int count = 0;
private final Integer intLock = new Integer(count);
public void boxedPrimitiveSolution() {
    synchronized (intLock) {
        count++;
        // ...
    }
}

6. クラスの同期

クラスがthisキーワードを使用してメソッド同期またはブロック同期を実装する場合、JVMはオブジェクト自体をモニター(その組み込みロック)として使用します。

信頼できないコードは、アクセス可能なクラスの固有のロックを取得して無期限に保持する可能性があります。 その結果、これはデッドロック状態を引き起こす可能性があります。

6.1. 悪い習慣

たとえば、synchronizedメソッドsetNameを使用してAnimalクラスを作成し、synchronizedを使用してメソッドsetOwnerを作成します。ブロック:

public class Animal {
    private String name;
    private String owner;
    
    // getters and constructors
    
    public synchronized void setName(String name) {
        this.name = name;
    }

    public void setOwner(String owner) {
        synchronized (this) {
            this.owner = owner;
        }
    }
}

それでは、 Animal クラスのインスタンスを作成し、その上で同期するいくつかの悪いコードを書いてみましょう。

Animal animalObj = new Animal("Tommy", "John");
synchronized (animalObj) {
    while(true) {
        Thread.sleep(Integer.MAX_VALUE);
    }
}

ここで、信頼できないコード例では、無期限の遅延が発生し、setNameメソッドとsetOwnerメソッドの実装が同じロックを取得できなくなります。

6.2. 解決

この脆弱性を防ぐための解決策は、プライベートロックオブジェクトです。

オブジェクト自体の組み込みロックの代わりに、クラス内で定義されたObjectクラスのプライベート最終インスタンスに関連付けられた組み込みロックを使用するという考え方です。

また、メソッドの同期の代わりにブロックの同期を使用して、同期されていないコードをブロックに入れないようにする柔軟性を追加する必要があります。

それでは、Animalクラスに必要な変更を加えましょう。

public class Animal {
    // ...

    private final Object objLock1 = new Object();
    private final Object objLock2 = new Object();

    public void setName(String name) {
        synchronized (objLock1) {
            this.name = name;
        }
    }

    public void setOwner(String owner) {
        synchronized (objLock2) {
            this.owner = owner;
        }
    }
}

ここでは、同時実行性を高めるために、複数の private final ロックオブジェクトを定義してロックスキームを細かくし、setNamesetOwner[の両方の方法の同期に関する懸念を分離しました。 X225X]。

さらに、synchronizedブロックを実装するメソッドがstatic変数を変更する場合、staticオブジェクトをロックして同期する必要があります。

private static int staticCount = 0;
private static final Object staticObjLock = new Object();
public void staticVariableSolution() {
    synchronized (staticObjLock) {
        count++;
        // ...
    }
}

7. 結論

この記事では、 String Boolean Integer Objectなどの特定のタイプでの同期に関連するいくつかの悪い習慣について説明しました。

この記事からの最も重要なポイントは、同期にプールされたオブジェクトまたは再利用可能なオブジェクトを使用することは推奨されないということです。

また、Objectクラスのプライベートファイナルインスタンスで同期することをすることをお勧めします。 このようなオブジェクトは、 public クラスと相互作用する可能性のある外部/信頼できないコードにアクセスできないため、このような相互作用によってデッドロックが発生する可能性が低くなります。

いつものように、ソースコードはGitHubから入手できます。