web-dev-qa-db-ja.com

非最終フィールドの同期

最終クラス以外のフィールドで同期するたびに警告が表示されます。コードは次のとおりです。

public class X  
{  
   private Object o;  

   public void setO(Object o)  
   {  
     this.o = o;  
   }  

   public void x()  
   {  
     synchronized (o) // synchronization on a non-final field  
     {  
     }  
   }  
 } 

そこで、コーディングを次のように変更しました。

 public class X  
 {  

   private final Object o;       
   public X()
   {  
     o = new Object();  
   }  

   public void x()  
   {  
     synchronized (o)
     {  
     }  
   }  
 }  

上記のコードが最終クラス以外のフィールドで同期する適切な方法であるかどうかはわかりません。非最終フィールドを同期するにはどうすればよいですか?

77
divz

まず第一に、より高いレベルの抽象化で並行性の問題に真剣に取り組むこと、つまり、ExecutorServices、Callables、Futuresなどの Java.util.concurrent のクラスを使用してそれを解決することを真剣に試みることをお勧めします。

そうは言っても、最終ではないフィールド自体での同期にはwrongはありません。 オブジェクト参照が変更された場合、コードの同じセクションが並行して実行される可能性があることに留意する必要があります。つまり、1つのスレッドが同期ブロックでコードを実行し、誰かがsetO(...)を呼び出すと、別のスレッドが同じ同期ブロックを同じインスタンスを同時に。

排他的アクセスが必要なオブジェクト(または、さらに良いことに、それを保護する専用のオブジェクト)で同期します。

114
aioobe

同期ブロックは一貫した方法で本当に同期されなくなったため、これは本当に良いアイデアではありません。

同期ブロックは、一度に1つのスレッドのみがいくつかの共有データにアクセスすることを保証することを意図していると想定して、以下を考慮してください。

  • スレッド1は同期ブロックに入ります。イェイ-共有データに排他的にアクセスできます...
  • スレッド2はsetO()を呼び出します
  • スレッド3(または2 ...)が同期ブロックに入ります。ほら!共有データへの排他的アクセス権があると考えられますが、スレッド1はまだそれを使用しています。

なぜあなたはこれが起こることを望んでいますか多分some非常に特殊な状況があり、それが理にかなっています...しかし、あなたは私に特定のユースケースを提示しなければなりません私がそれで満足する前に、私が上で与えた種類のシナリオを緩和する)。

41
Jon Skeet

私はジョンのコメントの1つに同意します:あなたはalways非-変数の参照が変更された場合に矛盾を防ぐための最終変数。したがって、いずれにしても、最初の経験則として:

ルール#1:フィールドがファイナルでない場合、常に(プライベート)ファイナルロックダミーを使用します。

理由1:ロックを保持し、変数の参照を自分で変更します。同期ロックの外側で待機している別のスレッドは、保護されたブロックに入ることができます。

理由#2:ロックを保持し、別のスレッドが変数の参照を変更します。結果は同じです。別のスレッドが保護されたブロックに入ることができます。

しかし、最終ロックダミーを使用する場合、別の問題があります:非最終オブジェクトは、synchronize(object)を呼び出すときにRAMとのみ同期されます。したがって、2番目の経験則として:

ルール#2:非最終オブジェクトをロックするときは、常に両方を行う必要があります:RAM同期。(唯一の選択肢は、オブジェクトのすべてのフィールドをvolatileとして宣言することです!)

これらのロックは、「ネストされたロック」とも呼ばれます。 常に同じ順序で呼び出す必要があることに注意してください。そうしないと、デッドロックが発生します

_public class X {
    private final LOCK;
    private Object o;

    public void setO(Object o){
        this.o = o;  
    }  

    public void x() {
        synchronized (LOCK) {
        synchronized(o){
            //do something with o...
        }
        }  
    }  
} 
_

ご覧のとおり、2つのロックは常に一緒に属しているため、同じ行に直接書き込みます。このように、10個のネストロックを行うこともできます。

_synchronized (LOCK1) {
synchronized (LOCK2) {
synchronized (LOCK3) {
synchronized (LOCK4) {
    //entering the locked space
}
}
}
}
_

別のスレッドがsynchronized (LOCK3)のような内部ロックを取得するだけであれば、このコードは壊れません。しかし、次のような別のスレッドを呼び出すと中断します。

_synchronized (LOCK4) {
synchronized (LOCK1) {  //dead lock!
synchronized (LOCK3) {
synchronized (LOCK2) {
    //will never enter here...
}
}
}
}
_

ファイナル以外のフィールドを処理する際に、このようなネストされたロックを回避する方法は1つだけです。

ルール#2-代替:オブジェクトのすべてのフィールドをvolatileとして宣言します。(これを行うことの不利な点、たとえば、読み取りの場合でもxレベルのキャッシュに保存されます)

したがって、aioobeはまったく正しいです。Java.util.concurrentを使用するだけです。または、同期に関するすべてを理解し始め、ネストされたロックを使用して自分でそれを行います。 ;)

非最終フィールドでの同期が壊れる理由の詳細については、テストケースをご覧ください: https://stackoverflow.com/a/21460055/2012947

また、RAMとキャッシュがこちらにあるため、まったく同期する必要がある理由について詳しくは: https://stackoverflow.com/a/21409975/2012947

12
Marcus

私はここで正しい答えを本当に見ていません、つまり、それを行うことは完全に大丈夫です。

なぜ警告であるのかさえ分かりませんが、何も問題はありません。 JVMは、値を読み取るときにsomeの有効なオブジェクト(またはnull)を取得し、anyのオブジェクトで同期できるようにします。

使用中にロックを実際に変更する予定がある場合(たとえば、使用を開始する前にinitメソッドからロックを変更するのではなく)、変更する予定の変数をvolatileにする必要があります。その後、行う必要があるのはboth古いオブジェクトと新しいオブジェクトで同期することだけで、値を安全に変更できます。

public volatile Object lock;

...

synchronized (lock) {
    synchronized (newObject) {
        lock = newObject;
    }
}

そこ。複雑ではありません。ロック(ミューテックス)を使用したコードの記述は、実際には非常に簡単です。それらを使わずにコードを書く(ロックフリーコード)のは難しいことです。

2
Evan Dark

AtomicReference 要件に適合します。

From Java atomic パッケージに関するドキュメント:

単一変数のロックフリースレッドセーフプログラミングをサポートするクラスの小さなツールキット。本質的に、このパッケージのクラスは、揮発性の値、フィールド、および配列要素の概念を、フォームのアトミックな条件付き更新操作も提供するものに拡張します。

boolean compareAndSet(expectedValue, updateValue);

サンプルコード:

String initialReference = "value 1";

AtomicReference<String> someRef =
    new AtomicReference<String>(initialReference);

String newReference = "value 2";
boolean exchanged = someRef.compareAndSet(initialReference, newReference);
System.out.println("exchanged: " + exchanged);

上記の例では、Stringを独自のObjectに置き換えます

関連するSEの質問:

JavaでAtomicReferenceを使用する場合

2
Ravindra babu

編集:したがって、このソリューション(Jon Skeetによって提案された)は、オブジェクト参照の変更中に「synchronized(object){}」の実装の原子性に問題がある可能性があります。私は別に尋ねました、そして、エリクソン氏によると、それはスレッドセーフではありません-参照: 同期ブロックアトミックに入る? 。だからそれをしない方法の例としてそれを取る-理由リンク;)

Synchronised()がアトミックである場合の動作を示すコードを参照してください。

public class Main {
    static class Config{
        char a='0';
        char b='0';
        public void log(){
            synchronized(this){
                System.out.println(""+a+","+b);
            }
        }
    }

    static Config cfg = new Config();

    static class Doer extends Thread {
        char id;

        Doer(char id) {
            this.id = id;
        }

        public void mySleep(long ms){
            try{Thread.sleep(ms);}catch(Exception ex){ex.printStackTrace();}
        }

        public void run() {
            System.out.println("Doer "+id+" beg");
            if(id == 'X'){
                synchronized (cfg){
                    cfg.a=id;
                    mySleep(1000);
                    // do not forget to put synchronize(cfg) over setting new cfg - otherwise following will happend
                    // here it would be modifying different cfg (cos Y will change it).
                    // Another problem would be that new cfg would be in parallel modified by Z cos synchronized is applied on new object
                    cfg.b=id;
                }
            }
            if(id == 'Y'){
                mySleep(333);
                synchronized(cfg) // comment this and you will see inconsistency in log - if you keep it I think all is ok
                {
                    cfg = new Config();  // introduce new configuration
                    // be aware - don't expect here to be synchronized on new cfg!
                    // Z might already get a lock
                }
            }
            if(id == 'Z'){
                mySleep(666);
                synchronized (cfg){
                    cfg.a=id;
                    mySleep(100);
                    cfg.b=id;
                }
            }
            System.out.println("Doer "+id+" end");
            cfg.log();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Doer X = new Doer('X');
        Doer Y = new Doer('Y');
        Doer Z = new Doer('Z');
        X.start();
        Y.start();
        Z.start();
    }

}
2
Vit Bernatik

2セントを追加するだけです:デザイナーを介してインスタンス化されるコンポーネントを使用したときにこの警告が表示されたため、コンストラクターはパラメーターを取得できないため、フィールドは実際には最終的になりません。言い換えれば、最終キーワードのない準決勝フィールドがありました。

それが単なる警告である理由だと思います。おそらくあなたは何か間違ったことをしているかもしれませんが、それは正しいかもしれません。

1
peenut

oのインスタンスの存続期間中にXが変更されない場合、2番目のバージョンは、同期が含まれているかどうかに関係なく、より良いスタイルです。

さて、最初のバージョンに何か問題があるかどうかは、そのクラスで他に何が起こっているかを知らずに答えることは不可能です。私はコンパイラがエラーを起こしやすいように見えることに同意する傾向があります(他の人が言ったことを繰り返しません)。

1
NPE