Java'da uçucu ve senkronize arasındaki fark


233

Bir değişken olarak ilan volatileve her zaman synchronized(this)Java bir blokta değişken erişme arasındaki fark merak ediyorum ?

Bu makaleye göre http://www.javamex.com/tutorials/synchronization_volatile.shtml söylenecek çok şey var ve birçok farklılık var, ancak bazı benzerlikler var.

Bu bilgi parçasıyla özellikle ilgileniyorum:

...

  • uçucu bir değişkene erişimin engellenme potansiyeli asla yoktur: sadece basit bir okuma veya yazma yapıyoruz, bu nedenle senkronize edilmiş bir bloğun aksine hiçbir kilidi tutmayacağız;
  • çünkü değişken bir değişkene erişmek hiçbir zaman kilit tutmaz, atomik bir işlem olarak okumak-güncellemek-yazmak istediğimiz durumlar için uygun değildir ("bir güncellemeyi kaçırmak için hazırlıklı olmadıkça);

Okuma-güncelleme-yazma ile ne anlama geliyor ? Bir yazma da bir güncelleme değil mi yoksa sadece güncellemenin okumaya bağlı bir yazma olduğu anlamına mı geliyor?

En önemlisi, değişkenleri volatilebir synchronizedbloktan erişmek yerine tanımlamak ne zaman daha uygundur ? volatileGirdi bağımlı değişkenler için kullanmak iyi bir fikir mi? Örneğin render, işleme döngüsü aracılığıyla okunan ve bir tuşa basma olayı tarafından ayarlanan denilen bir değişken var mı?

Yanıtlar:


383

İplik güvenliğinin iki yönü olduğunu anlamak önemlidir .

  1. yürütme kontrolü ve
  2. bellek görünürlüğü

Birincisi, kodun ne zaman yürütüldüğünü (talimatların yürütüldüğü sıra dahil) ve eşzamanlı olarak yürütülüp yürütülemeyeceğini kontrol etmekle ilgilidir ve ikincisi, yapılan işlemin belleğindeki etkilerin diğer iş parçacıkları tarafından görülebildiği zamanla ilgilidir. Her CPU, ana bellek ile ana bellek arasında birkaç önbellek seviyesine sahip olduğundan, farklı CPU'larda veya çekirdeklerde çalışan iş parçacıkları, belirli bir zamanda "belleği" farklı şekilde görebilir, çünkü iş parçacıklarının ana belleğin özel kopyalarını edinmesine ve üzerinde çalışmasına izin verilir.

Kullanımı, synchronizedbaşka bir iş parçacığının aynı nesne için monitör (veya kilit) elde etmesini önler , böylece aynı nesne üzerinde eşitleme ile korunan tüm kod bloklarının aynı anda yürütülmesini önler . Senkronizasyon da bir "olur-öncesi" bellek bariyer, hiçbir şey noktasına bir kilit bazı iplik bültenleri kadar yapılması öyle ki bir hafıza görünürlük kısıtlamasını neden oluşturur görünür sonradan edinme başka bir evreye aynı kilidi o kilidi edinilen önce meydana gelmiş olduğu. Pratik terimlerle, mevcut donanımda, bu genellikle bir monitör alındığında CPU önbelleklerinin temizlenmesine neden olur ve serbest bırakıldığında ana belleğe yazar (her ikisi de (nispeten) pahalıdır.

Kullanılması volatilekuvvetler, diğer taraftan, uçucu değişkene tüm erişimler (okuma veya yazma) etkili bir işlemci önbelleklerine dışına uçucu değişken tutarak ana belleğe oluşmaya. Bu, değişkenin görünürlüğünün doğru olması ve erişim sırasının önemli olmaması gereken bazı eylemler için yararlı olabilir. Kullanmak volatileayrıca atomik olmalarının tedavisini değiştirir longve doublebunlara erişim gerektirir; bazı (eski) donanımlarda bu modern 64 bit donanımda olmasa da kilit gerektirebilir. Java 5+ için yeni (JSR-133) bellek modelinde, uçuculuk anlambilimi, bellek görünürlüğü ve talimat sıralaması ile senkronize olduğu kadar güçlü olacak şekilde güçlendirilmiştir (bkz. Http://www.cs.umd.edu) /users/pugh/java/memoryModel/jsr-133-faq.html#volatile). Görünürlük amacıyla, uçucu bir alana her erişim yarı senkronizasyon gibi davranır.

Yeni bellek modeli altında, değişken değişkenlerin birbirleriyle yeniden sıralanamayacağı hala doğrudur. Aradaki fark, artık çevrelerindeki normal alan erişimlerini yeniden sıralamanın o kadar kolay olmamasıdır. Uçucu bir alana yazma, monitör sürümü ile aynı hafıza etkisine sahiptir ve uçucu bir alandan okuma, bir monitör edinimi ile aynı hafıza etkisine sahiptir. Aslında, yeni bellek modeli, uçucu alan erişimlerinin diğer alan erişimleriyle yeniden sıralanmasına daha katı kısıtlamalar getirdiği için, uçucu olsun olmasın, Auçucu alana yazdığı zaman iş parçacığında görülebilen her şey okunduğunda fiş parçacığında görünür hale gelir .Bf

- JSR 133 (Java Bellek Modeli) SSS

Bu nedenle, şimdi her iki bellek bariyeri biçimi (mevcut JMM'nin altında), derleyicinin veya çalışma süresinin bariyer boyunca talimatları yeniden sipariş etmesini engelleyen bir talimat yeniden sıralama bariyerine neden olur. Eski JMM'de, volatil yeniden siparişi engellemedi. Bu önemli olabilir, çünkü bellek engelleri dışında uygulanan tek sınırlama, herhangi bir belirli iş parçacığı için , kodun net etkisinin, talimatların tam olarak göründükleri sırayla yürütülmesi ile aynı olmasıdır. kaynak.

Bir uçucu kullanım, paylaşılan ancak değişmeyen bir nesnenin anında yeniden yaratılmasıdır, diğer pek çok iş parçacığı, yürütme döngülerindeki belirli bir noktada nesneye referans alır. Bir kez yayınlandıktan sonra yeniden oluşturulmuş nesneyi kullanmaya başlamak için diğer iş parçacığı gerekir, ancak tam eşitleme ek yükü gerekmez ve onun görevlisi çekişme ve önbellek yıkama.

// Declaration
public class SharedLocation {
    static public SomeObject someObject=new SomeObject(); // default object
    }

// Publishing code
// Note: do not simply use SharedLocation.someObject.xxx(), since although
//       someObject will be internally consistent for xxx(), a subsequent 
//       call to yyy() might be inconsistent with xxx() if the object was 
//       replaced in between calls.
SharedLocation.someObject=new SomeObject(...); // new object is published

// Using code
private String getError() {
    SomeObject myCopy=SharedLocation.someObject; // gets current copy
    ...
    int cod=myCopy.getErrorCode();
    String txt=myCopy.getErrorText();
    return (cod+" - "+txt);
    }
// And so on, with myCopy always in a consistent state within and across calls
// Eventually we will return to the code that gets the current SomeObject.

Özellikle okuma-güncelleme-yazma sorunuzla ilgili olarak. Aşağıdaki güvenli olmayan kodu göz önünde bulundurun:

public void updateCounter() {
    if(counter==1000) { counter=0; }
    else              { counter++; }
    }

Şimdi, updateCounter () yöntemi senkronize edilmediğinde, iki iş parçacığı aynı anda girebilir. Ne olabileceğine dair birçok permütasyon arasında, iş parçacığı-1 counter == 1000 için testi yapar ve doğru bulur ve daha sonra askıya alınır. Daha sonra evre-2 aynı testi yapar ve bunu doğru görür ve askıya alınır. Sonra iş parçacığı-1 devam eder ve sayacı 0 olarak ayarlar. Daha sonra iş parçacığı-2 devam eder ve iş parçacığı-1 güncelleştirmesini kaçırdığı için sayacı 0 olarak ayarlar. Bu, açıkladığım gibi iş parçacığı geçişi gerçekleşmese bile, iki farklı CPU çekirdeğinde iki farklı önbelleğe alınmış sayacın bulunması ve iş parçacıklarının her biri ayrı bir çekirdek üzerinde çalıştığı için de olabilir. Bu nedenle, bir iş parçacığının bir değerde sayacı olabilir ve diğerinin yalnızca önbellekleme nedeniyle tamamen farklı bir değerde sayacı olabilir.

Bu örnekte önemli olan, değişken sayacın ana bellekten önbelleğe okunması, önbellekte güncellenmesi ve daha sonra bir bellek engeli oluştuğunda veya başka bir şey için önbellek gerektiğinde belirli bir noktada ana belleğe geri yazılmasıdır. Sayacın yapılmasıvolatile yapılması bu kodun iş parçacığı güvenliği için yetersizdir, çünkü maksimum ve atamalar için test, bir dizi atomik olmayan read+increment+writemakine talimatı olan artış da dahil olmak üzere ayrı işlemlerdir :

MOV EAX,counter
INC EAX
MOV counter,EAX

Uçucu değişkenler yalnızca üzerlerinde gerçekleştirilen tüm işlemler "atomik" olduğunda yararlıdır , örneğin, tamamen oluşturulmuş bir nesneye yapılan bir referansın sadece okunması veya yazılması (ve aslında, genellikle sadece tek bir noktadan yazılması). Başka bir örnek, dizinin yalnızca referansın yerel bir kopyasını alarak okunması şartıyla, yazma üzerine kopyalama listesini destekleyen geçici bir dizi başvurusu olacaktır.


5
Çok teşekkürler! Sayaçlı örneği anlamak kolaydır. Ancak, işler gerçek olduğunda, biraz farklı.
Albus Dumbledore

"Pratik terimlerle, mevcut donanımda, bu genellikle bir monitör alındığında CPU önbelleklerinin yıkanmasına neden olur ve serbest bırakıldığında ana belleğe yazar, her ikisi de pahalıdır (nispeten konuşur)." . CPU önbellekleri derken, her iş parçacığında yerel Java Yığınları ile aynı mıdır? veya bir iş parçacığının kendi yerel Heap sürümü var mı? Burada aptal olduğum için özür dilerim.
NishM

1
@nishm Aynı değil, ancak ilgili iş parçacıklarının yerel önbelleklerini içerecektir. .
Lawrence Dol

1
@ MarianPaździoch: Bir artış veya azalma bir okuma veya yazma DEĞİL , bir okuma ve yazma değil; bir kayda bir okuma, daha sonra bir kayıt artışı, daha sonra belleğe geri yazma. Okuma ve yazma işlemleri ayrı ayrı atomiktir, ancak bu tür birden çok işlem değildir.
Lawrence Dol

2
Yani, SSS göre değil yapılan işlemler sadece bir kilit edinme beri kilidi açıldıktan sonra görünür hale, ancak tüm bu iş parçacığı tarafından yapılan işlemler görünür yapılır. Kilit alımından önce yapılan işlemler bile.
Lii

97

uçucu bir alan değiştiricidir , senkronize ise kod bloklarını ve yöntemlerini değiştirir . Dolayısıyla, bu iki anahtar kelimeyi kullanarak basit bir erişimcinin üç varyasyonunu belirtebiliriz:

    int i1;
    int geti1() {return i1;}

    volatile int i2;
    int geti2() {return i2;}

    int i3;
    synchronized int geti3() {return i3;}

geti1()i1geçerli iş parçacığında o anda depolanan değere erişir . İş parçacıklarının yerel kopyaları olabilir ve verilerin diğer iş parçacıklarında tutulan verilerle aynı olması gerekmez.Özellikle, iş parçacığında başka bir iş parçacığı güncellenmiş olabilir i1, ancak geçerli iş parçacığındaki değer bundan farklı olabilir güncellenmiş değer. Aslında Java'nın bir "ana" bellek fikri vardır ve bu değişkenler için geçerli "doğru" değeri tutan bellektir. İş parçacıklarının değişkenler için kendi veri kopyaları olabilir ve iş parçacığı kopyaları "ana" bellekten farklı olabilir. "Ana" bellek değeri olması için Yani aslında, bu mümkün 1 için i1thread1 bir değere sahip üzere, 2 için i1ve için thread2bir değere sahip 3 için thread2 güncellenmiş i1 ancak bu güncellenmiş değeri hem henüz "ana" bellek veya diğer parçacıkları yayılır edilmemiştir var.i1Eğer thread1 ve

Öte yandan, geti2()etkin bir şekildei2 "ana" bellekten . Bir değişken değişkenin, o anda "ana" bellekte tutulan değerden farklı bir değişkenin yerel bir kopyasına sahip olmasına izin verilmez. Etkili olarak, uçucu olarak bildirilen bir değişkenin verileri tüm evrelerde senkronize edilmiş olması gerekir, böylece herhangi bir evrede değişkene her eriştiğinizde veya güncellediğinizde, diğer tüm evreler hemen aynı değeri görür. Genellikle uçucu değişkenler "düz" değişkenlerden daha yüksek erişim ve güncelleme yüküne sahiptir. Genel olarak iş parçacıklarının daha iyi verimlilik için kendi veri kopyalarına sahip olmasına izin verilir.

Volitil ve senkronize arasında iki fark vardır.

Öncelikle senkronize edilmiş monitörlerde, kod bloğu yürütmek için bir kerede yalnızca bir iş parçacığını zorlayabilen kilitler alır ve serbest bırakır. Senkronize edilmesinin oldukça iyi bilinen yönü budur. Ancak senkronize edilmiş bellek de senkronize edilir. Aslında senkronize edilmiş evre belleğinin tamamını "ana" bellekle senkronize eder. Böylece yürütme geti3()aşağıdakileri yapar:

  1. İş parçacığı bunun için monitör üzerindeki kilidi alır.
  2. İş parçacığı belleği tüm değişkenlerini temizler, yani tüm değişkenlerini "ana" bellekten etkili bir şekilde okur.
  3. Kod bloğu yürütülür (bu durumda dönüş değerini, "ana" bellekten sıfırlanmış olabilen mevcut i3 değerine ayarlamak).
  4. (Değişkenlerde yapılan herhangi bir değişiklik artık normalde "ana" belleğe yazılır, ancak geti3 () için herhangi bir değişikliğimiz yoktur.)
  5. İş parçacığı bunun için monitör üzerindeki kilidi serbest bırakır.

Dolayısıyla, uçucunun yalnızca iş parçacığı belleği ile "ana" bellek arasındaki bir değişkenin değerini senkronize ettiği durumlarda, senkronize iş parçacığı belleği ile "ana" bellek arasındaki tüm değişkenlerin değerini senkronize eder ve önyükleme yapmak için bir monitörü kilitler ve serbest bırakır. Açık bir şekilde senkronize edilenlerin uçucudan daha fazla yükü vardır.

http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html


35
-1, Uçucu bir kilit almaz, yazdıktan sonra tüm iş parçacıkları arasında görünürlük sağlamak için temeldeki CPU mimarisini kullanır.
Michael Barker

Yazmaların atomikliğini garanti etmek için bir kilidin kullanılabileceği bazı durumlar olabileceğini belirtmek gerekir. Örneğin, genişletilmiş genişlik haklarını desteklemeyen 32 bitlik bir platformda uzun yazma. Intel, uçucu uzunlıkları işlemek için SSE2 kayıtlarını (128 bit genişlik) kullanarak bunu önler. Bununla birlikte, bir uçucunun kilit olarak kabul edilmesi büyük olasılıkla kodunuzda kötü hatalara yol açacaktır.
Michael Barker

2
Uçucu değişkenlerin kilitleri tarafından paylaşılan önemli semantik, her ikisinin de Happens-Before kenarları sağlamasıdır (Java 1.5 ve üstü). Senkronize bir bloğa girmek, bir kilidi çıkarmak ve bir uçucudan okumak, bir "edinme" olarak kabul edilir ve bir kilidin serbest bırakılması, senkronize edilmiş bir bloktan çıkılması ve bir uçucunun yazılması, bir "serbest bırakma" biçimidir.
Michael Barker

20

synchronizedyöntem seviyesi / blok seviyesi erişim kısıtlaması değiştiricisidir. Kritik bölüm için bir dişin kilide sahip olduğundan emin olacaktır. Sadece bir kilidi olan iplik synchronizedbloğa girebilir . Diğer iş parçacıkları bu kritik bölüme erişmeye çalışıyorsa, geçerli sahibin kilidi serbest bırakmasını beklemek zorundalar.

volatiletüm iş parçacıklarını ana bellekten değişkenin en son değerini almaya zorlayan değişken erişim değiştiricisidir. volatileDeğişkenlere erişmek için kilitleme gerekmez . Tüm evreler, değişken değişken değerine aynı anda erişebilir.

Uçucu değişken: Datedeğişken kullanmak için iyi bir örnek .

Tarih değişkeni yaptığınızı varsayın volatile. Bu değişkene erişen tüm evreler her zaman ana bellekten en son verileri alır, böylece tüm evreler gerçek (gerçek) Tarih değerini gösterir. Aynı değişken için farklı zaman gösteren farklı iş parçacıklarına ihtiyacınız yoktur. Tüm evreler doğru Tarih değerini göstermelidir.

resim açıklamasını buraya girin

Kavramın daha iyi anlaşılması için bu makaleye bir göz atın volatile.

Lawrence Dol açıkladı senin read-write-update query.

Diğer sorgularınızla ilgili

Değişkenleri eşzamanlı olarak erişmek yerine değişken olarak bildirmek ne zaman daha uygundur?

volatileTüm iş parçacıklarının, Date değişkeni için açıkladığım örnek gibi, gerçek zamanlı olarak değişkenin gerçek değerini alması gerektiğini düşünüyorsanız kullanmanız gerekir.

Girdi bağımlı değişkenler için uçucu kullanmak iyi bir fikir mi?

Cevap ilk sorgudakiyle aynı olacaktır.

Daha iyi anlamak için bu makaleye bakın .


Böylece okuma aynı anda gerçekleşebilir ve CPU ana belleği CPU iş parçacığı önbelleğine önbelleğe almadığı için tüm iş parçacığı en son değeri okuyacaktır, ama yazmaya ne dersiniz? Yazma eşzamanlı olmamalı mı? İkinci soru: eğer bir blok senkronize edilmiş, ancak değişken uçucu değilse, senkronize bloktaki bir değişkenin değeri başka bir kod bloğundaki başka bir evre tarafından hala değiştirilebilir mi?
the_prole

11

tl; dr :

Çoklu kullanım ile ilgili 3 ana sorun vardır:

1) Yarış Koşulları

2) Önbellek / eski bellek

3) Complier ve CPU optimizasyonları

volatile2 ve 3'ü çözebilir, ancak çözemez 1. synchronized/ açık kilitler 1, 2 ve 3'ü çözebilir.

Detaylandırma :

1) Bu iş parçacığının güvensiz kodunu düşünün:

x++;

Bir işlem gibi görünse de, aslında 3: bellekteki x'in mevcut değerini okumak, ona 1 eklemek ve tekrar belleğe kaydetmek. Birkaç iş parçacığı aynı anda yapmaya çalışırsa, işlemin sonucu tanımsızdır. xBaşlangıçta 1 olsaydı , kodu işleten 2 iplikten sonra 2 olabilir ve kontrolün hangi işlemin hangi kısmının tamamlandığına bağlı olarak 3 olabilir ve kontrolün diğer dişe aktarılmasından önce. Bu bir tür yarış koşulu .

synchronizedBir kod bloğunda kullanmak onu atomik hale getirir - yani 3 işlem bir kerede gerçekleşir gibi yapar ve başka bir iş parçacığının ortada gelip müdahale etmesinin bir yolu yoktur. Yani eğer x1 idi ve 2 konu ürününün için deneyin x++biz biliyoruz o yarış durumu sorunu çözer Yani 3'e eşit olacak sonunda.

synchronized (this) {
   x++; // no problem now
}

İşaretleme xolarak volatileyapmaz x++;, bu sorunu çözmez, böylece atom.

2) Ayrıca, evrelerin kendi bağlamları vardır - yani ana bellekteki değerleri önbelleğe alabilirler. Bu, birkaç iş parçacığının bir değişkenin kopyalarına sahip olabileceği, ancak değişkenin yeni durumunu diğer iş parçacıkları arasında paylaşmadan çalışma kopyalarında çalıştıkları anlamına gelir.

Bunu bir iş parçacığında düşünün x = 10;. Ve bir süre sonra, başka bir iş parçacığında x = 20;. xDiğer iş parçacığı yeni değeri çalışma belleğine kaydettiğinden ancak ana belleğe kopyalamadığından , değerindeki değişiklik ilk iş parçacığında görünmeyebilir. Ya da ana belleğe kopyaladığını, ancak ilk iş parçacığı çalışma kopyasını güncellemediğini. Yani şimdi ilk iş parçacığı kontrol ederse if (x == 20)cevap olacaktır false.

Bir değişkeni volatiletemel olarak işaretlemek , tüm iş parçacıklarına yalnızca ana bellekte okuma ve yazma işlemleri yapmasını söyler. synchronizedher iş parçacığına, bloğa girdiklerinde değerlerini ana bellekten güncellemelerini söyler ve bloktan çıktıklarında sonucu ana belleğe temizler.

Veri yarışlarından farklı olarak, ana belleğe basmalar yine de oluştuğundan eski belleğin (yeniden) üretilmesi o kadar kolay değildir.

3) Complier ve CPU (iş parçacıkları arasında herhangi bir senkronizasyon biçimi olmadan) tüm kodu tek iş parçacıklı olarak ele alabilir. Yani, bazı kodlara bakabilir, bu çok iş parçacıklı bir açıdan çok anlamlıdır ve tek dişli gibi, çok anlamlı olmadığı gibi davranır. Bu nedenle, bir koda bakabilir ve optimizasyon amacıyla, bu kodun birden fazla iş parçacığında çalışacak şekilde tasarlanmadığını bilmiyorsa, yeniden sıralamaya ve hatta parçalarını tamamen kaldırmaya karar verebilir.

Aşağıdaki kodu göz önünde bulundurun:

boolean b = false;
int x = 10;

void threadA() {
    x = 20;
    b = true;
}

void threadB() {
    if (b) {
        System.out.println(x);
    }
}

ThreadB yalnızca 20 bolarak bayarlandıktan sonra true olarak ayarlandığından , threadB'nin yalnızca 20 yazdırabileceğini (veya threadB doğru kontrolü yapılmadan önce threadB yürütülürse hiçbir şey yazdıramayacağını) düşünürsünüz x, ancak derleyici / CPU yeniden sıralamaya karar verebilir threadA, bu durumda threadB da 10. İşaretleme baskı olabilir bolarak volatileonu yeniden sıralanmış (veya bazı durumlarda atılır) olmayacağını garanti eder. Hangi threadB sadece 20 yazdırabilir (ya da hiçbir şey). Yöntemlerin senkronize olarak işaretlenmesi aynı sonucu elde edecektir. Ayrıca bir değişkeni volatileyalnızca yeniden sıralanmamasını sağlar, ancak önceki / sonraki her şey yine de yeniden sıralanabilir, böylece senkronizasyon bazı senaryolarda daha uygun olabilir.

Java 5 Yeni Bellek Modeli'nden önce uçucunun bu sorunu çözmediğini unutmayın.


1
"Bir işlem gibi görünse de, aslında 3: bellekteki x'in mevcut değerini okumak, ona 1 eklemek ve tekrar belleğe kaydetmek." - Doğru, çünkü bellekten alınan değerlerin eklenebilmesi / değiştirilmesi için CPU devresinden geçmesi gerekiyor. Bu sadece tek bir Montaj INCişlemine dönüşse de, temeldeki CPU işlemleri hala 3 kattır ve iplik güvenliği için kilitleme gerektirir. İyi bir nokta. Bununla birlikte, INC/DECkomutlar montajda atomik olarak işaretlenebilir ve yine de 1 atomik işlem olabilir.
Zombiler

@Zombiler, bu yüzden x ++ için senkronize edilmiş bir blok oluşturduğumda, işaretli bir atomik INC / DEC haline mi dönüyor yoksa düzenli bir kilit kullanıyor mu?
David Refaeli

Bilmiyorum! Ne biliyorum INC / DEC atomik değildir çünkü bir CPU için, değeri yüklemek ve OKUMA ve aynı zamanda (hafızaya) yazmak zorunda, tıpkı diğer aritmetik işlemler gibi.
Zombies
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.