Java ile senkronize (bu) kaçının?


381

Java senkronizasyonu hakkında SO'ya her soru çıktığında, bazı insanlar synchronized(this)kaçınılması gerektiğini belirtmek için çok istekli olurlar . Bunun yerine, özel bir referans üzerinde bir kilidin tercih edileceğini iddia ediyorlar.

Verilen nedenlerden bazıları:

Ben de dahil olmak üzere diğer insanlar synchronized(this)çok kullanılan bir deyim olduğunu (Java kütüphanelerinde de) güvenli ve iyi anlaşıldığını iddia ediyorlar . Kaçınılmamalıdır çünkü bir hatanız var ve çok iş parçacıklı programınızda neler olup bittiğine dair bir ipucunuz yok. Başka bir deyişle: geçerliyse kullanın.

İşi thisyaparken kilitlenmekten kaçınmanın tercih edildiği bazı gerçek dünya örneklerini (foobar şeyler yok) görmekle ilgileniyorum synchronized(this).

Bu nedenle: her zaman kaçınmalı synchronized(this)ve özel bir referanstaki bir kilitle değiştirmeli misiniz ?


Bazı ek bilgiler (cevaplar verildiğinde güncellenir):

  • örnek senkronizasyonu hakkında konuşuyoruz
  • hem örtük ( synchronizedyöntemler) hem de açık biçimleri synchronized(this)dikkate alınır
  • Bloch'u veya konuyla ilgili diğer yetkilileri teklif ederseniz, sevmediğiniz parçaları dışarıda bırakmayın (örn. Etkili Java, İş Parçacığı Güvenliği öğesi: Genellikle örneğin kendisinin kilididir, ancak istisnalar vardır.)
  • Kilitlemenizde synchronized(this)sağlar dışında ayrıntıya ihtiyacınız varsa , synchronized(this)geçerli değildir, bu yüzden sorun değildir

4
Ayrıca bağlamın önemli olduğunu da belirtmek isterim - "Genel olarak, örneğin kendisinin kilidi" biti, kilidi genel hale getirdiğinizde, koşullu bir iş parçacığı için güvenli sınıfı belgeleme hakkında bir bölüm içinde. Başka bir deyişle, bu karar zaten bu kararı verdiğinizde geçerlidir.
Jon Skeet

Dahili senkronizasyon olmadığında ve harici senkronizasyon gerektiğinde, kilit genellikle örneğin kendisidir, diyor Bloch. Öyleyse neden 'bu' kilitli dahili senkronizasyon için böyle bir durum söz konusu olmasın? (Dokümantasyonun önemi başka bir konudur.)
eljenso

Harici bir Nesne üzerinde kilitleme büyük olasılıkla CPU önbellekleri (cf. MESIF ve MOESI) arasında değiştirilecek ve değiştirilecek ayrı bir önbellek hattı gerektireceğinden, genişletilmiş ayrıntı düzeyi ile ekstra CPU önbelleği ve veri yolu istekleri arasında bir denge vardır.
ArtemGr

1
Bence, savunmacı programlama dünyasında, hataları deyimle değil, kodla önlersiniz. Birisi bana "Senkronizasyonunuz ne kadar optimize edilmiş?" Sorusunu sorduğunda, "Çok, bir başkası deyimi takip etmedikçe" yerine "Çok" demek istiyorum.
Sgene9

Yanıtlar:


132

Her noktayı ayrı ayrı ele alacağım.

  1. Bazı kötü kodlar kilidini çalabilir (bu çok popüler, ayrıca "yanlışlıkla" bir varyantı var)

    Yanlışlıkla endişeleniyorum . Bunun anlamı, bu kullanımın thissınıfınızın açık arayüzünün bir parçası olması ve belgelenmesi gerektiğidir. Bazen başka kodların kilidinizi kullanabilmesi istenir. Bu, Collections.synchronizedMapjavadoc gibi şeyler için geçerlidir .

  2. Aynı sınıftaki tüm senkronize yöntemler aynı kilidi kullanır ve bu da verimi azaltır

    Bu aşırı basit düşüncedir; sadece kurtulmak synchronized(this)sorunu çözmez. Verim için uygun senkronizasyon daha fazla düşünülecektir.

  3. (Gereksiz) çok fazla bilgi açığa çıkarıyorsunuz

    Bu # 1'in bir çeşididir. Kullanımı synchronized(this)arayüzünüzün bir parçasıdır. Eğer bunun açığa çıkmasını istemiyorsanız / buna ihtiyacınız yoksa yapmayın.


1. "senkronize", sınıfınızın açık arayüzünün bir parçası değildir. 2. katılıyorum 3. bkz. 1.
eljenso

66
Esasen senkronize edilir (bu) , dış kodun sınıfınızın çalışmasını etkileyebileceği anlamına gelir. Bu yüzden, dil olmasa bile, onu arayüz olarak belgelemeniz gerektiğini iddia ediyorum.
Darron

15
Benzer. Collections.synchronizedMap () için Javadoc'a bakın - döndürülen nesne dahili olarak senkronize (bu) kullanır ve tüketicinin yineleme gibi büyük ölçekli atomik işlemler için aynı kilidi kullanmasını bekler.
Darron

3
Aslında Collections.synchronizedMap () öğesi, senkronize edilmiş (bu) dahili olarak KULLANMAZ, özel bir son kilit nesnesi kullanır.
Bas Leijdekkers

1
@Bas Leijdekkers: belgeler , senkronizasyonun döndürülen harita örneğinde gerçekleştiğini açıkça belirtir. İlginç olan, görünümlerin döndürdüğü keySet()ve values()kilitlenmediği (onların) thisdeğil, tüm harita işlemleri için tutarlı davranış elde etmek için önemli olan harita örneğidir. Kilit nesnesinin bir değişkeni hesaba katmasının nedeni, alt sınıfın SynchronizedSortedMaporijinal harita örneğinde kilitlenen alt haritaları uygulamak için ona gereksinim duymasıdır.
Holger

86

Öncelikle şunu belirtmeliyiz:

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

semantik olarak şuna eşittir:

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

kullanmamak için bir sebep synchronized(this). synchronized(this)Blok çevresinde bir şeyler yapabileceğinizi iddia edebilirsiniz . Genel neden, senkronize kontrolü hiç yapmak zorunda kalmamaktır, bu da her türlü eşzamanlılık problemine, özellikle de çift ​​kontrol kilitleme problemine yol açar , bu da nispeten basit bir kontrol yapmanın ne kadar zor olabileceğini göstermeye devam eder threadsafe.

Özel kilit, asla kötü bir fikir olmayan savunma mekanizmasıdır.

Ayrıca, belirttiğiniz gibi, özel kilitler ayrıntı düzeyini kontrol edebilir. Bir nesne üzerindeki bir işlem kümesi diğeriyle tamamen ilgisiz olabilir, ancak synchronized(this)hepsine erişimi karşılıklı olarak dışlar.

synchronized(this) sadece sana hiçbir şey vermiyor.


4
"senkronize (bu) gerçekten size hiçbir şey vermiyor." Tamam, ben bir senkronizasyon (myPrivateFinalLock) ile değiştirin. Bu bana ne veriyor? Bunun savunma mekanizması olduğunu söylüyorsun. Neye karşı korunuyorum?
eljenso

14
Harici nesneler tarafından 'bu' yanlışlıkla (veya kötü amaçlı) kilitlenmeye karşı korunursunuz.
cletus

14
Bu yanıta hiç katılmıyorum: Her zaman mümkün olan en kısa süre için bir kilit tutulmalıdır ve tam olarak tüm yöntemi senkronize etmek yerine senkronize bir blok etrafında "şeyler" yapmak istemenizin nedeni budur .
Olivier

5
Senkronize blok dışında bir şeyler yapmak her zaman iyi niyetlidir. Mesele şu ki, insanlar bunu çoğu zaman yanlış anlıyorlar ve bunu çift kontrollu kilitleme probleminde olduğu gibi fark etmiyorlar. Cehenneme giden yol iyi niyetlerle döşenmiştir.
cletus

16
Genel olarak "X, asla kötü bir fikir olmayan savunmacı bir mekanizmadır" ile aynı fikirde değilim. Bu tutum nedeniyle gereksiz yere çok fazla şişirilmiş kod var.
finnw

54

Eşitlenmiş (bu) kullanırken, sınıf örneğini kilit olarak kullanırsınız. Bu, kilit diş 1 ile alınırken , diş 2'nin beklemesi gerektiği anlamına gelir .

Aşağıdaki kodu varsayalım:

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


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

Değişken modifiye Yöntem 1 a ve değişken modifiye yöntem 2 b , iki ipliği aynı değişken eşzamanlı modifikasyonu kaçınılmalıdır ve o. İken AMA thread1 değiştirerek bir ve thread2 modifiye b kendisine herhangi yarış durumu olmadan gerçekleştirilebilir.

Ne yazık ki, bir kilit için aynı başvuruyu kullandığımız için yukarıdaki kod buna izin vermeyecektir; Bu, bir yarış koşulunda olmasalar bile iş parçacıklarının beklemesi gerektiği ve kodun programın eşzamanlılığını feda ettiği anlamına gelir.

Çözüm, iki farklı değişken için 2 farklı kilit kullanmaktır :

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 ++;      
        }
        // ................
    }

}

Yukarıdaki örnek daha ince taneli kilitler (bir yerine 2 kilit ( sırasıyla a ve b değişkenleri için lockA ve lockB ) kullanır ve sonuç olarak daha iyi eşzamanlılığa izin verir, öte yandan ilk örnekten daha karmaşık hale gelir ...


Bu çok tehlikelidir. Artık bir istemci tarafı (bu sınıfın kullanıcısı) kilit siparişi gereksinimi getirdiniz. İki iş parçacığı farklı bir sırayla method1 () ve method2 () çağırıyorsa, büyük olasılıkla kilitlenme olasılığı vardır, ancak bu sınıftaki kullanıcının durumun böyle olduğu hakkında hiçbir fikri yoktur.
daveb

7
"Senkronize (bu)" tarafından sağlanmayan ayrıntı düzeyi sorumluğumun dışında. Kilit alanlarınız kesin olmamalı mı?
eljenso

10
bir kilitlenme elde etmek için A ile senkronize edilen bloktan B. daveb ile senkronize edilen bloğa bir çağrı yapmalıyız, yanılıyorsunuz ...
Andreas Bakurov

3
Bu örnekte görebildiğim kadarıyla kilitlenme yok. Sadece sözde kod olduğunu kabul ediyorum ama java.util.concurrent.locks.Lock gibi java.util.concurrent.locks.ReentrantLock
Shawn Vader

15

Dogmatik kurallara körü körüne uymama konusunda hemfikir olsam da, "kilit çalma" senaryosu sizin için çok eksantrik görünüyor mu? Bir iş parçacığı gerçekten de nesnenizin üzerindeki kilidi "dıştan" ( synchronized(theObject) {...}) alabilir ve senkronize edilmiş örnek yöntemlerinde bekleyen diğer iş parçacıklarını engelleyebilir.

Kötü amaçlı koda inanmıyorsanız, bu kodun üçüncü taraflardan gelebileceğini düşünün (örneğin, bir çeşit uygulama sunucusu geliştiriyorsanız).

"Kazara" sürüm daha az olası görünüyor, ancak dedikleri gibi "aptalca bir şey yapın ve birisi daha iyi bir aptal icat edecektir".

Bu yüzden sınıfın ne yaptığına bağlı düşünce okuluna katılıyorum.


Eljenso adlı kullanıcının ilk 3 yorumunu düzenle:

Kilit çalma problemini hiç yaşamadım ama işte hayali bir senaryo:

Diyelim ki sisteminiz bir sunucu uygulaması kapsayıcısı ve düşündüğümüz nesne ServletContextuygulama. Onun getAttributebağlam nitelikleri paylaşılan veri olarak yöntem, iş parçacığı güvenli olmalıdır; yani onusynchronized . Ayrıca, konteyner uygulamanıza dayalı bir genel barındırma hizmeti sağladığınızı düşünelim.

Ben müşterinizim ve "iyi" sunucu uygulamamı sitenize dağıtın. Kodumda bir çağrı vargetAttribute .

Başka bir müşteri olarak gizlenen bir hacker, kötü niyetli sunucu uygulamasını sitenize yerleştirir. Aşağıdaki kodu içeririnitYöntemde :

senkronize edildi (this.getServletConfig (). getServletContext ()) {
   while (true) {}
}

Aynı sunucu uygulaması bağlamını paylaştığımızı varsayarsak (iki sunucu uygulaması aynı sanal ana bilgisayarda olduğu sürece spesifikasyon tarafından izin verilir), getAttributeçağrım sonsuza kadar kilitlenir. Bilgisayar korsanı benim sunucu uygulamamda bir DoS elde etti.

getAttributeÖzel bir kilitle senkronize edilirse bu saldırı mümkün değildir , çünkü 3. taraf kodu bu kilidi alamaz.

İtiraf etmeliyim ve bir sunucu uygulaması konteynerinin nasıl çalıştığına dair basit bir bakış açısı, ama IMHO bunu kanıtlıyor.

Bu yüzden tasarım seçimimi güvenlik değerlendirmesine göre yapacağım: örneklere erişimi olan kod üzerinde tam kontrole sahip olacak mıyım? Bir iş parçacığının süresiz olarak bir örneği kilitlemesinin sonucu ne olur?


sınıf-ne-sınıf-bağlıdır-yaparsa: 'önemli' bir nesne ise, özel referans kilitlemek? Başka örnek kilitleme yeterli mi?
eljenso

6
Evet, kilit çalma senaryosu bana çok zor geliyor. Herkes bundan bahsediyor, ama bunu kim yaptı ya da deneyimledi? Yapmamanız gereken bir nesneyi "yanlışlıkla" kilitlerseniz, bu tür bir durum için bir ad vardır: bu bir hatadır. Düzelt.
eljenso

4
Ayrıca, dahili referanslara kilitleme "harici senkronizasyon saldırısından" bağımsız değildir: kodun senkronize edilen belirli bir kısmının harici bir olayın gerçekleşmesini beklediğini biliyorsanız (örneğin, dosya yazma, DB'deki değer, zamanlayıcı olayı) engellemek için düzenleyin.
eljenso

İtiraf edeyim o aptallardan biri olduğumu itiraf edeyim, ama gençken yaptım. Kodun açık bir kilit nesnesi oluşturarak daha temiz olduğunu düşündüm ve bunun yerine monitöre katılmak için gereken başka bir özel son nesne kullandım. Nesnenin kendisinde bir senkronizasyon yaptığını bilmiyordum.
Alan

12

Bu duruma bağlıdır.
Yalnızca bir paylaşım varlığı veya birden fazla varsa.

Tüm çalışma örneğine buradan bakın

Küçük bir giriş.

İş parçacıkları ve paylaşılabilir varlıklar
Birden fazla iş parçacığının aynı öğeye erişmesi mümkündür, örneğin tek bir messageQueue paylaşan birden fazla bağlantı İş parçacıkları eşzamanlı olarak çalıştığından, birinin verilerini başka biri tarafından geçersiz kılma şansı olabilir, bu da dağınık bir durum olabilir.
Bu nedenle, paylaşılabilir varlığa bir kerede yalnızca bir iş parçacığıyla erişilmesini sağlamak için bir yola ihtiyacımız var. (UYUMLULUK).

Senkronize blok
senkronize () blok, paylaşılabilir varlığın aynı anda erişimini sağlamanın bir yoludur.
İlk olarak, küçük bir benzetme
Varsayalım ki bir tuvalet içinde iki kişilik bir P1, P2 (iplikler) bir Lavabo (paylaşılabilir varlık) var ve bir kapı (kilit) var.
Şimdi bir kişinin aynı anda lavabo kullanmasını istiyoruz.
Bir yaklaşım, kapı kilitlendiğinde kapıyı P1 ile kilitlemektir P2, p1 çalışmasını tamamlayana kadar bekler
P1 kapının kilidini açar,
sonra sadece p1 lavabo kullanabilir.

sözdizimi.

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

"this", sınıfla ilişkilendirilmiş içsel kilit sağlamıştır (Java geliştiricisi, Object nesnesini her nesnenin monitör olarak çalışabileceği şekilde tasarlamıştır). Yukarıdaki yaklaşım, yalnızca bir paylaşılan varlık ve birden çok iş parçacığı (1: N) olduğunda işe yarar. N paylaşılabilir varlık-M vida dişi Şimdi bir tuvalet içinde iki lavabo ve sadece bir kapının olduğu bir durumu düşünün. Eğer önceki yaklaşımı kullanıyorsak, p2 dışarıda beklerken sadece p1 her seferinde bir lavabo kullanabilir. Hiç kimse B2 (lavabo) kullanmadığı için kaynak israfıdır. Daha akıllıca bir yaklaşım, tuvalet içinde daha küçük bir oda oluşturmak ve lavabo başına bir kapı sağlamak olacaktır. Bu şekilde P1, B1'e erişebilir ve P2, B2'ye ve tersine erişebilir.
resim açıklamasını buraya girin

washbasin1;  
washbasin2;

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

  synchronized(lock1)
  {
    washbasin1;
  }

  synchronized(lock2)
  {
    washbasin2;
  }

resim açıklamasını buraya girin
resim açıklamasını buraya girin

Konular ----> burada daha fazlasını görün


11

Bu konuda C # ve Java kamplarında farklı bir fikir birliği var gibi görünüyor. Gördüğüm Java kodunun çoğu kullanır:

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

C # kodunun çoğunluğu tartışmasız daha güvenli olmayı seçer:

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

C # deyim kesinlikle daha güvenlidir. Daha önce de belirtildiği gibi, kilit dışına kötü amaçlı / yanlışlıkla erişilemez. Java kodu da bu risk altındadır, ancak Java topluluğunun zamanla biraz daha az güvenli, ancak biraz daha kısa sürüme çekildiği görülmektedir.

Bu, Java'ya karşı bir kazı anlamına gelmiyor, sadece her iki dilde çalışma deneyimimin bir yansıması.


3
Belki de C # daha genç bir dil olduğundan, Java kampında anlaşılan kötü kalıplardan öğrendiler ve bunun gibi kodları daha iyi kodladılar? Ayrıca daha az singleton var mı? :)
Bill K

3
O o. Muhtemelen doğrudur, ama yem için yükselmeyeceğim! Kesin diyebileceğim bir şey, C # kodunda daha büyük harfler var;)
serg10

1
Sadece doğru değil (güzel koymak için)
tcurdt

7

java.util.concurrentPaket büyük ölçüde benim iş parçacığı güvenli kod karmaşıklığını azalttı. Sadece devam etmek için anekdot kanıtlarım var, ancak gördüğüm çoğu işsynchronized(x) bir Kilit, Semafor veya Mandalı yeniden uyguluyor, ancak alt düzey monitörleri kullanıyor gibi görünüyor.

Bunu göz önünde bulundurarak, bu mekanizmalardan herhangi birini kullanarak senkronize etmek, bir kilidi sızdırmak yerine dahili bir nesne üzerinde senkronizasyona benzer. Bu, monitöre girişi iki veya daha fazla iş parçacığıyla denetlediğiniz konusunda mutlak bir kesinliğe sahip olmanız açısından faydalıdır.


6
  1. Mümkünse verilerinizi değişmez hale getirin ( final değişkenler)
  2. Birden çok iş parçacığında paylaşılan verilerin mutasyonundan kaçınamıyorsanız, üst düzey programlama yapıları kullanın [ör. Granüler LockAPI]

Bir Kilit, paylaşılan bir kaynağa özel erişim sağlar: tek seferde yalnızca bir iş parçacığı kilidi alabilir ve paylaşılan kaynağa tüm erişim, kilidin önce edinilmesini gerektirir.

Arabirimi ReentrantLockuygulayan örnek kodLock

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

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

Senkronize Kilitlemenin Avantajları (bu)

  1. Senkronize yöntemlerin veya ifadelerin kullanılması, tüm kilit alımını ve serbest bırakılmasını blok yapılı bir şekilde gerçekleşmeye zorlar.

  2. Kilit uygulamaları, senkronize yöntemlerin ve ifadelerin kullanımı üzerinde ek işlevler

    1. Bir kilit elde etmek için engellenmeyen bir girişim (tryLock() )
    2. Kesilebilen kilidi alma girişimi (lockInterruptibly() )
    3. Zaman aşımı ( tryLock(long, TimeUnit)) yapabilen kilidi alma girişimi .
  3. Bir Lock sınıfı, örtük monitör kilidinden oldukça farklı olan davranış ve anlambilim de sağlayabilir.

    1. garantili sipariş
    2. tekrar girmeyen kullanım
    3. Kilitlenme tespiti

Çeşitli SE türleri ile ilgili bu SE sorusuna bir göz atın Locks:

Senkronizasyon ve Kilit

Senkronize bloklar yerine gelişmiş eşzamanlılık API'sını kullanarak iş parçacığı güvenliğini sağlayabilirsiniz. Bu belge sayfası , iplik güvenliğini sağlamak için iyi programlama yapıları sağlar.

Nesneleri Kilitle , birçok eşzamanlı uygulamayı basitleştiren kilitleme deyimlerini destekler.

Yürütücüler iş parçacıklarını başlatmak ve yönetmek için üst düzey bir API tanımlar. Java.util.concurrent tarafından sağlanan yönetici uygulamaları, büyük ölçekli uygulamalar için uygun iş parçacığı havuzu yönetimi sağlar.

Eşzamanlı Koleksiyonlar büyük veri koleksiyonlarını yönetmeyi kolaylaştırır ve senkronizasyon ihtiyacını büyük ölçüde azaltabilir.

Atomik Değişkenler , senkronizasyonu en aza indiren ve bellek tutarlılığı hatalarını önlemeye yardımcı olan özelliklere sahiptir.

ThreadLocalRandom (JDK 7'de), birden fazla iş parçacığından verimli psödondom sayılarının üretilmesini sağlar.

Diğer programlama yapıları için de java.util.concurrent ve java.util.concurrent.atomic paketlerine bakın .


5

Buna karar verdiyseniz:

  • yapmanız gereken mevcut nesneye kilitlemektir; ve
  • bütün bir yöntemden daha küçük bir ayrıntı ile kilitlemek istersiniz;

o zaman synizezd (bu) üzerinde bir tabu görmüyorum.

Bazı insanlar bir yöntemin tüm içeriği içinde kasıtlı olarak senkronize edilmiş (bu) yöntemi (senkronize edilmiş yöntemi işaretlemek yerine) kullanırlar çünkü gerçekte hangi nesnenin senkronize edildiğini "okuyucuya daha açık" olduğunu düşünürler. İnsanlar bilinçli bir seçim yaptıkları sürece (örneğin, bunu yaparak yönteme ekstra bayt kodları eklediklerini ve bunun potansiyel optimizasyonlar üzerinde bir etkisi olabileceğini anlayın), özellikle bununla ilgili bir sorun görmüyorum . Her zaman programınızın eşzamanlı davranışını belgelemelisiniz, bu yüzden "'senkronize edilmiş' davranışı yayınlıyor" argümanını bu kadar ilgi çekici görmüyorum.

Hangi nesnenin kilidini kullanmanız gerektiği sorusuna gelince, yaptığınız şeyin mantığı ve sınıfınızın tipik olarak nasıl kullanılacağı mantığı ile beklenirse , mevcut nesne üzerinde senkronizasyonda yanlış bir şey olmadığını düşünüyorum . Örneğin, bir koleksiyonla, mantıksal olarak kilitlemeyi beklediğiniz nesne genellikle koleksiyonun kendisidir.


1
"Eğer bu mantık tarafından beklenirse ..." benim de karşılaşmaya çalıştığım bir nokta. Her zaman özel kilitleri kullanma noktasını görmüyorum , ancak genel fikir birliği daha iyi gibi görünüyor, çünkü incinmediği ve daha savunmacı olduğu için.
eljenso

4

Brian Goetz'un Uygulamada Java Eşzamanlılığı adlı bir kitapta, bunların her birinin neden kemeriniz altında hayati teknikler olduğuna dair iyi bir açıklama olduğunu düşünüyorum. Bir noktayı çok netleştiriyor - nesnenizin durumunu korumak için aynı kilidi "HER YERDE" kullanmalısınız. Senkronize yöntem ve bir nesne üzerinde senkronizasyon genellikle el ele gider. Vector, tüm yöntemlerini senkronize eder. Bir vektör nesnesinin tanıtıcısı varsa ve "yoksa koy" yapacaksanız, yalnızca kendi yöntemlerini senkronize eden Vector sizi devletin bozulmasından koruyamaz. Senkronize edilmiş (vectorHandle) kullanarak senkronize etmeniz gerekir. Bu, SAME kilidinin, vektör için bir tutamağa sahip olan ve vektörün genel durumunu koruyacağı her iş parçacığı tarafından alınmasına neden olur. Buna istemci tarafı kilitleme denir. Aslında vektörün tüm yöntemlerini senkronize ettiğini (bu) / senkronize ettiğini ve dolayısıyla vectorHandle nesnesi üzerinde senkronizasyonun vektör nesneleri durumunun düzgün senkronizasyonuna neden olacağını biliyoruz. Sadece bir iplik güvenli koleksiyonu kullandığınız için iplik güvenli olduğuna inanmak aptalca. ConcurrentHashMap'in bu tür işlemleri atomik hale getirmek için açıkça putIfAbsent yöntemini getirmesinin nedeni budur.

Özetle

  1. Yöntem düzeyinde senkronizasyon, istemci tarafında kilitlemeye izin verir.
  2. Özel bir kilit nesneniz varsa - istemci tarafı kilidini imkansız hale getirir. Sınıfınızın "yoksa varsa koy" işlevselliği olmadığını biliyorsanız, bu iyi olur.
  3. Bir kütüphane tasarlıyorsanız - o zaman bu konuda senkronize etmek veya yöntemi senkronize etmek genellikle daha akıllıca olur. Çünkü nadiren sınıfınızın nasıl kullanılacağına karar verme pozisyonundasınız.
  4. Vector özel bir kilit nesnesi kullansaydı - "yoksa koy" ifadesini doğru elde etmek imkansız olurdu. İstemci kodu hiçbir zaman özel kilide dokunmaz, böylece durumunu korumak için EXACT SAME LOCK'u kullanma temel kuralını ihlal eder.
  5. Diğerlerinin işaret ettiği gibi, bu veya senkronize yöntemlerle senkronizasyonda bir sorun vardır - birisi kilit alabilir ve asla serbest bırakamaz. Diğer tüm dişler kilidin serbest bırakılmasını beklerdi.
  6. Öyleyse ne yaptığınızı bilin ve doğru olanı benimseyin.
  7. Birisi, özel bir kilit nesnesine sahip olmanın size daha iyi ayrıntı düzeyi sağladığını (örneğin, iki işlem ilgisizse), daha iyi verim sağlayan farklı kilitlerle korunabileceğini savundu. Ama bu bence tasarım kokusu ve kod kokusu değil - iki işlem tamamen ilgisiz ise neden SAME sınıfının bir parçası? Bir sınıf kulübü neden ilgisiz işlevselliklere sahip olmalı? Bir yardımcı sınıf olabilir mi? Hmmmm - bazı örnek aynı örnek üzerinden dize manipülasyon ve takvim tarih biçimlendirme sağlar ?? ... bana hiç mantıklı gelmiyor !!

3

Hayır, her zaman olmamalısın . Ancak, belirli bir nesne üzerinde sadece kendilerine göre iş parçacığı güvenli olması gereken birden fazla endişe olduğunda bundan kaçınma eğilimindeyim. Örneğin, "label" ve "parent" alanlarına sahip değiştirilebilir bir veri nesneniz olabilir; bunların iş parçacığı güvenli olması gerekir, ancak birini değiştirmek diğerinin yazılmasını / okunmasını engellemeye gerek yoktur. (Uygulamada, uçucu alanları bildirerek ve / veya java.util.concurrent'ın AtomicFoo sarmalayıcılarını kullanarak bundan kaçınırım).

Senkronizasyon genel olarak biraz beceriksizdir, çünkü iş parçacıklarının birbirlerinin etrafında nasıl çalışmasına izin verilebileceğini düşünmek yerine büyük bir kilidi tokatlar. Kullanmak synchronized(this)bile daha hantal ve anti-sosyal, çünkü "hiç kimse bir şeyi değiştiremez Ben kilidi tutarken bu sınıfta " diyor. Bunu ne sıklıkta yapmanız gerekiyor?

Çok daha ayrıntılı kilitleri tercih ederdim; her şeyin değişmesini durdurmak isteseniz bile (belki nesneyi serileştiriyorsunuz), aynı şeyi elde etmek için tüm kilitleri elde edebilirsiniz, ayrıca bu daha açıktır. Kullandığınızda synchronized(this), neden senkronize ettiğiniz veya yan etkilerin ne olabileceği net değil. Kullanırsanız synchronized(labelMonitor), hatta daha da iyisi labelLock.getWriteLock().lock(), ne yaptığınızı ve kritik bölümünüzün etkilerinin sınırlandırıldığı açıktır.


3

Kısa cevap : Farkı anlamak ve koda bağlı olarak seçim yapmak zorundasınız.

Uzun cevap : Genel olarak çekişmeyi azaltmak için senkronize etmekten (bu) kaçınmayı tercih ederim, ancak özel kilitler farkında olmanız gereken karmaşıklığı ekler. Bu yüzden doğru iş için doğru senkronizasyonu kullanın. Çok iş parçacıklı programlama konusunda deneyimli değilseniz, bu konuya örnek kilitleme ve okumaya devam etmeyi tercih ederim. (Söyledi: sadece senkronize etme (bu) kullanmak , sınıfınızı otomatik olarak tamamen iş parçacığı açısından güvenli hale getirmez.) Bu kolay bir konu değil, ancak alıştıktan sonra senkronize (bu) olup olmadığının cevabı doğal olarak geliyor .


Deneyiminize bağlı olduğunu söylediğinizde sizi doğru anlıyor muyum?
eljenso

İlk olarak yazmak istediğiniz koda bağlıdır. Sadece senkronize etmemek için yönlendirirken biraz daha fazla deneyime ihtiyacınız olabileceğini söylemek (bu).
tcurdt

2

Bir kilit, görünürlüğü sağlamak veya bazı verileri yarışa yol açabilecek eşzamanlı modifikasyondan korumak için kullanılır .

Atomik olmak için sadece ilkel tip işlemleri yapmanız gerektiğinde ve benzerleri gibi seçenekler vardır AtomicInteger.

Ama böyle birbiriyle ilişkili iki tamsayı olduğunu varsayın xve ybirbirleriyle ilişkilidir ve bir atomik biçimde değiştirilmesi gerektiğini koordinatları. Sonra aynı kilidi kullanarak onları koruyacaksınız.

Bir kilit sadece birbiriyle ilişkili durumu korumalıdır. Ne az, ne fazla. synchronized(this)Her yöntemde kullanırsanız , sınıfın durumu ilgisiz olsa bile, ilişkisiz durumu güncelleştirse bile tüm evreler çekişme ile karşı karşıya kalır.

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

Yukarıdaki örnekte, her ikisini de değiştiren tek bir yöntemim var xvey değil iki farklı yöntem gibi xve yilgili ve mutasyona için iki farklı yöntem verilmiş olsaydı xve yayrıca daha sonra olmazdı parçacığı için güvenlidir.

Bu örnek, uygulanması gereken yolu göstermek için değil, sadece göstermek içindir. Bunu yapmanın en iyi yolu onu DERECE yapmaktır .

Şimdi karşıt Point örneğeTwoCounters , devlet birbiriyle ilgisiz olduğu için iki farklı kilitle korunan devletin @Andreas tarafından zaten sağlanmış bir örneği var .

İlişkisiz durumları korumak için farklı kilitler kullanma işlemine Kilit Çizgi veya Kilit Bölme adı verilir


1

Olmamasına senkronize nedeni bu bazen birden kilit daha ihtiyaç (genellikle bazı ek düşünme sonra kaldırılırsa ikinci kilit, ama yine de ara durumda gerek) olmasıdır. Eğer kilidi varsa bu , her zaman iki kilit biri olan hatırlamak zorunda bu ; özel bir Nesneyi kilitlerseniz değişken adı size bunu söyler.

Okuyucunun bakış açısından, bunun üzerinde kilitlenme görürseniz , her zaman iki soruyu cevaplamanız gerekir:

  1. Ne erişimin tür tarafından korunan bu ?
  2. bir kilit gerçekten yeterli, birisi hata getirmedi mi?

Bir örnek:

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

İki iş parçacığı longOperation()iki farklı durumda başlarsa , BadObjectkilitlerini alırlar; çağırmanın zamanı geldiğindel.onMyEvent(...) , bir kilitlenme var çünkü ipliklerin hiçbiri diğer nesnenin kilidini alamaz.

Bu örnekte, biri kısa ve biri uzun olan olmak üzere iki kilit kullanarak çıkmazı ortadan kaldırabiliriz.


2
Bu örnekte bir kilitlenme elde etmenin tek yolu, BadObjectA'nın longOperationB'yi çağırması , A'yı geçmesi myListenerve tersi. İmkansız değil, ama oldukça kıvrımlı, önceki puanlarımı destekliyor.
eljenso

1

Daha önce de belirtildiği gibi, senkronize fonksiyon sadece "this" kullandığında senkronize blok kilit nesnesi olarak kullanıcı tanımlı değişkeni kullanabilir. Ve tabii ki fonksiyonunuzun senkronize edilmesi gereken alanlarla manipüle edebilirsiniz.

Ancak herkes kilit nesnesi olarak "this" kullanarak senkronize fonksiyon ve blok arasında hiçbir fark olmadığını söylüyor. Bu doğru değil, her iki durumda da oluşturulacak bayt kodunda fark var. Senkronize blok kullanımı durumunda, "bu" ya referans veren yerel değişken tahsis edilmelidir. Ve sonuç olarak, biraz daha büyük bir fonksiyon boyutuna sahip olacağız (sadece birkaç fonksiyonunuz varsa ilgili değil).

Burada bulabileceğiniz farkın daha ayrıntılı açıklaması: http://www.artima.com/insidejvm/ed2/threadsynchP.html

Aşağıdaki bakış açısı nedeniyle senkronize blok kullanımı da iyi değildir:

Senkronize edilen anahtar kelime bir alanda çok sınırlıdır: senkronize bir bloktan çıkarken, o kilidi bekleyen tüm evrelerin engeli kaldırılmalıdır, ancak bu evrelerden sadece biri kilidi alır; diğerleri ise kilidin alındığını görür ve engellenmiş duruma geri döner. Bu sadece boşa harcanan işleme döngülerinin pek çoğu değil: genellikle bir iş parçacığının engellemesini kaldırmak için kullanılan bağlam anahtarı, diskin belleğinde disk belleği de içerir ve bu çok, çok, pahalıdır.

Bu alanda daha fazla ayrıntı için bu makaleyi okumanızı tavsiye ederim: http://java.dzone.com/articles/synchronized-considered


1

Bu gerçekten diğer cevaplara tamamlayıcıdır, ancak kilitlemek için özel nesneleri kullanmanın ana itirazınız, sınıfınızı iş mantığıyla ilgili olmayan alanlarla karıştırmaksa, Project Lombok @Synchronizedderleme zamanında kazan plakasını oluşturmalıdır:

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

derlemek

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

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

0

Senkronize kullanım için iyi bir örnek (bu).

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

Burada görebileceğiniz gibi, buradaki senkronize yöntemlerle uzun (muhtemelen sonsuz çalışma döngüsü yöntemi) işbirliği yapmak için senkronizasyonu kullanıyoruz.

Tabii ki özel alanda senkronize edilmiş kullanarak kolayca yeniden yazılabilir. Ancak bazen, senkronize yöntemlerle bazı tasarımlara sahip olduğumuzda (yani eski sınıf, türetilmiş, senkronize (bu) tek çözüm olabilir).


Burada kilit olarak herhangi bir nesne kullanılabilir. Olmasına gerek yok this. Özel bir alan olabilir.
finnw

Doğru, ancak bu örneğin amacı, yöntem senkronizasyonunu kullanmaya karar verirsek, uygun senkronizasyonun nasıl yapılacağını göstermekti.
Bart Prokop

0

Yapmak istediğiniz göreve bağlı, ama ben kullanmam. Ayrıca, gerçekleştirmek istediğiniz iş parçacığı tasarrufunun ilk etapta senkronize (bu) yapılamadığını kontrol edin. API'da size yardımcı olabilecek bazı güzel kilitler de var :)


0

Sadece bağımlılık olmadan kodun atomik bölümlerindeki benzersiz özel referanslar için olası bir çözümden bahsetmek istiyorum. Kilitli statik bir Hashmap ve yığın bilgilerini (tam sınıf adı ve satır numarası) kullanarak otomatik olarak gerekli referansları oluşturan atomic () adında basit bir statik yöntem kullanabilirsiniz. Sonra yeni kilit nesnesi yazmadan deyimleri senkronize etmek için bu yöntemi kullanabilirsiniz.

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

0

synchronized(this)Kilit mekanizması olarak kullanmaktan kaçının : Bu, tüm sınıf örneğini kilitler ve kilitlenmelere neden olabilir. Bu gibi durumlarda, yalnızca belirli bir yöntemi veya değişkeni kilitlemek için kodu yeniden düzenleyin, böylece tüm sınıf kilitlenmez. Synchronisedyöntem seviyesi içinde kullanılabilir. Aşağıdaki kod
kullanmak yerine, synchronized(this)bir yöntemi nasıl kilitleyebileceğinizi gösterir.

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

0

Bu soru zaten çözülmüş olsa da 2019'daki iki sentim.

Ne yaptığınızı biliyorsanız 'this' üzerinde kilitleme fena değil ama sahnenin arkasında 'this' üzerinde kilitleme (ne yazık ki yöntem tanımlamasında senkronize edilen anahtar kelimenin izin verdiği).

Sınıfınızdaki kullanıcıların kilidinizi 'çalmasını' (yani diğer iş parçacıklarının onunla başa çıkmasını önlemek) istiyorsanız, aslında tüm senkronize edilmiş yöntemlerin başka bir senkronizasyon yöntemi çalışırken beklemesini istersiniz. Kasıtlı ve iyi düşünülmüş olmalıdır (ve dolayısıyla kullanıcılarınızın bunu anlamasına yardımcı olmak için belgelenmelidir).

Daha ayrıntılı olarak söylemek gerekirse, erişilebilir olmayan bir kilidi kilitlerseniz (kimse kilidinizi 'çalamaz' ', tam kontrole sahip olursunuz vb. ..).

Benim için sorun, yöntem tanımı imzasındaki senkronize anahtar kelimenin programcıların düşünmemesini çok kolaylaştırması ne çok kilitlemek hakkında bir multi sorunlara girmek istemiyorsanız düşünülmesi gereken önemli bir şeydir dişli program.

Sınıfınızdaki kullanıcıların bu tür şeyleri yapmasını istemediğinizi 'tipik olarak' veya 'tipik olarak' istediğinizi iddia edemezsiniz ... Bu, hangi işlevselliği kodladığınıza bağlıdır. Tüm kullanım durumlarını tahmin edemediğiniz için küçük bir kural oluşturamazsınız.

Örneğin, dahili bir kilit kullanan yazıcıyı düşünün, ancak insanlar çıktılarının serpiştirilmesini istemiyorlarsa bunu birden fazla iş parçacığından kullanmakta zorlanırlar.

Kilidiniz sınıfın dışında erişilebilir olsun ya da olmasın, sınıfın sahip olduğu işleve göre bir programcı olarak kararınızdır. API'nin bir parçasıdır. Örneğin, kullanan koddaki değişiklikleri bozma riski olmadan, senkronize edilmiş (bu) durumundan senkronize edilmiş (provateObjet) konumuna gidemezsiniz.

Not 1: Açık bir kilit nesnesi kullanarak ve açığa çıkararak senkronize olan (bu) 'başardığı' her şeyi elde edebileceğinizi biliyorum, ancak davranışınızın iyi belgelenmesi ve aslında 'bu' üzerindeki kilitlemenin ne anlama geldiğini bilmek gereksizdir.

Not 2: Bazı kodlar yanlışlıkla kilidinizi çalıyorsa, bir hata olduğunu ve bunu çözmeniz gerektiği argümanı ile aynı fikirde değilim. Bu, bir bakıma, herkese açık olmamakla birlikte tüm yöntemlerimi herkese açık hale getirebileceğimi söylemekle aynı argüman. Birisi 'yanlışlıkla' benim özel yöntem olması amaçlanan çağırıyor onun bir hata. Neden bu kazayı ilk etapta etkinleştirin !!! Eğer kilidini çalma yeteneği sınıfınız için bir problemse, buna izin vermeyin. Kadar basit.


-3

Sanırım bir (kilidini kullanan başka biri) ve iki (aynı kilidi kullanan tüm yöntemler gereksiz yere) oldukça büyük bir uygulamada olabilir. Özellikle geliştiriciler arasında iyi bir iletişim olmadığında.

Taş dökülmez, çoğunlukla iyi uygulama ve hataları önleme meselesidir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.