非最終フィールドの同期


91

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

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)
     {  
     }  
   }  
 }  

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

回答:


127

まず第一に、より高いレベルの抽象化で並行性の問題に対処するために本当に一生懸命努力することをお勧めします。つまり、ExecutorServices、Callables、Futuresなどのjava.util.concurrentのクラスを使用して解決します。

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

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


1
私は、それを言っている場合は、あなたが非最後のフィールドで同期、あなたがオブジェクトに排他的にアクセスして、コードの実行のスニペットがあるという事実を認識しておく必要がありo、同期ブロックに達した時に参照しました。o参照するオブジェクトが変更された場合、別のスレッドがやって来て、同期されたコードブロックを実行できます。
aioobe 2011

42
私はあなたの親指のルールに同意しません-私は他の状態を守ることを唯一の目的とするオブジェクトで同期することを好みます。オブジェクトをロックする以外に何もしていない場合は、他のコードでロックできないことは間違いありません。次にメソッドを呼び出す「実際の」オブジェクトをロックすると、そのオブジェクトもそれ自体で同期できるため、ロックについて推論するのが難しくなります。
Jon Skeet 2011

9
私の答えで言うように、私はそれを私に非常に注意深く正当化する必要があると思います、なぜあなたはそのようなことをしたいのですか?またthis、で同期することもお勧めしません-ロックの目的でのみクラスにfinal変数を作成することをお勧めします。これにより、他の人が同じオブジェクトをロックするのを防ぐことができます。
Jon Skeet 2011

1
それはもう一つの良い点です、そして私は同意します。非最終変数をロックするには、慎重に正当化する必要があります。
aioobe 2011

同期に使用されるオブジェクトの変更に関するメモリの可視性の問題についてはよくわかりません。「コードの同じセクションを並行して実行できるように」、オブジェクトを変更してから、その変更を正しく確認するコードに依存することは、大きな問題になると思います。同期ブロック内でアクセスされる変数とは対照的に、メモリモデルによって、ロックに使用されるフィールドのメモリの可視性にどのような保証が拡張されるかはわかりません。私の経験則では、何かを同期する場合、それは最終的なものでなければなりません。
マイクQ

47

同期されたブロックが一貫した方法で実際に同期されなくなったため、これは実際には良い考えではありません。

同期されたブロックが、一度に1つのスレッドのみが一部の共有データにアクセスすることを保証することを目的としていると仮定して、次のことを考慮してください。

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

なぜこれを実現したいのですか?おそらく、それが理にかなっている非常に特殊な状況がいくつかあります...しかし、私が満足する前に、特定のユースケースを(上記のシナリオの種類を軽減する方法とともに)提示する必要がありますそれ。


2
@aioobe:しかし、スレッド1は、リストを変更している(そして頻繁に参照しているo)コードを実行している可能性があります。実行の途中で、別のリストの変更を開始します。それはどのように良い考えでしょうか?他の方法で触れたオブジェクトをロックするのが良いかどうかについては、基本的に意見が分かれていると思います。私はむしろ、ロックに関して他のコードが何をするのかを知らなくても、自分のコードについて推論することができます。
Jon Skeet 2011

2
@Felype:別の質問として、より詳細な質問をする必要があるようですが、はい、ロックと同じように別のオブジェクトを作成することがよくあります。
Jon Skeet 2013

3
@VitBernatik:いいえ。スレッドXが構成の変更を開始し、スレッドYが同期されている変数の値を変更し、スレッドZが構成の変更を開始すると、XとZの両方が同時に構成を変更します。これは悪いことです。 。
Jon Skeet 2015年

1
要するに、そのようなロックオブジェクトを常にfinalと宣言する方が安全ですよね?
St.Antario 2015年

2
@LinkTheProgrammer: "同期されたメソッドは、インスタンス内のすべてのオブジェクトを同期します"-いいえ、そうではありません。それは単に真実ではないので、同期についての理解を再検討する必要があります。
ジョンスキート2015年

12

Johnのコメントの1つに同意します。変数の参照が変更された場合の不整合を防ぐために、非最終変数にアクセスするときは常に最終ロックダミーを使用する必要があります。したがって、どのような場合でも、最初の経験則として:

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

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

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

ただし、ファイナルロックダミーを使用する場合は、別の問題があります。非同期オブジェクトは、synchronize(object)を呼び出すときにのみ、RAMと同期されるため、間違ったデータを取得する可能性があります。したがって、2番目の経験則として:

ルール#2:非最終オブジェクトをロックするときは、常に次の両方を行う必要があります。RAM同期のために、最終ロックダミーと非最終オブジェクトのロックを使用します。(唯一の代替手段は、オブジェクトのすべてのフィールドを揮発性として宣言することです!)

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

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-代替:オブジェクトのすべてのフィールドを揮発性として宣言します。(ここでは、これを行うことの欠点については説明しません。たとえば、読み取りの場合でもxレベルのキャッシュにストレージを保存できないなどです。)

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

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

また、RAMとキャッシュが原因で同期が必要な理由の詳細については、https//stackoverflow.com/a/21409975/2012947をご覧ください。


1
o設定とオブジェクトの読み取りの間に「happens-before」関係を確立するには、のセッターをsynchronized(LOCK)でラップする必要があると思いますo。:私は私の同様の質問でこれを議論していstackoverflow.com/questions/32852464/...
Petrakeas

dataObjectを使用して、dataObjectメンバーへのアクセスを同期します。それはどうして間違っているのですか?dataObjectが別の場所を指し始めた場合は、同時スレッドがデータを変更しないように、新しいデータで同期させたいと思います。それで何か問題はありますか?
ハーメン2015年

2

私はここで正しい答えを見ていません。つまり、それを行ってもまったく問題ありません

なぜそれが警告なのかさえわかりません、それは何も悪いことではありません。JVMは、あなたが得ることを確認するいくつかのあなたが値を読んだときに有効なオブジェクトバック(またはnull)を、そしてあなたが上で同期させることができる任意のオブジェクト。

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

public volatile Object lock;

..。

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

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


これは機能しない可能性があります。oがO1への参照として開始されたとすると、スレッドT1はo(= O1)とO2をロックし、oをO2に設定します。同時に、スレッドT2はO1をロックし、T1がロックを解除するのを待ちます。ロックO1を受信すると、oをO3に設定します。このシナリオでは、T1がO1を解放してからT2がO1をロックするまでの間に、O1はoを介したロックに対して無効になりました。この時点で、別のスレッドがロックにo(= O2)を使用し、T2とのレースで中断することなく続行できます。
GPS

2

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

synchronized()がアトミックである場合にどのように機能するかをコードで確認してください。

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();
    }

}

1
これ問題ないかもしれませんが、メモリモデルに、同期する値が最後に書き込まれた値であるという保証があるかどうかはわかりません。アトミックに「読み取りと同期」する保証はないと思います。個人的には、簡単にするために、とにかく他の用途があるモニターでの同期は避けようとしています。(個別のフィールドを持つことにより、コードは慎重
Jon Skeet 2015年

@ジョン。答えてくれてありがとう!あなたの心配を聞きます。この場合、外部ロックが「同期された原子性」の問題を回避することに同意します。したがって、好ましいでしょう。ランタイムでより多くの構成を導入し、スレッドグループごとに異なる構成を共有したい場合もありますが(私の場合ではありませんが)。そして、この解決策は興味深いものになるかもしれません。私は同期された()原子性の質問stackoverflow.com/questions/29217266/…を投稿しました-それでそれが使用できるかどうかを確認します(そして誰かが返信します)
Vit Bernatik 2015年

2

AtomicReferenceはお客様の要件に適しています。

アトミックパッケージに関するJavaドキュメントから:

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

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を使用するのはいつですか?


1

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

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


1

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.