Son olmayan alanın senkronizasyonu


91

Final olmayan bir sınıf alanında her senkronizasyonda bir uyarı görüntüleniyor. İşte kod:

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

bu yüzden kodlamayı şu şekilde değiştirdim:

 public class X  
 {  

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

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

Yukarıdaki kodun son olmayan bir sınıf alanında eşitleme yapmanın doğru yolu olduğundan emin değilim. Son olmayan bir alanı nasıl senkronize edebilirim?

Yanıtlar:


127

Her şeyden önce, eşzamanlılık sorunlarını daha yüksek bir soyutlama düzeyinde ele almak için gerçekten çok uğraşmanızı tavsiye ederim, yani bunu java.util.concurrent'daki ExecutorServices, Callables, Futures vb. Sınıfları kullanarak çözmek .

Bununla birlikte, son olmayan bir alanda kendi başına senkronizasyon yapmakta yanlış bir şey yoktur . Nesne başvurusu değişirse, kodun aynı bölümünün paralel olarak çalıştırılabileceğini aklınızda bulundurmanız yeterlidir . Yani, eğer bir iş parçacığı kodu eşitlenmiş blokta çalıştırırsa ve birisi çağırırsa setO(...), başka bir iş parçacığı aynı eşzamanlı bloğu aynı anda aynı örnek üzerinde çalıştırabilir .

Özel erişime ihtiyaç duyduğunuz nesneyi (veya daha iyisi onu korumaya adanmış bir nesneyi) senkronize edin.


1
Ben, söylüyorum eğer olmayan bir nihai sahada senkronize nesnenin özel erişimle kod ishal pasajı farkında olmalıdır osenkronize blok ulaşıldı zamana gönderme. Nesne odeğişikliklere atıfta bulunursa , başka bir iş parçacığı gelebilir ve senkronize edilmiş kod bloğunu çalıştırabilir.
aioobe

42
Başparmak kuralınıza katılmıyorum - Tek amacı diğer durumu korumak olan bir nesnede senkronize olmayı tercih ederim . Bir nesneye kilitlemek dışında hiçbir şey yapmıyorsanız, başka hiçbir kodun onu kilitleyemeyeceğinden emin olursunuz. Yöntemlerini çağırdığınız "gerçek" bir nesneye kilitlenirseniz, bu nesne kendi kendine de senkronize olabilir, bu da kilitleme hakkında mantık yürütmeyi zorlaştırır.
Jon Skeet

9
Benim cevap dediğimiz gibi, seni neden, bana çok dikkatli haklı olması gerekir düşünüyorum istediğiniz böyle bir şey yapmak için. Ve ben de senkronize etmeyi tavsiye etmiyorum this- sınıfta yalnızca kilitleme amacıyla bir son değişken oluşturmanızı tavsiye ederim , bu da başkalarının aynı nesneye kilitlenmesini engeller.
Jon Skeet

1
Bu başka bir iyi nokta ve ben de aynı fikirdeyim; son olmayan bir değişkeni kilitlemek kesinlikle dikkatli bir gerekçelendirme gerektirir.
aioobe

Eşitleme için kullanılan bir nesneyi değiştirmeyle ilgili bellek görünürlüğü sorunlarından emin değilim. Bence bir nesneyi değiştirip kodun bu değişikliği doğru şekilde görmesine güvenerek "aynı kod bölümü paralel olarak çalıştırılabilir" diye büyük sorun yaşarsınız. Senkronizasyon bloğu içinde erişilen değişkenlerin aksine, bellek modeli tarafından, kilitlemek için kullanılan alanların bellek görünürlüğüne hangi garantilerin genişletildiğinden emin değilim. Benim genel kuralım, eğer bir şeyi senkronize ederseniz, nihai olmalıdır.
Mike Q

47

Bu gerçekten iyi bir fikir değil - çünkü senkronize bloklarınız artık tutarlı bir şekilde gerçekten senkronize edilmiyor .

Senkronize blokların, bir seferde yalnızca bir iş parçacığının bazı paylaşılan verilere erişmesini sağlama amaçlı olduğunu varsayarsak, şunları göz önünde bulundurun:

  • Diş 1, senkronize bloğa girer. Yay - paylaşılan verilere özel erişime sahip ...
  • Thread 2 çağrı setO ()
  • İplik 3 (veya hala 2 ...) senkronize bloğa girer. Eek! Paylaşılan verilere özel erişime sahip olduğunu düşünüyor, ancak iş parçacığı 1 hala onunla uğraşıyor ...

Neden olur istemek Bunun gerçekleşmesi? Belki mantıklı olduğu bazı çok özel durumlar vardır ... ama memnun olmadan önce bana belirli bir kullanım senaryosu (yukarıda verdiğim senaryo türünü hafifletmenin yollarıyla birlikte) sunmanız gerekir. o.


2
@aioobe: Ama o zaman iş parçacığı 1 hala listeyi değiştiren (ve sık sık başvuran) bir kod çalıştırıyor olabilir o- ve yürütmenin bir kısmı farklı bir listeyi değiştirmeye başlar. Bu nasıl iyi bir fikir olabilir? Bence dokunduğunuz nesnelere başka şekillerde kilitlenmenin iyi bir fikir olup olmadığı konusunda temelde anlaşamıyoruz. Diğer kodun kilitleme açısından ne yaptığını bilmeden kodum hakkında akıl yürütmeyi tercih ederim.
Jon Skeet

2
@Felype: Görünüşe göre daha ayrıntılı bir soruyu ayrı bir soru olarak sormalısınız - ama evet, genellikle kilitler gibi ayrı nesneler yaratırım.
Jon Skeet

3
@VitBernatik: Hayır. Eğer X iş parçacığı yapılandırmayı değiştirmeye başlarsa, iş parçacığı Y eşitlenmekte olan değişkenin değerini değiştirir, ardından iş parçacığı Z yapılandırmayı değiştirmeye başlar, bu durumda hem X hem de Z aynı anda yapılandırmayı değiştirir, bu kötüdür .
Jon Skeet

1
Kısacası, bu tür kilit nesnelerini her zaman nihai ilan etmemiz daha güvenli, değil mi?
St.Antario

2
@LinkTheProgrammer: "Senkronize edilmiş bir yöntem örnekteki her bir nesneyi senkronize eder" - hayır değil. Bu kesinlikle doğru değil ve senkronizasyon anlayışınızı yeniden gözden geçirmelisiniz.
Jon Skeet

12

John'un yorumlarından birine katılıyorum: Değişkenin referans değişiklikleri durumunda tutarsızlıkları önlemek için son olmayan bir değişkene erişirken daima bir son kilit kukla kullanmalısınız. Yani her durumda ve ilk kural olarak:

Kural 1: Bir alan nihai değilse, her zaman bir (özel) son kilit kukla kullanın.

Neden # 1: Kilidi tutuyorsunuz ve değişkenin referansını kendiniz değiştiriyorsunuz. Senkronize kilidin dışında bekleyen başka bir iş parçacığı, korumalı bloğa girebilir.

Neden # 2: Kilidi tutuyorsunuz ve başka bir iş parçacığı değişkenin referansını değiştiriyor. Sonuç aynıdır: Korunan bloğa başka bir iş parçacığı girebilir.

Ancak son bir kilit kukla kullanıldığında, başka bir sorun daha vardır : Yanlış veri alabilirsiniz, çünkü nihai olmayan nesneniz yalnızca senkronizasyon (nesne) çağrılırken RAM ile senkronize edilir. Dolayısıyla, ikinci bir kural olarak:

Kural # 2: Nihai olmayan bir nesneyi kilitlerken, her zaman ikisini de yapmanız gerekir: RAM senkronizasyonu için son kilit kukla ve nihai olmayan nesnenin kilidi kullanma. (Tek alternatif, nesnenin tüm alanlarını değişken olarak ilan etmek olacaktır!)

Bu kilitlere "iç içe kilitler" de denir. Onları her zaman aynı sırayla aramanız gerektiğini unutmayın, aksi takdirde kilitleneceksiniz :

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

Gördüğünüz gibi iki kilidi doğrudan aynı satıra yazıyorum, çünkü bunlar her zaman birbirine aittir. Bunun gibi, 10 iç içe geçme kilidi bile yapabilirsiniz:

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

synchronized (LOCK3)Başka bir iş parçacığında olduğu gibi sadece bir iç kilit alırsanız bu kodun kırılmayacağını unutmayın . Ancak başka bir ileti dizisinde şunun gibi bir şey çağırırsanız kırılır:

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

Son olmayan alanları işlerken bu tür iç içe geçmiş kilitler etrafında tek bir geçici çözüm vardır:

Kural # 2 - Alternatif: Nesnenin tüm alanlarını geçici olarak bildirin. (Burada bunu yapmanın dezavantajlarından söz etmeyeceğim, örneğin, okumalar için bile x seviyesi önbelleklerde herhangi bir depolamayı önlemek gibi.)

Bu nedenle aioobe oldukça haklı: Sadece java.util.concurrent kullanın. Veya senkronizasyonla ilgili her şeyi anlamaya başlayın ve bunu iç içe geçmiş kilitlerle kendiniz yapın. ;)

Son olmayan alanlardaki senkronizasyonun neden bozulduğuyla ilgili daha fazla ayrıntı için, test durumuma bir göz atın: https://stackoverflow.com/a/21460055/2012947

Ve RAM ve önbellekler nedeniyle neden senkronize olmanız gerektiğine dair daha fazla ayrıntı için buraya bir göz atın: https://stackoverflow.com/a/21409975/2012947


1
Bence oayar ve okuma nesnesi arasında "önce-olur" ilişkisi kurmak için ayarlayıcısını senkronize (KİLİT) ile sarmalamanız gerekir o. Bunu benzer bir
sorumla tartışıyorum

DataObject üyelerine erişimi senkronize etmek için dataObject kullanıyorum. Bu nasıl yanlış? DataObject farklı bir yere işaret etmeye başlarsa, eşzamanlı iş parçacıklarının onu değiştirmesini önlemek için yeni verilerde eşitlenmesini istiyorum. Bununla ilgili herhangi bir sorun var mı?
Harmen

2

Burada doğru cevabı gerçekten göremiyorum, yani, bunu yapmak tamamen sorun değil.

Neden bir uyarı olduğundan bile emin değilim, bunda yanlış bir şey yok. JVM almak emin olur bazı Bir değer okuduğunuzda geçerli nesne geri (veya null) ve üzerinde senkronize edebilirsiniz herhangi bir nesne.

Kilidi kullanımdayken gerçekten değiştirmeyi planlıyorsanız (örneğin, kullanmaya başlamadan önce bir init yönteminden değiştirmenin aksine), değiştirmeyi planladığınız değişkeni yapmanız gerekir volatile. Daha sonra yapmanız gereken tek şey hem eski hem de yeni nesneyi senkronize etmektir ve değeri güvenle değiştirebilirsiniz.

public volatile Object lock;

...

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

Orada. Karmaşık değil, kilitlerle (muteksler) kod yazmak aslında oldukça kolaydır. Bunlar olmadan kod yazmak (ücretsiz kodu kilitlemek) zor olan şeydir.


Bu işe yaramayabilir. O'nun O1'e göre başladığını söyleyin, ardından T1 dişlisini o (= O1) ve O2'yi kilitler ve o'u O2'ye ayarlar. Aynı zamanda Diş T2, O1'i kilitler ve T1'in kilidini açmasını bekler. O1 kilidini aldığında, O3 olarak ayarlanacaktır. Bu senaryoda, T1'in serbest bırakılması O1 ile T2'nin O1'i kilitlemesi arasında O1, o üzerinden kilitleme için geçersiz hale geldi. Bu sırada başka bir iş parçacığı kilitleme için o (= O2) kullanabilir ve T2 ile yarışta kesintisiz devam edebilir.
GPS

2

DÜZENLEME: Yani bu çözüm (Jon Skeet tarafından önerildiği gibi) nesne referansı değişirken "senkronize (nesne) {}" uygulamasının atomikliği ile ilgili bir sorun olabilir. Ayrı ayrı sordum ve Bay Ericson'a göre iş parçacığı güvenli değil - bkz: Senkronize bloğa girmek atomik mi? . Öyleyse bunu nasıl YAPILMAMASI gerektiğini örnek olarak alın - neden bağlantılarıyla;)

Senkronize () atomik olursa nasıl çalışacağını koda bakın:

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

    static Config cfg = new Config();

    static class Doer extends Thread {
        char id;

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

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

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

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

}

1
Bu sorun olmayabilir - ancak bellek modelinde, senkronize ettiğiniz değerin en son yazılan değer olduğuna dair herhangi bir garanti olup olmadığını bilmiyorum - atomik olarak "oku ve senkronize et" garantisi olduğunu sanmıyorum. Kişisel olarak, basit olması için başka kullanımları olan monitörlerde senkronizasyon yapmaktan kaçınmaya çalışıyorum. (Ayrı bir alana sahip olduğunuzda, dikkatlice düşünmek yerine kod açıkça doğru hale gelir .)
Jon Skeet

@Jon. Cevap için teşekkürler! Endişeni duyuyorum. Bu durumda harici kilidin "senkronize atomisite" sorununu ortadan kaldıracağını kabul ediyorum. Bu nedenle tercih edilebilir. Çalışma zamanında daha fazla konfigürasyon sunmak ve farklı iş parçacığı grupları için farklı konfigürasyonu paylaşmak isteyebileceğiniz durumlar olabilir (benim durumum olmasa da). Ve sonra bu çözüm ilginç hale gelebilir. Senkronize () atomisiteyle ilgili stackoverflow.com/questions/29217266/… sorusunu gönderdim - bu yüzden kullanılıp kullanılamayacağını göreceğiz (ve birisi cevaplayacak mı)
Vit Bernatik

2

AtomicReference ihtiyacınıza uygun.

Atomik paket hakkında java belgelerinden :

Tek değişkenler üzerinde kilitsiz iş parçacığı güvenli programlamayı destekleyen küçük bir araç seti. Özünde, bu paketteki sınıflar, uçucu değerler, alanlar ve dizi öğeleri kavramını, formun atomik koşullu güncelleme işlemini de sağlayanlara genişletir:

boolean compareAndSet(expectedValue, updateValue);

Basit kod:

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

Yukarıdaki örnekte, StringkendiObject

İlgili SE sorusu:

Java'da AtomicReference ne zaman kullanılır?


1

Bir oörneğinin ömrü boyunca hiçbir zaman değişmezse X, ikinci sürüm, senkronizasyonun dahil olup olmadığına bakılmaksızın daha iyi bir stildir.

Şimdi, ilk versiyonda bir sorun olup olmadığını, o sınıfta başka neler olup bittiğini bilmeden cevaplamak imkansız. Derleyiciye hataya açık göründüğüne katılma eğilimindeyim (başkalarının söylediklerini tekrar etmeyeceğim).


1

Sadece iki kuruşumu ekleyerek: Designer aracılığıyla başlatılan bileşeni kullandığımda bu uyarıyı aldım, bu nedenle alanı gerçekten son olamaz, çünkü yapıcı parametreleri alamaz. Başka bir deyişle, son anahtar kelime olmadan yarı son alanım vardı.

Sanırım bu yüzden sadece bir uyarı: Muhtemelen yanlış bir şey yapıyorsunuz, ama bu da doğru olabilir.

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.