Sinkronisasi bidang non-final

91

Peringatan muncul setiap kali saya menyinkronkan pada bidang kelas non-final. Ini kodenya:

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

jadi saya mengubah pengkodean dengan cara berikut:

 public class X  
 {  

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

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

Saya tidak yakin kode di atas adalah cara yang tepat untuk melakukan sinkronisasi pada bidang kelas non-final. Bagaimana cara menyinkronkan bidang non final?

divz
sumber

Jawaban:

127

Pertama-tama, saya mendorong Anda untuk benar-benar berusaha keras untuk menangani masalah konkurensi pada tingkat abstraksi yang lebih tinggi, yaitu menyelesaikannya menggunakan kelas dari java.util.concurrent seperti ExecutorServices, Callables, Futures, dll.

Karena itu, tidak ada yang salah dengan menyinkronkan pada bidang non-final itu sendiri. Anda hanya perlu mengingat bahwa jika referensi objek berubah, bagian kode yang sama dapat dijalankan secara paralel . Yaitu, jika satu utas menjalankan kode di blok tersinkronisasi dan seseorang memanggil setO(...), utas lain dapat menjalankan blok tersinkronisasi yang sama pada contoh yang sama secara bersamaan.

Sinkronisasi pada objek yang Anda butuhkan akses eksklusifnya (atau, lebih baik lagi, objek yang didedikasikan untuk menjaganya).

aioobe
sumber
1
Saya mengatakan bahwa, jika Anda menyinkronkan pada bidang non-final, Anda harus menyadari fakta bahwa potongan kode berjalan dengan akses eksklusif ke objek yang odirujuk pada saat blok yang disinkronkan tercapai. Jika objek yang omerujuk ke perubahan, thread lain dapat datang dan menjalankan blok kode tersinkronisasi.
aioobe
42
Saya tidak setuju dengan aturan praktis Anda - Saya lebih suka melakukan sinkronisasi pada objek yang tujuan utamanya adalah untuk menjaga negara lain. Jika Anda tidak pernah melakukan apa pun dengan objek selain menguncinya, Anda tahu pasti bahwa tidak ada kode lain yang dapat menguncinya. Jika Anda mengunci objek "nyata" yang metodenya kemudian Anda panggil, objek tersebut juga dapat menyinkronkan dirinya sendiri, sehingga lebih sulit untuk menjelaskan tentang penguncian tersebut.
Jon Skeet
9
Seperti yang saya katakan dalam jawaban saya, saya pikir saya harus membenarkannya dengan sangat hati-hati kepada saya, mengapa Anda ingin melakukan hal seperti itu. Dan saya juga tidak akan merekomendasikan sinkronisasi this- Saya akan merekomendasikan membuat variabel terakhir di kelas hanya untuk tujuan penguncian , yang menghentikan orang lain untuk mengunci objek yang sama.
Jon Skeet
1
Itu poin bagus lainnya, dan saya setuju; mengunci variabel non-final pasti membutuhkan pembenaran yang cermat.
aioobe
Saya tidak yakin tentang masalah visibilitas memori seputar mengubah objek yang digunakan untuk sinkronisasi. Saya pikir Anda akan mendapat masalah besar saat mengubah objek dan kemudian mengandalkan kode untuk melihat perubahan itu dengan benar sehingga "bagian kode yang sama dapat dijalankan secara paralel". Saya tidak yakin apa, jika ada, jaminan yang diperluas oleh model memori ke visibilitas memori bidang yang digunakan untuk mengunci, sebagai lawan dari variabel yang diakses dalam blok sinkronisasi. Aturan praktis saya adalah, jika Anda menyinkronkan sesuatu, itu harus final.
Mike Q
47

Ini benar-benar bukan ide yang bagus - karena blok tersinkronisasi Anda tidak lagi benar-benar disinkronkan secara konsisten.

Dengan asumsi blok tersinkronisasi dimaksudkan untuk memastikan bahwa hanya satu utas yang mengakses beberapa data bersama pada satu waktu, pertimbangkan:

  • Thread 1 memasuki blok yang disinkronkan. Hore - ini memiliki akses eksklusif ke data bersama ...
  • Thread 2 panggilan setO ()
  • Thread 3 (atau masih 2 ...) memasuki blok tersinkronisasi. Eek! Ia pikir itu memiliki akses eksklusif ke data yang dibagikan, tetapi utas 1 masih mengotak-atiknya ...

Mengapa Anda ingin ini terjadi? Mungkin ada beberapa situasi yang sangat terspesialisasi yang masuk akal ... tetapi Anda harus memberi saya kasus penggunaan tertentu (bersama dengan cara-cara mengurangi jenis skenario yang saya berikan di atas) sebelum saya puas dengan Itu.

Jon Skeet
sumber
2
@aioobe: Tapi kemudian utas 1 masih bisa menjalankan beberapa kode yang memutasi daftar (dan sering merujuk ke o) - dan di tengah jalan eksekusi mulai memutasi daftar yang berbeda. Bagaimana itu bisa menjadi ide yang bagus? Saya pikir pada dasarnya kami tidak setuju apakah mengunci benda yang Anda sentuh dengan cara lain atau tidak. Saya lebih suka bisa bernalar tentang kode saya tanpa pengetahuan tentang apa yang dilakukan kode lain dalam hal penguncian.
Jon Skeet
2
@Felype: Sepertinya Anda harus mengajukan pertanyaan yang lebih detail sebagai pertanyaan terpisah - tapi ya, saya sering membuat objek terpisah hanya sebagai kunci.
Jon Skeet
3
@VitBernatik: Tidak. Jika utas X mulai mengubah konfigurasi, utas Y mengubah nilai variabel yang sedang disinkronkan, lalu utas Z mulai mengubah konfigurasi, maka X dan Z akan mengubah konfigurasi pada saat yang sama, yang mana buruk .
Jon Skeet
1
Singkatnya, lebih aman jika kita selalu mendeklarasikan objek kunci tersebut sebagai final, bukan?
St. Antario
2
@LinkTheProgrammer: "Metode tersinkronisasi menyinkronkan setiap objek dalam instance" - tidak, tidak. Itu tidak benar, dan Anda harus meninjau kembali pemahaman Anda tentang sinkronisasi.
Jon Skeet
12

Saya setuju dengan salah satu komentar John: Anda harus selalu menggunakan dummy kunci terakhir saat mengakses variabel non-final untuk mencegah ketidakkonsistenan jika referensi variabel berubah. Jadi dalam kasus apa pun dan sebagai aturan praktis pertama:

Aturan # 1: Jika sebuah field bukan final, selalu gunakan dummy kunci final (private).

Alasan # 1: Anda memegang kunci dan mengubah referensi variabel sendiri. Utas lain yang menunggu di luar kunci tersinkronisasi akan dapat memasuki blok yang dijaga.

Alasan # 2: Anda memegang kunci dan utas lain mengubah referensi variabel. Hasilnya sama: utas lain dapat memasuki blok yang dilindungi.

Tetapi ketika menggunakan dummy kunci terakhir, ada masalah lain : Anda mungkin mendapatkan data yang salah, karena objek non-final Anda hanya akan disinkronkan dengan RAM saat memanggil sinkronisasi (objek). Jadi, sebagai aturan praktis kedua:

Aturan # 2: Saat mengunci objek non-final, Anda selalu perlu melakukan keduanya: Menggunakan dummy kunci terakhir dan kunci objek non-final demi sinkronisasi RAM. (Satu-satunya alternatif adalah mendeklarasikan semua bidang objek sebagai volatile!)

Kunci ini juga disebut "kunci bersarang". Perhatikan bahwa Anda harus memanggil mereka selalu dalam urutan yang sama, jika tidak, Anda akan menemui jalan buntu :

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...
        }
        }  
    }  
} 

Seperti yang Anda lihat, saya menulis kedua gembok itu langsung pada baris yang sama, karena keduanya selalu saling terkait. Seperti ini, Anda bahkan dapat melakukan 10 kunci bersarang:

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

Perhatikan bahwa kode ini tidak akan rusak jika Anda hanya mendapatkan kunci dalam seperti synchronized (LOCK3)pada utas lainnya. Tetapi itu akan rusak jika Anda memanggil di utas lain sesuatu seperti ini:

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

Hanya ada satu solusi untuk mengatasi kunci bersarang tersebut saat menangani bidang non-final:

Aturan # 2 - Alternatif: Deklarasikan semua bidang objek sebagai volatile. (Saya tidak akan berbicara di sini tentang kerugian melakukan ini, misalnya mencegah penyimpanan apa pun di cache tingkat x bahkan untuk pembacaan, aso.)

Jadi oleh karena itu aioobe cukup tepat: Gunakan saja java.util.concurrent. Atau mulailah memahami segala sesuatu tentang sinkronisasi dan lakukan sendiri dengan kunci bersarang. ;)

Untuk detail lebih lanjut mengapa sinkronisasi pada bidang non-final rusak, lihat kasus pengujian saya: https://stackoverflow.com/a/21460055/2012947

Dan untuk lebih jelasnya mengapa Anda perlu disinkronkan sama sekali karena RAM dan cache, lihat di sini: https://stackoverflow.com/a/21409975/2012947

Marcus
sumber
1
Saya pikir Anda harus membungkus penyetel odengan tersinkronisasi (KUNCI) untuk membangun hubungan "terjadi-sebelum" antara setelan dan objek bacaan o. Saya membahas ini dalam pertanyaan serupa saya: stackoverflow.com/questions/32852464/…
Petrakeas
Saya menggunakan dataObject untuk menyinkronkan akses ke anggota dataObject. Bagaimana itu salah? Jika dataObject mulai menunjuk ke suatu tempat yang berbeda, saya ingin itu disinkronkan pada data baru untuk mencegah thread bersamaan untuk memodifikasinya. Ada masalah dengan itu?
Harmen
2

Saya tidak melihat jawaban yang benar di sini, yaitu, Tidak masalah melakukannya.

Saya bahkan tidak yakin mengapa itu peringatan, tidak ada yang salah dengan itu. JVM memastikan bahwa Anda mendapatkan kembali beberapa objek yang valid (atau null) saat Anda membaca nilai, dan Anda dapat menyinkronkan pada objek apa pun .

Jika Anda berencana untuk benar-benar mengubah kunci saat sedang digunakan (sebagai kebalikan dari misalnya mengubahnya dari metode init, sebelum Anda mulai menggunakannya), Anda harus membuat variabel yang Anda rencanakan untuk diubah volatile. Maka semua yang perlu Anda lakukan adalah untuk melakukan sinkronisasi pada kedua lama dan objek baru, dan Anda dapat dengan aman mengubah nilai

public volatile Object lock;

...

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

Sana. Tidak rumit, menulis kode dengan kunci (mutex) sebenarnya cukup mudah. Menulis kode tanpa mereka (kode bebas kunci) adalah hal yang sulit.

Evan Dark
sumber
Ini mungkin tidak berhasil. Katakanlah o dimulai sebagai ref ke O1, lalu utas T1 mengunci o (= O1) dan O2 dan set o ke O2. Pada saat yang sama Thread T2 mengunci O1 dan menunggu T1 untuk membukanya. Ketika menerima kunci O1, itu akan diatur ke O3. Dalam skenario ini, Antara T1 melepaskan O1 dan T2 mengunci O1, O1 menjadi tidak valid untuk mengunci melalui o. Pada saat ini thread lain dapat menggunakan o (= O2) untuk mengunci dan melanjutkan balapan tanpa gangguan dengan T2.
GPS
2

EDIT: Jadi solusi ini (seperti yang disarankan oleh Jon Skeet) mungkin memiliki masalah dengan atomicity implementasi "disinkronkan (objek) {}" sementara referensi objek berubah. Saya bertanya secara terpisah dan menurut Tuan erickson itu bukan utas aman - lihat: Apakah memasuki atom blok tersinkronisasi? . Jadi ambillah sebagai contoh bagaimana TIDAK melakukannya - dengan tautan mengapa;)

Lihat kode cara kerjanya jika synchronized () akan menjadi atom:

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

}
Vit Bernatik
sumber
1
Ini mungkin baik-baik saja - tetapi saya tidak tahu apakah ada jaminan dalam model memori bahwa nilai yang Anda sinkronkan adalah nilai yang paling baru ditulis - menurut saya tidak ada jaminan untuk "membaca dan menyinkronkan" secara menyeluruh. Secara pribadi saya mencoba menghindari sinkronisasi pada monitor yang memiliki kegunaan lain, untuk kesederhanaan. (Dengan memiliki bidang terpisah, kode menjadi jelas benar daripada harus bernalar dengan hati-hati.)
Jon Skeet
@On. Thx for answer! Saya mendengar kekhawatiran Anda. Saya setuju untuk kasus ini kunci eksternal akan menghindari pertanyaan tentang "atomisitas tersinkronisasi". Jadi akan lebih disukai. Meskipun mungkin ada kasus di mana Anda ingin memperkenalkan dalam runtime lebih banyak konfigurasi dan berbagi konfigurasi yang berbeda untuk grup utas yang berbeda (meskipun itu bukan kasus saya). Dan kemudian solusi ini mungkin menarik. Saya memposting pertanyaan stackoverflow.com/questions/29217266/… dari sinkronisasi () atomicity - jadi kita akan melihat apakah itu dapat digunakan (dan seseorang membalas)
Vit Bernatik
2

AtomicReference sesuai dengan kebutuhan Anda.

Dari dokumentasi java tentang paket atom :

Perangkat kelas kecil yang mendukung pemrograman thread-safe tanpa kunci pada variabel tunggal. Intinya, kelas dalam paket ini memperluas gagasan nilai volatil, bidang, dan elemen larik ke yang juga menyediakan operasi pembaruan bersyarat atom dalam bentuk:

boolean compareAndSet(expectedValue, updateValue);

Kode sampel:

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

Dalam contoh di atas, Anda mengganti Stringdengan milik Anda sendiriObject

Pertanyaan SE terkait:

Kapan menggunakan AtomicReference di Java?

Ravindra babu
sumber
1

Jika otidak pernah berubah selama masa pakai instance X, versi kedua adalah gaya yang lebih baik terlepas dari apakah sinkronisasi terlibat.

Sekarang, apakah ada yang salah dengan versi pertama tidak mungkin dijawab tanpa mengetahui apa lagi yang terjadi di kelas itu. Saya akan cenderung setuju dengan kompiler bahwa itu memang terlihat rawan kesalahan (saya tidak akan mengulangi apa yang dikatakan orang lain).

NPE
sumber
1

Hanya menambahkan dua sen saya: Saya mendapat peringatan ini ketika saya menggunakan komponen yang dipakai melalui desainer, jadi bidangnya tidak dapat benar-benar final, karena konstruktor tidak dapat mengambil parameter. Dengan kata lain, saya memiliki bidang kuasi-final tanpa kata kunci akhir.

Saya pikir itu sebabnya ini hanya peringatan: Anda mungkin melakukan sesuatu yang salah, tetapi mungkin juga benar.

peenut
sumber