Hindari disinkronkan (ini) di Jawa?

381

Setiap kali sebuah pertanyaan muncul di SO tentang sinkronisasi Java, beberapa orang sangat ingin menunjukkan bahwa synchronized(this)harus dihindari. Sebaliknya, mereka mengklaim, kunci pada referensi pribadi lebih disukai.

Beberapa alasan yang diberikan adalah:

Orang lain, termasuk saya, berpendapat bahwa synchronized(this)ini adalah ungkapan yang banyak digunakan (juga di perpustakaan Jawa), aman dan dipahami dengan baik. Seharusnya tidak dihindari karena Anda memiliki bug dan Anda tidak memiliki petunjuk tentang apa yang terjadi di program multithreaded Anda. Dengan kata lain: jika itu berlaku, maka gunakanlah.

Saya tertarik melihat beberapa contoh dunia nyata (tidak ada hal foobar) di mana menghindari kunci thislebih disukai ketika synchronized(this)juga akan melakukan pekerjaan.

Karena itu: haruskah Anda selalu menghindari synchronized(this)dan menggantinya dengan kunci pada referensi pribadi?


Beberapa info lebih lanjut (diperbarui saat jawaban diberikan):

  • kita berbicara tentang sinkronisasi contoh
  • baik implisit ( synchronizedmetode) dan bentuk eksplisit synchronized(this)dianggap
  • jika Anda mengutip Bloch atau otoritas lain tentang masalah ini, jangan tinggalkan bagian yang tidak Anda sukai (mis. Java Efektif, item tentang Keamanan Thread: Biasanya itu adalah kunci pada instance itu sendiri, tetapi ada pengecualian.)
  • jika Anda membutuhkan rincian dalam penguncian selain dari synchronized(this)penyediaan, maka synchronized(this)tidak berlaku sehingga tidak menjadi masalah
Eljenso
sumber
4
Saya juga ingin menunjukkan bahwa konteks itu penting - bit "Biasanya itu adalah kunci pada contoh itu sendiri" ada dalam bagian tentang mendokumentasikan kelas yang aman dengan syarat, ketika Anda membuat kunci publik. Dengan kata lain, kalimat itu berlaku ketika Anda sudah membuat keputusan ini.
Jon Skeet
Dengan tidak adanya sinkronisasi internal, dan ketika sinkronisasi eksternal diperlukan, kunci sering menjadi contoh itu sendiri, kata Bloch pada dasarnya. Jadi mengapa ini tidak berlaku untuk sinkronisasi internal dengan kunci 'ini' juga? (Pentingnya dokumentasi adalah masalah lain.)
eljenso
Ada tradeoff antara granularity yang diperluas dan cache CPU tambahan dan permintaan bus di atas kepala, karena penguncian pada objek eksternal kemungkinan besar akan membutuhkan jalur cache terpisah untuk dimodifikasi dan ditukar antara cache CPU (lih. MESIF dan MOESI).
ArtemGr
1
Saya pikir, di dunia pemrograman defensif, Anda mencegah bug bukan dengan idiom tetapi dengan kode. Ketika seseorang bertanya kepada saya, "Seberapa optimal sinkronisasi Anda?", Saya ingin mengatakan "Sangat", bukan "Sangat, kecuali orang lain tidak mengikuti idiom".
Sgene9

Jawaban:

132

Saya akan membahas setiap poin secara terpisah.

  1. Beberapa kode jahat dapat mencuri kunci Anda (sangat populer yang ini, juga memiliki varian "tidak sengaja")

    Saya lebih khawatir tentang tidak sengaja . Jumlahnya adalah bahwa penggunaan ini thisadalah bagian dari antarmuka kelas Anda yang terbuka, dan harus didokumentasikan. Terkadang kemampuan kode lain untuk menggunakan kunci Anda diinginkan. Ini berlaku untuk hal-hal seperti Collections.synchronizedMap(lihat javadoc).

  2. Semua metode yang disinkronkan dalam kelas yang sama menggunakan kunci yang sama persis, yang mengurangi throughput

    Ini adalah pemikiran yang terlalu sederhana; hanya menyingkirkan synchronized(this)tidak akan menyelesaikan masalah. Sinkronisasi yang tepat untuk throughput akan membutuhkan lebih banyak pemikiran.

  3. Anda (tidak perlu) mengekspos terlalu banyak informasi

    Ini adalah varian dari # 1. Penggunaan synchronized(this)adalah bagian dari antarmuka Anda. Jika Anda tidak ingin / butuh ini terbuka, jangan lakukan itu.

Darron
sumber
1. "disinkronkan" bukan bagian dari antarmuka terbuka kelas Anda. 2. setuju 3. lihat 1.
eljenso
66
Pada dasarnya disinkronkan (ini) adalah terkena karena itu berarti bahwa kode eksternal dapat mempengaruhi pengoperasian kelas Anda. Jadi saya menegaskan bahwa Anda harus mendokumentasikannya sebagai antarmuka, meskipun bahasanya tidak.
Darron
15
Serupa. Lihat Javadoc for Collections.synchronizedMap () - objek yang dikembalikan menggunakan disinkronkan (ini) secara internal dan mereka mengharapkan konsumen mengambil keuntungan dari itu untuk menggunakan kunci yang sama untuk operasi atom skala besar seperti iterasi.
Darron
3
Bahkan Collections.synchronizedMap () TIDAK menggunakan sinkronisasi (ini) secara internal, ia menggunakan objek kunci akhir pribadi.
Bas Leijdekkers
1
@Bas Leijdekkers: dokumentasi dengan jelas menetapkan bahwa sinkronisasi terjadi pada instance peta yang dikembalikan. Yang menarik, adalah, bahwa pandangan dikembalikan oleh keySet()dan values()tidak mengunci (mereka) this, tetapi contoh peta, yang penting untuk mendapatkan perilaku yang konsisten untuk semua operasi peta. Alasannya, objek kunci difaktorkan ke dalam variabel, adalah, bahwa subclass SynchronizedSortedMapmembutuhkannya untuk mengimplementasikan sub-peta yang mengunci pada instance peta asli.
Holger
86

Yah, pertama-tama harus ditunjukkan bahwa:

public void blah() {
  synchronized (this) {
    // do stuff
  }
}

secara semantik setara dengan:

public synchronized void blah() {
  // do stuff
}

yang merupakan salah satu alasan untuk tidak menggunakan synchronized(this). Anda mungkin berpendapat bahwa Anda dapat melakukan hal-hal di sekitar synchronized(this)blok. Alasan yang biasa adalah untuk mencoba dan menghindari harus melakukan pemeriksaan yang disinkronkan sama sekali, yang mengarah ke semua jenis masalah konkurensi, khususnya masalah penguncian dua kali lipat , yang hanya menunjukkan betapa sulitnya membuat pemeriksaan yang relatif sederhana threadsafe.

Kunci pribadi adalah mekanisme pertahanan, yang tidak pernah merupakan ide yang buruk.

Juga, seperti yang Anda singgung, kunci pribadi dapat mengontrol granularity. Satu set operasi pada suatu objek mungkin sama sekali tidak terkait dengan yang lain tetapi synchronized(this)akan saling mengecualikan akses ke semuanya.

synchronized(this) hanya benar-benar tidak memberi Anda apa-apa.

cletus
sumber
4
"Disinkronkan (ini) hanya benar-benar tidak memberi Anda apa-apa." Oke, saya menggantinya dengan sinkronisasi (myPrivateFinalLock). Apa yang memberi saya? Anda membicarakannya sebagai mekanisme pertahanan. Apa yang saya lindungi?
eljenso
14
Anda terlindungi dari penguncian 'ini' yang tidak disengaja (atau berbahaya) oleh objek-objek eksternal.
cletus
14
Saya sama sekali tidak setuju dengan jawaban ini: kunci harus selalu diadakan untuk waktu sesingkat mungkin, dan itulah alasan mengapa Anda ingin "melakukan hal-hal" di sekitar blok yang disinkronkan daripada menyinkronkan seluruh metode .
Olivier
5
Melakukan hal-hal di luar blok yang disinkronkan selalu dengan niat baik. Intinya adalah bahwa orang sering salah dalam hal ini dan bahkan tidak menyadarinya, seperti dalam masalah penguncian ganda. Jalan menuju neraka ditaburi dengan niat baik.
cletus
16
Saya tidak setuju secara umum dengan "X adalah mekanisme defensif, yang tidak pernah merupakan ide yang buruk." Ada banyak kode kembung yang tidak perlu di luar sana karena sikap ini.
finnw
54

Saat Anda menggunakan sinkronisasi (ini), Anda menggunakan instance kelas sebagai kunci itu sendiri. Ini berarti bahwa sementara kunci diperoleh oleh utas 1 , utas 2 harus menunggu.

Misalkan kode berikut:

public void method1() {
    // do something ...
    synchronized(this) {
        a ++;      
    }
    // ................
}


public void method2() {
    // do something ...
    synchronized(this) {
        b ++;      
    }
    // ................
}

Metode 1 memodifikasi variabel a dan metode 2 memodifikasi variabel b , modifikasi bersamaan dari variabel yang sama dengan dua utas harus dihindari dan itu. TETAPI sementara thread1 memodifikasi a dan memodifikasi thread2 b itu dapat dilakukan tanpa kondisi balapan.

Sayangnya, kode di atas tidak akan mengizinkan ini karena kami menggunakan referensi yang sama untuk kunci; Ini berarti bahwa utas bahkan jika mereka tidak dalam kondisi balapan harus menunggu dan jelas kode mengorbankan konkurensi program.

Solusinya adalah menggunakan 2 kunci berbeda untuk dua variabel berbeda:

public class Test {

    private Object lockA = new Object();
    private Object lockB = new Object();

    public void method1() {
        // do something ...
        synchronized(lockA) {
            a ++;      
        }
        // ................
    }


    public void method2() {
        // do something ...
        synchronized(lockB) {
            b ++;      
        }
        // ................
    }

}

Contoh di atas menggunakan lebih banyak kunci berbutir halus (2 kunci, bukan satu ( lockA dan lockB masing-masing untuk variabel a dan b ) dan sebagai hasilnya memungkinkan konkurensi yang lebih baik, di sisi lain itu menjadi lebih kompleks daripada contoh pertama ...

Andreas Bakurov
sumber
Ini sangat berbahaya. Anda sekarang telah memperkenalkan persyaratan pemesanan kunci sisi klien (pengguna kelas ini). Jika dua utas memanggil method1 () dan method2 () dalam urutan yang berbeda, mereka cenderung menemui jalan buntu, tetapi pengguna kelas ini tidak tahu bahwa ini adalah masalahnya.
daveb
7
Granularity tidak disediakan oleh "disinkronkan (ini)" di luar ruang lingkup pertanyaan saya. Dan bukankah bidang kunci Anda harus final?
eljenso
10
untuk memiliki kebuntuan kita harus melakukan panggilan dari blok disinkronisasi oleh A ke blok disinkronisasi oleh B. daveb, Anda salah ...
Andreas Bakurov
3
Sejauh ini tidak ada jalan buntu dalam contoh ini. Saya menerimanya hanya pseudocode tapi saya akan menggunakan salah satu implementasi dari java.util.concurrent.locks.Lock seperti java.util.concurrent.locks.ReentrantLock
Shawn Vader
15

Sementara saya setuju untuk tidak mengikuti aturan dogmatis secara membabi buta, apakah skenario "pencurian kunci" tampak begitu eksentrik bagi Anda? Utas memang dapat memperoleh kunci pada objek Anda "eksternal" ( synchronized(theObject) {...}), memblokir utas lainnya menunggu metode instance yang disinkronkan.

Jika Anda tidak percaya pada kode berbahaya, pertimbangkan bahwa kode ini dapat berasal dari pihak ketiga (misalnya jika Anda mengembangkan semacam server aplikasi).

Versi "tidak disengaja" tampaknya kurang mungkin, tetapi seperti yang mereka katakan, "membuat sesuatu yang idiot-proof dan seseorang akan menciptakan idiot yang lebih baik".

Jadi saya setuju dengan itu-tergantung-pada-apa-yang-kelas-sekolah pemikiran.


Edit 3 komentar pertama eljenso berikut:

Saya tidak pernah mengalami masalah pencurian kunci tetapi ini adalah skenario imajiner:

Katakanlah sistem Anda adalah wadah servlet, dan objek yang kami pertimbangkan adalah ServletContextimplementasinya. Its getAttributemetode harus benang-aman, sebagai atribut konteks adalah data bersama; jadi Anda menyatakannya sebagai synchronized. Mari kita bayangkan juga Anda menyediakan layanan hosting publik berdasarkan pada implementasi wadah Anda.

Saya adalah pelanggan Anda dan menyebarkan servlet "baik" saya di situs Anda. Kebetulan kode saya berisi panggilan ke getAttribute.

Seorang hacker, yang menyamar sebagai pelanggan lain, menyebarkan servlet jahatnya di situs Anda. Ini berisi kode berikut dalam initmetode:

disinkronkan (this.getServletConfig (). getServletContext ()) {
   while (true) {}
}

Dengan asumsi kita berbagi konteks servlet yang sama (diizinkan oleh spesifikasi selama dua servlet berada di host virtual yang sama), panggilan saya aktif getAttributeterkunci selamanya. Peretas telah mencapai DoS pada servlet saya.

Serangan ini tidak mungkin jika getAttributedisinkronkan pada kunci pribadi, karena kode pihak ketiga tidak dapat memperoleh kunci ini.

Saya akui bahwa contohnya dibuat dan pandangan yang terlalu sederhana tentang bagaimana wadah servlet bekerja, tetapi IMHO itu membuktikan intinya.

Jadi saya akan membuat pilihan desain berdasarkan pertimbangan keamanan: akankah saya memiliki kendali penuh atas kode yang memiliki akses ke instance? Apa yang akan menjadi konsekuensi dari utas yang memegang kunci pada instance tanpa batas?

Olivier
sumber
it-depend-on-what-the-class-do: jika ini merupakan objek 'penting', lalu kunci pada referensi pribadi? Kalau tidak, penguncian contoh akan cukup?
eljenso
6
Ya, skenario pencurian kunci tampaknya jauh bagi saya. Semua orang menyebutkannya, tetapi siapa yang benar-benar melakukannya atau mengalaminya? Jika Anda "secara tidak sengaja" mengunci objek yang tidak seharusnya Anda miliki, maka ada nama untuk jenis situasi ini: itu adalah bug. Memperbaikinya.
eljenso
4
Selain itu, mengunci referensi internal tidak bebas dari "serangan sinkronisasi eksternal": jika Anda tahu bahwa bagian kode yang disinkronkan menunggu peristiwa eksternal terjadi (misalnya, penulisan file, nilai dalam DB, peristiwa waktu) Anda mungkin dapat mengatur untuk memblokir juga.
eljenso
Biarkan saya mengaku bahwa saya salah satu dari orang-orang idiot itu, al beit saya melakukannya ketika saya masih muda. Saya pikir kode itu lebih bersih dengan tidak membuat objek kunci eksplisit dan, sebagai gantinya, menggunakan objek final pribadi lain yang diperlukan untuk berpartisipasi dalam monitor. Saya tidak tahu bahwa objek itu sendiri melakukan sinkronisasi pada dirinya sendiri. Anda dapat membayangkan hijinx berikutnya ...
Alan Cabrera
12

Itu tergantung situasi.
Jika Hanya ada satu entitas berbagi atau lebih dari satu.

Lihat contoh kerja lengkap di sini

Pengantar kecil.

Utas dan entitas yang dapat dibagikan
Dimungkinkan untuk beberapa utas untuk mengakses entitas yang sama, misalnya untuk beberapa koneksi. Benang berbagi satu pesan tunggal. Karena utas berjalan secara bersamaan mungkin ada kemungkinan mengesampingkan data seseorang dengan yang lain yang mungkin menjadi situasi kacau.
Jadi kita perlu beberapa cara untuk memastikan bahwa entitas yang dapat dibagikan diakses hanya oleh satu utas pada satu waktu. (CONCURRENCY).

Blok
disinkronkan () blok disinkronkan adalah cara untuk memastikan akses bersamaan dari entitas yang dapat dibagikan.
Pertama, analogi kecil
Misalkan Ada dua orang P1, P2 (utas) sebuah Baskom (entitas yang dapat dibagikan) di dalam kamar mandi dan ada pintu (kunci).
Sekarang kami ingin satu orang menggunakan wastafel sekaligus.
Suatu pendekatan adalah dengan mengunci pintu oleh P1 ketika pintu dikunci P2 menunggu sampai p1 menyelesaikan pekerjaannya
P1 membuka kunci pintu
maka hanya p1 yang dapat menggunakan wastafel.

sintaksis.

synchronized(this)
{
  SHARED_ENTITY.....
}

"this" menyediakan kunci intrinsik yang terkait dengan kelas (pengembang Java mendesain kelas Object sedemikian rupa sehingga setiap objek dapat berfungsi sebagai monitor). Pendekatan di atas berfungsi dengan baik ketika hanya ada satu entitas bersama dan beberapa utas (1: N). N entitas yang dapat dibagikan - M thread Sekarang pikirkan situasi ketika ada dua wastafel di dalam kamar mandi dan hanya satu pintu. Jika kita menggunakan pendekatan sebelumnya, hanya p1 yang dapat menggunakan satu wastafel sekaligus, sementara p2 akan menunggu di luar. Ini pemborosan sumber daya karena tidak ada yang menggunakan B2 (wastafel). Pendekatan yang lebih bijaksana adalah menciptakan ruang yang lebih kecil di dalam kamar kecil dan menyediakan satu pintu untuk setiap wastafel. Dengan cara ini, P1 dapat mengakses B1 dan P2 dapat mengakses B2 dan sebaliknya.
masukkan deskripsi gambar di sini

washbasin1;  
washbasin2;

Object lock1=new Object();
Object lock2=new Object();

  synchronized(lock1)
  {
    washbasin1;
  }

  synchronized(lock2)
  {
    washbasin2;
  }

masukkan deskripsi gambar di sini
masukkan deskripsi gambar di sini

Lihat lebih lanjut tentang Utas ----> di sini

Rohit Singh
sumber
11

Tampaknya ada konsensus yang berbeda di kamp-kamp C # dan Java mengenai hal ini. Mayoritas kode Java yang saya lihat menggunakan:

// apply mutex to this instance
synchronized(this) {
    // do work here
}

sedangkan mayoritas kode C # memilih yang lebih aman:

// instance level lock object
private readonly object _syncObj = new object();

...

// apply mutex to private instance level field (a System.Object usually)
lock(_syncObj)
{
    // do work here
}

Idi C # tentu saja lebih aman. Seperti disebutkan sebelumnya, tidak ada akses jahat / tidak sengaja ke kunci dapat dilakukan dari luar mesin virtual. Kode Java juga memiliki risiko ini, tetapi tampaknya komunitas Java cenderung mengikuti versi yang sedikit kurang aman, tetapi sedikit lebih singkat.

Itu tidak dimaksudkan sebagai penggalian terhadap Jawa, hanya cerminan pengalaman saya bekerja pada kedua bahasa.

serg10
sumber
3
Mungkin karena C # adalah bahasa yang lebih muda, mereka belajar dari pola-pola buruk yang ditemukan di kamp Jawa dan kode hal-hal seperti ini lebih baik? Apakah jumlah lajang juga lebih sedikit? :)
Bill K
3
Hehe. Sangat mungkin benar, tapi aku tidak akan naik ke umpan! Satu berpikir saya dapat mengatakan dengan pasti bahwa ada huruf kapital lebih banyak di C # kode;)
serg10
1
Hanya tidak benar (dengan kata lain)
tcurdt
7

The java.util.concurrentpaket telah jauh berkurang kompleksitas benang kode saya aman. Saya hanya memiliki bukti anekdotal untuk melanjutkan, tetapi sebagian besar pekerjaan yang saya lihat synchronized(x)tampaknya menerapkan kembali Kunci, Semaphore, atau Latch, tetapi menggunakan monitor tingkat bawah.

Dengan mengingat hal ini, sinkronisasi menggunakan salah satu mekanisme ini analog dengan sinkronisasi pada objek internal, daripada membocorkan kunci. Ini bermanfaat karena Anda memiliki kepastian mutlak bahwa Anda mengontrol entri ke monitor dengan dua utas atau lebih.

jamesh
sumber
6
  1. Jadikan data Anda tidak berubah jika memungkinkan ( finalvariabel)
  2. Jika Anda tidak dapat menghindari mutasi data bersama di banyak utas, gunakan konstruksi pemrograman tingkat tinggi [mis. LockAPI granular ]

Kunci menyediakan akses eksklusif ke sumber daya bersama: hanya satu utas pada satu waktu yang dapat memperoleh kunci dan semua akses ke sumber daya bersama mengharuskan kunci diperoleh terlebih dahulu.

Kode sampel untuk digunakan ReentrantLockyang mengimplementasikan Lockantarmuka

 class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() {
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

Keuntungan dari Lock over Synchronized (ini)

  1. Penggunaan metode atau pernyataan yang disinkronkan memaksa semua akuisisi dan pelepasan kunci terjadi dengan cara terstruktur-blok.

  2. Implementasi kunci menyediakan fungsionalitas tambahan atas penggunaan metode dan pernyataan yang disinkronkan dengan menyediakan

    1. Upaya non-pemblokiran untuk mendapatkan kunci ( tryLock())
    2. Upaya mendapatkan kunci yang dapat diinterupsi ( lockInterruptibly())
    3. Upaya mendapatkan kunci yang dapat habis waktu ( tryLock(long, TimeUnit)).
  3. Kelas Lock juga dapat memberikan perilaku dan semantik yang sangat berbeda dari kunci monitor implisit, seperti

    1. pemesanan terjamin
    2. penggunaan non-peserta
    3. Deteksi kebuntuan

Lihatlah pertanyaan SE ini mengenai berbagai jenis Locks:

Sinkronisasi vs Kunci

Anda dapat mencapai keamanan utas dengan menggunakan API konkurensi canggih alih-alih blok yang disinkronkan. Halaman dokumentasi ini menyediakan konstruksi pemrograman yang baik untuk mencapai keamanan utas.

Lock Objects mendukung idiom pengunci yang menyederhanakan banyak aplikasi bersamaan.

Pelaksana menetapkan API tingkat tinggi untuk meluncurkan dan mengelola utas. Implementasi pelaksana yang disediakan oleh java.util.concurrent menyediakan manajemen kumpulan thread yang cocok untuk aplikasi skala besar.

Koleksi Bersamaan membuatnya lebih mudah untuk mengelola koleksi data yang besar, dan dapat sangat mengurangi kebutuhan untuk sinkronisasi.

Variabel Atom memiliki fitur yang meminimalkan sinkronisasi dan membantu menghindari kesalahan konsistensi memori.

ThreadLocalRandom (dalam JDK 7) menyediakan generasi nomor pseudorandom yang efisien dari berbagai utas.

Mengacu pada java.util.concurrent dan java.util.concurrent.atomic paket juga untuk konstruksi pemrograman lainnya.

Ravindra babu
sumber
5

Jika Anda telah memutuskan bahwa:

  • hal yang perlu Anda lakukan adalah mengunci objek saat ini; dan
  • Anda ingin menguncinya dengan rincian lebih kecil dari keseluruhan metode;

maka saya tidak melihat tabu atas sinkronisasi (ini).

Beberapa orang sengaja menggunakan sinkronisasi (ini) (alih-alih menandai metode yang disinkronkan) di dalam seluruh isi metode karena mereka pikir itu "lebih jelas bagi pembaca" di mana objek sebenarnya disinkronkan. Selama orang-orang membuat pilihan berdasarkan informasi (mis. Pahami bahwa dengan melakukan hal itu mereka benar-benar memasukkan bytecodes tambahan ke dalam metode dan ini bisa berdampak pada optimisasi potensial), saya tidak terlalu melihat masalah dengan ini . Anda harus selalu mendokumentasikan perilaku bersamaan dari program Anda, jadi saya tidak melihat argumen "'disinkronkan' menerbitkan perilaku" sebagai hal yang begitu meyakinkan.

Mengenai pertanyaan tentang kunci objek mana yang harus Anda gunakan, saya pikir tidak ada yang salah dengan menyinkronkan objek saat ini jika ini akan diharapkan oleh logika dari apa yang Anda lakukan dan bagaimana kelas Anda biasanya akan digunakan . Misalnya, dengan koleksi, objek yang secara logis Anda harapkan akan dikunci adalah koleksi itu sendiri.

Neil Coffey
sumber
1
"Jika ini diharapkan oleh logika ..." adalah hal yang saya coba sampaikan juga. Saya tidak melihat gunanya selalu menggunakan kunci pribadi, meskipun konsensus umum tampaknya lebih baik, karena tidak sakit dan lebih defensif.
eljenso
4

Saya pikir ada penjelasan yang baik tentang mengapa masing-masing teknik penting di bawah ikat pinggang Anda dalam sebuah buku berjudul Java Concurrency In Practice oleh Brian Goetz. Dia membuat satu titik sangat jelas - Anda harus menggunakan kunci yang sama "DI MANA SAJA" untuk melindungi keadaan objek Anda. Metode yang disinkronkan dan sinkronisasi pada suatu objek sering berjalan seiring. Misalnya Vektor menyinkronkan semua metodenya. Jika Anda memiliki pegangan untuk objek vektor dan akan melakukan "menempatkan jika tidak ada" maka hanya menyinkronkan vektor metode sendiri tidak akan melindungi Anda dari korupsi negara. Anda perlu melakukan sinkronisasi menggunakan sinkronisasi (vectorHandle). Ini akan menghasilkan kunci SAMA yang diperoleh oleh setiap utas yang memiliki pegangan pada vektor dan akan melindungi keseluruhan keadaan vektor. Ini disebut penguncian sisi klien. Kita tahu bahwa sebenarnya vektor tidak disinkronkan (ini) / menyinkronkan semua metode dan karenanya sinkronisasi pada objek vectorHandle akan menghasilkan sinkronisasi yang tepat dari keadaan objek vektor. Adalah bodoh untuk percaya bahwa Anda aman karena hanya menggunakan koleksi aman. Inilah tepatnya alasan ConcurrentHashMap secara eksplisit memperkenalkan metode putIfAbsent - untuk membuat operasi seperti itu menjadi atom.

Singkatnya

  1. Sinkronisasi pada level metode memungkinkan penguncian sisi klien.
  2. Jika Anda memiliki objek kunci pribadi - itu membuat penguncian sisi klien tidak mungkin. Ini bagus jika Anda tahu bahwa kelas Anda tidak memiliki tipe fungsi "put if absent".
  3. Jika Anda mendesain pustaka - maka menyinkronkan ini atau menyinkronkan metode ini seringkali lebih bijaksana. Karena Anda jarang berada dalam posisi untuk memutuskan bagaimana kelas Anda akan digunakan.
  4. Seandainya Vector menggunakan objek kunci pribadi - tidak mungkin mendapatkan "put if absent" dengan benar. Kode klien tidak akan pernah mendapatkan pegangan ke kunci pribadi sehingga melanggar aturan dasar menggunakan EXACT SAMA KUNCI untuk melindungi negaranya.
  5. Menyinkronkan ini atau metode yang disinkronkan memang memiliki masalah seperti yang ditunjukkan orang lain - seseorang bisa mendapatkan kunci dan tidak pernah melepaskannya. Semua utas lainnya akan terus menunggu kunci dilepaskan.
  6. Jadi ketahuilah apa yang Anda lakukan dan adopsi yang benar.
  7. Seseorang berpendapat bahwa memiliki objek kunci pribadi memberi Anda granularitas yang lebih baik - misalnya jika dua operasi tidak terkait - mereka dapat dijaga oleh kunci yang berbeda sehingga menghasilkan throughput yang lebih baik. Tapi ini saya pikir adalah bau desain dan bukan bau kode - jika dua operasi benar-benar tidak berhubungan mengapa mereka bagian dari kelas SAMA? Mengapa klub kelas memiliki fungsi yang tidak berhubungan sama sekali? Mungkin kelas utilitas? Hmmmm - beberapa util menyediakan manipulasi string dan format tanggal kalender melalui instance yang sama ?? ... setidaknya tidak masuk akal bagiku !!
Sandesh Sadhale
sumber
3

Tidak, kamu seharusnya tidak selalu . Namun, saya cenderung menghindarinya ketika ada beberapa kekhawatiran pada objek tertentu yang hanya perlu di-threadsafe untuk diri mereka sendiri. Misalnya, Anda mungkin memiliki objek data yang dapat diubah yang memiliki bidang "label" dan "induk"; ini harus threadsafe, tetapi mengubah yang satu tidak perlu menghalangi yang lain untuk ditulis / dibaca. (Dalam praktiknya saya akan menghindari ini dengan mendeklarasikan bidang volatil dan / atau menggunakan pembungkus AtomicFoo java.util.concurrent).

Sinkronisasi pada umumnya agak canggung, karena menampar kunci besar ke bawah daripada memikirkan dengan tepat bagaimana thread dapat dibiarkan saling bekerja sama. Menggunakannya synchronized(this)bahkan lebih klumsier dan anti-sosial, karena mengatakan "tidak ada yang dapat mengubah apa pun di kelas ini sementara saya memegang kunci". Seberapa sering Anda benar-benar perlu melakukan itu?

Saya lebih suka memiliki kunci granular; bahkan jika Anda ingin menghentikan segala sesuatu dari perubahan (mungkin Anda membuat serial objek), Anda dapat memperoleh semua kunci untuk mencapai hal yang sama, ditambah lebih eksplisit seperti itu. Saat Anda menggunakan synchronized(this), tidak jelas mengapa Anda melakukan sinkronisasi, atau apa efek sampingnya. Jika Anda menggunakan synchronized(labelMonitor), atau bahkan lebih baik labelLock.getWriteLock().lock(), jelas apa yang Anda lakukan dan apa efek dari bagian kritis Anda terbatas.

Andrzej Doyle
sumber
3

Jawaban singkat : Anda harus memahami perbedaannya dan membuat pilihan tergantung pada kodenya.

Jawaban panjang : Secara umum saya lebih suka menghindari sinkronisasi (ini) untuk mengurangi pertikaian tetapi kunci pribadi menambah kerumitan yang harus Anda perhatikan. Jadi gunakan sinkronisasi yang tepat untuk pekerjaan yang tepat. Jika Anda tidak begitu berpengalaman dengan pemrograman multi-threaded saya lebih suka tetap mengunci misalnya dan membaca tentang topik ini. (Yang mengatakan: hanya menggunakan sinkronisasi (ini) tidak secara otomatis membuat kelas Anda sepenuhnya thread-safe.) Ini bukan topik yang mudah tetapi setelah Anda terbiasa, jawaban apakah akan menggunakan sinkronisasi (ini) atau tidak datang secara alami .

tcurdt
sumber
Apakah saya mengerti Anda dengan benar ketika Anda mengatakan itu tergantung pada pengalaman Anda?
eljenso
Di tempat pertama itu tergantung pada kode yang ingin Anda tulis. Hanya mengatakan bahwa Anda mungkin perlu sedikit lebih banyak pengalaman ketika Anda mengalihkan untuk tidak menggunakan sinkronisasi (ini).
tcurdt
2

Kunci digunakan untuk visibilitas atau untuk melindungi beberapa data dari modifikasi bersamaan yang dapat menyebabkan balapan.

Ketika Anda hanya perlu membuat operasi tipe primitif menjadi atom ada pilihan yang tersedia seperti AtomicIntegerdan suka.

Tetapi misalkan Anda memiliki dua bilangan bulat yang saling terkait seperti xdan ykoordinat, yang saling terkait dan harus diubah secara atom. Maka Anda akan melindungi mereka menggunakan kunci yang sama.

Kunci seharusnya hanya melindungi keadaan yang terkait satu sama lain. Tidak kurang dan tidak lebih. Jika Anda menggunakan synchronized(this)dalam setiap metode maka bahkan jika keadaan kelas tidak terkait semua utas akan menghadapi pertentangan bahkan jika memperbarui keadaan tidak terkait.

class Point{
   private int x;
   private int y;

   public Point(int x, int y){
       this.x = x;
       this.y = y;
   }

   //mutating methods should be guarded by same lock
   public synchronized void changeCoordinates(int x, int y){
       this.x = x;
       this.y = y;
   }
}

Dalam contoh di atas saya hanya memiliki satu metode yang bermutasi baik xdan ybukan dua metode yang berbeda xdan yterkait dan jika saya telah memberikan dua metode yang berbeda untuk bermutasi xdan ysecara terpisah maka itu tidak akan aman thread.

Contoh ini hanya untuk menunjukkan dan tidak harus cara penerapannya. Cara terbaik untuk melakukannya adalah membuatnya IMMUTABLE .

Sekarang berlawanan dengan Pointcontoh, ada contoh TwoCounterssudah disediakan oleh @ Andreas di mana negara yang dilindungi oleh dua kunci yang berbeda karena negara tidak terkait satu sama lain.

Proses menggunakan kunci yang berbeda untuk melindungi keadaan yang tidak terkait disebut Lock Striping atau Lock Splitting

Narendra Pathai
sumber
1

Alasan untuk tidak menyinkronkan ini adalah bahwa kadang-kadang Anda membutuhkan lebih dari satu kunci (kunci kedua sering dihapus setelah beberapa pemikiran tambahan, tetapi Anda masih membutuhkannya dalam kondisi peralihan). Jika Anda mengunci ini , Anda harus selalu ingat yang mana dari dua kunci ini ; Jika Anda mengunci objek pribadi, nama variabel memberitahu Anda itu.

Dari sudut pandang pembaca, jika Anda melihat mengunci ini , Anda selalu harus menjawab dua pertanyaan:

  1. akses apa yang dilindungi oleh ini ?
  2. apakah satu kunci benar-benar cukup, bukankah seseorang memperkenalkan bug?

Sebuah contoh:

class BadObject {
    private Something mStuff;
    synchronized setStuff(Something stuff) {
        mStuff = stuff;
    }
    synchronized getStuff(Something stuff) {
        return mStuff;
    }
    private MyListener myListener = new MyListener() {
        public void onMyEvent(...) {
            setStuff(...);
        }
    }
    synchronized void longOperation(MyListener l) {
        ...
        l.onMyEvent(...);
        ...
    }
}

Jika dua utas dimulai longOperation()pada dua contoh berbeda BadObject, mereka mendapatkan kunci mereka; ketika saatnya untuk memohon l.onMyEvent(...), kita memiliki jalan buntu karena tidak ada utas yang dapat memperoleh kunci objek lain.

Dalam contoh ini kita dapat menghilangkan kebuntuan dengan menggunakan dua kunci, satu untuk operasi pendek dan satu untuk yang lama.

18446744073709551615
sumber
2
Satu-satunya cara untuk mendapatkan jalan buntu dalam contoh ini adalah ketika BadObjectA memanggil longOperationB, melewati A myListener, dan sebaliknya. Bukan tidak mungkin, tetapi cukup berbelit-belit, mendukung poin saya sebelumnya.
eljenso
1

Seperti yang sudah dikatakan di sini, blok yang disinkronkan dapat menggunakan variabel yang ditentukan pengguna sebagai objek kunci, ketika fungsi yang disinkronkan hanya menggunakan "ini". Dan tentu saja Anda dapat memanipulasi dengan area fungsi Anda yang harus disinkronkan dan sebagainya.

Tetapi semua orang mengatakan bahwa tidak ada perbedaan antara fungsi yang disinkronkan dan blok yang mencakup seluruh fungsi menggunakan "ini" sebagai objek kunci. Itu tidak benar, perbedaannya adalah dalam kode byte yang akan dihasilkan dalam kedua situasi. Dalam hal penggunaan blok tersinkronisasi harus dialokasikan variabel lokal yang memegang referensi untuk "ini". Dan sebagai hasilnya kita akan memiliki ukuran fungsi yang sedikit lebih besar (tidak relevan jika Anda hanya memiliki sedikit fungsi).

Penjelasan lebih rinci tentang perbedaan ini dapat Anda temukan di sini: http://www.artima.com/insidejvm/ed2/threadsynchP.html

Penggunaan blok tersinkronisasi juga tidak baik karena sudut pandang berikut:

Kata kunci yang disinkronkan sangat terbatas di satu area: ketika keluar dari blok yang disinkronkan, semua utas yang menunggu kunci itu harus diblokir, tetapi hanya satu utas yang dapat mengambil kunci; semua yang lain melihat bahwa kunci diambil dan kembali ke keadaan diblokir. Itu bukan hanya banyak siklus pemrosesan yang terbuang: sering kali konteks beralih untuk membuka blokir utas juga melibatkan paging memori dari disk, dan itu sangat, sangat, mahal.

Untuk detail lebih lanjut dalam bidang ini, saya sarankan Anda membaca artikel ini: http://java.dzone.com/articles/synchronized-considered

Roma
sumber
1

Ini benar-benar hanya tambahan untuk jawaban yang lain, tetapi jika keberatan utama Anda untuk menggunakan objek pribadi untuk mengunci adalah bahwa hal itu mengacaukan kelas Anda dengan bidang yang tidak terkait dengan logika bisnis, maka Proyek Lombok harus @Synchronizedmembuat boilerplate pada waktu kompilasi:

@Synchronized
public int foo() {
    return 0;
}

kompilasi ke

private final Object $lock = new Object[0];

public int foo() {
    synchronized($lock) {
        return 0;
    }
}
Michael
sumber
0

Contoh yang baik untuk penggunaan yang disinkronkan (ini).

// add listener
public final synchronized void addListener(IListener l) {listeners.add(l);}
// remove listener
public final synchronized void removeListener(IListener l) {listeners.remove(l);}
// routine that raise events
public void run() {
   // some code here...
   Set ls;
   synchronized(this) {
      ls = listeners.clone();
   }
   for (IListener l : ls) { l.processEvent(event); }
   // some code here...
}

Seperti yang dapat Anda lihat di sini, kami menggunakan sinkronisasi pada ini untuk memudahkan bekerja sama secara panjang (mungkin loop tak terbatas dari metode yang dijalankan) dengan beberapa metode yang disinkronkan di sana.

Tentu saja dapat dengan mudah ditulis ulang dengan menggunakan sinkronisasi pada bidang pribadi. Tetapi kadang-kadang, ketika kita sudah memiliki beberapa desain dengan metode disinkronkan (yaitu kelas warisan, kita berasal dari, disinkronkan (ini) dapat menjadi satu-satunya solusi).

Bart Prokop
sumber
Objek apa pun dapat digunakan sebagai kunci di sini. Tidak perlu begitu this. Itu bisa menjadi bidang pribadi.
finnw
Benar, tetapi tujuan dari contoh ini adalah untuk menunjukkan bagaimana sinkronisasi yang tepat, jika kami memutuskan untuk menggunakan sinkronisasi metode.
Bart Prokop
0

Tergantung pada tugas yang ingin Anda lakukan, tetapi saya tidak akan menggunakannya. Juga, periksa apakah thread-save-ness yang ingin Anda temani tidak dapat dilakukan dengan menyinkronkan (ini) sejak awal? Ada juga beberapa kunci bagus di API yang mungkin membantu Anda :)

Harald Schilly
sumber
0

Saya hanya ingin menyebutkan solusi yang mungkin untuk referensi pribadi yang unik di bagian kode atom tanpa ketergantungan. Anda dapat menggunakan Hashmap statis dengan kunci dan metode statis sederhana bernama atomic () yang membuat referensi yang diperlukan secara otomatis menggunakan informasi tumpukan (nama kelas lengkap dan nomor baris). Kemudian Anda dapat menggunakan metode ini dalam menyinkronkan pernyataan tanpa menulis objek kunci baru.

// Synchronization objects (locks)
private static HashMap<String, Object> locks = new HashMap<String, Object>();
// Simple method
private static Object atomic() {
    StackTraceElement [] stack = Thread.currentThread().getStackTrace(); // get execution point 
    StackTraceElement exepoint = stack[2];
    // creates unique key from class name and line number using execution point
    String key = String.format("%s#%d", exepoint.getClassName(), exepoint.getLineNumber()); 
    Object lock = locks.get(key); // use old or create new lock
    if (lock == null) {
        lock = new Object();
        locks.put(key, lock);
    }
    return lock; // return reference to lock
}
// Synchronized code
void dosomething1() {
    // start commands
    synchronized (atomic()) {
        // atomic commands 1
        ...
    }
    // other command
}
// Synchronized code
void dosomething2() {
    // start commands
    synchronized (atomic()) {
        // atomic commands 2
        ...
    }
    // other command
}
Kolarčík Václav
sumber
0

Hindari penggunaan synchronized(this)sebagai mekanisme penguncian: Ini mengunci seluruh instance kelas dan dapat menyebabkan deadlock. Dalam kasus seperti itu, refactor kode untuk mengunci hanya metode atau variabel tertentu, sehingga seluruh kelas tidak terkunci. Synchroniseddapat digunakan di dalam level metode.
Alih-alih menggunakan synchronized(this), kode di bawah ini menunjukkan bagaimana Anda bisa mengunci metode.

   public void foo() {
if(operation = null) {
    synchronized(foo) { 
if (operation == null) {
 // enter your code that this method has to handle...
          }
        }
      }
    }
surendrapanday
sumber
0

Dua sen saya pada 2019 meskipun pertanyaan ini sudah bisa diselesaikan.

Mengunci 'ini' tidak buruk jika Anda tahu apa yang Anda lakukan tetapi di belakang layar mengunci 'ini' adalah (yang sayangnya kata kunci yang disinkronkan dalam definisi metode memungkinkan).

Jika Anda benar-benar ingin pengguna kelas Anda dapat 'mencuri' kunci Anda (mis. Mencegah utas lain untuk menanganinya), Anda sebenarnya ingin semua metode yang disinkronkan menunggu saat metode sinkronisasi lain berjalan dan seterusnya. Itu harus disengaja dan dipikirkan dengan baik (dan karenanya didokumentasikan untuk membantu pengguna Anda memahaminya).

Untuk lebih rumit, sebaliknya Anda harus tahu apa yang Anda 'dapatkan' (atau 'hilang') jika Anda mengunci kunci yang tidak dapat diakses (tidak ada yang bisa 'mencuri' kunci Anda, Anda berada dalam kontrol penuh dan sebagainya. ..)

Masalahnya bagi saya adalah bahwa kata kunci yang disinkronkan dalam metode definisi tanda tangan membuatnya terlalu mudah bagi programmer untuk tidak memikirkan apa yang harus dikunci yang merupakan hal penting yang hebat untuk dipikirkan jika Anda tidak ingin mengalami masalah dalam multi - Program yang terinstal.

Seseorang tidak dapat berargumen bahwa 'biasanya' Anda tidak ingin pengguna kelas Anda dapat melakukan hal-hal ini atau 'biasanya' yang Anda inginkan ... Itu tergantung pada fungsionalitas apa yang Anda koding. Anda tidak dapat membuat aturan praktis karena Anda tidak dapat memprediksi semua kasus penggunaan.

Pertimbangkan untuk misalnya penulis cetak yang menggunakan kunci internal tetapi kemudian orang-orang kesulitan untuk menggunakannya dari banyak utas jika mereka tidak ingin hasil mereka interleave.

Haruskah kunci Anda dapat diakses di luar kelas atau tidak adalah keputusan Anda sebagai programmer berdasarkan fungsi apa yang dimiliki kelas. Itu adalah bagian dari api. Anda tidak dapat pindah misalnya dari disinkronkan (ini) ke disinkronkan (provateObjet) tanpa risiko melanggar perubahan dalam kode yang menggunakannya.

Catatan 1: Saya tahu Anda dapat mencapai apa pun yang disinkronkan (ini) 'mencapai' dengan menggunakan objek kunci eksplisit dan mengeksposnya tapi saya pikir itu tidak perlu jika perilaku Anda didokumentasikan dengan baik dan Anda benar-benar tahu apa artinya mengunci 'ini'.

Catatan 2: Saya tidak setuju dengan argumen bahwa jika beberapa kode secara tidak sengaja mencuri kunci Anda, ini adalah bug dan Anda harus menyelesaikannya. Ini adalah argumen yang sama dengan mengatakan bahwa saya dapat membuat semua metode saya publik bahkan jika mereka tidak dimaksudkan untuk umum. Jika seseorang 'secara tidak sengaja' memanggil saya yang dimaksudkan sebagai metode pribadi, ini adalah bug. Mengapa mengaktifkan kecelakaan ini sejak awal !!! Jika kemampuan mencuri kunci Anda adalah masalah bagi kelas Anda, jangan izinkan. Sesimpel itu.

Amit Mittal
sumber
-3

Saya pikir poin satu (orang lain menggunakan kunci Anda) dan dua (semua metode menggunakan kunci yang sama tidak perlu) dapat terjadi pada aplikasi yang cukup besar. Terutama ketika tidak ada komunikasi yang baik antar pengembang.

Itu tidak dilempar batu, itu sebagian besar masalah praktik yang baik dan mencegah kesalahan.

Yoni Roit
sumber