Java'da çok iş parçacıklı programlama söz konusu olduğunda, iş parçacığı güvenliği atlanmamalıdır. İş parçacığı güvenliği atomiklik, görünürlük ve sıra gibi özellikleri içerir. Bugün, aralarındaki bağlantıya ve gerçekleştirme ilkesine bir göz atalım.
Konular ve yarış koşulları
Geliştirilen uygulama bir süreç içinde çalışacaktır, diğer bir deyişle süreç, programın çalışan örneğidir. Bir Java programını çalıştırmanın özü, bir Java sanal makine sürecini çalıştırmaktır.
Bir işlem birden fazla iş parçacığı içerebilirse ve bu iş parçacıkları işlemdeki kaynakları paylaşır. Herhangi bir kod parçası tek bir iş parçacığında çalışacak ve aynı zamanda birden çok iş parçacığında çalışacaktır. Bir iş parçacığı tarafından tamamlanacak hesaplamalara görevler denir.
Programın verimliliğini artırmak için, birlikte çalışmak için birden fazla görev oluşturacağız.Bu çalışma modu paralel olabilir.A görevi yürütülürken, B görevi de yürütülür.
Yürütme sırasında birden fazla görev (iş parçacığı) aynı kaynak (değişken) üzerinde çalışıyorsa, bu kaynağa (değişken) paylaşılan kaynak (değişken) denir.
Birden çok iş parçacığı aynı anda paylaşılan kaynaklar üzerinde çalıştığında, örneğin: kaynak yazma, çalıştırılan kaynakların farklı zamanlarda farklı sonuçlar görmesine neden olacak bir yarış koşulu olacaktır.
Aşağıdaki gibi aynı kaynağa / değişkene erişen birden çok iş parçacığı örneğini görelim:
A ve B iş parçacıkları, iki iş parçacığı arasında nasıl geçiş yapılacağını bilmeden aynı anda Counter nesnesinde add () yöntemini çalıştırdığında, JVM kodu aşağıdaki sırayla çalıştıracaktır:
Yukarıdaki işlemler A ve B evreleri arasına eklendiğinde, aşağıdaki durumlar ortaya çıkacaktır:
İki iş parçacığı, count değişkenine sırasıyla 2 ve 3 ekler İstenen sonuç, iki iş parçacığının yürütülmesinden sonra count değerinin 5'e eşit olmasıdır.
Bununla birlikte, iki iş parçacığı tarafından bellekten okunan başlangıç değeri 0 olsa bile, sırayla 2 ve 3 eklenir ve sırasıyla belleğe geri yazılır.
Bununla birlikte, son değer beklenen 5 değil, belleğe en son yazılan iş parçacığının (A iş parçacığı) değeridir (3).
Hafızaya son yazma A evresi olduğundan sonuç 3'tür, ancak B evresinin belleğe en son yazması da mümkündür, bu nedenle sonuç bilinemez.
Bu nedenle, senkronizasyon mekanizması kullanılmazsa, iş parçacıkları arasındaki çapraz yazma kaynakları / değişkenleri kontrol edilemez olacaktır.
Bir hesaplama sonucunun doğruluğunun zamanla ilişkili olduğu bu fenomeni Yarış Koşulu olarak adlandırıyoruz.
İş parçacığı güvenli
Daha önce, birden çok iş parçacığının aynı anda bir kaynak / değişken yazdığı yarış koşullarından bahsetmiştik. Bu durumun ortaya çıkması nihai sonuçta belirsizliğe neden olacaktır.
Yazılı kaynak Java'da bir sınıf olarak kabul edilirse, bu sınıf iş parçacığı açısından güvenli değildir.
Bu sınıf tek iş parçacıklı bir ortamda normal olarak çalışsa bile, çok iş parçacıklı bir ortamda normal şekilde çalışamaz. Örneğin: ArrayList, HashMap, SimpledateFormat.
Ardından, iplik güvenliğini sağlamak için aşağıdaki üç hususu dikkate almanız gerekir, yani:
Atomiklik
Atomiklik, belirli bir işlemin veya birden fazla işlemin tamamının yürütüldüğü veya yürütülmediği ve hiçbir ara işlemin gerçekleşmeyeceği anlamına gelir. Aynı durum iş parçacığı yürütmesine geçerken de geçerlidir Evre içinde yürütülen işlemler ya yürütülmez ya da hepsi yürütülür.
Örneğin, Java'da temel veri türü okuma işlemi atomik bir işlemdir, aşağıdaki ifadeye bakın:
İlk cümle atomik bir işlemdir, çünkü 10 değerini doğrudan x'e atar ve evre bu cümleyi yürüttüğünde, doğrudan belleğe 10 yazar.
İkinci cümle üç işlem içerir, x'in değerini okuyun, 1 işlem ekleyin ve yeni bir değer yazın. Bu üç işlem birlikte atomik işlemler değildir.
Java'daki atomlar şunları içerir:
Farz edelim ki A ve B ifadeleri 2'yi birlikte çalıştırıyor ve x değişkenine aynı anda yazıyor.Atomik işlem tatmin olmadığından, elde edilen sonuç da belirsiz. Java'da atomikliğe ulaşmanın iki yolu vardır. Biri kilit kullanmaktır (Kilit).
Kilit özeldir. Birden fazla iş parçacığı tarafından erişildiğinde, paylaşılan bir değişkene herhangi bir zamanda bir iş parçacığı tarafından erişilebileceğini garanti edebilir, bu da yarış koşulları olasılığını ortadan kaldırır.
Diğeri, doğrudan donanım (işlemci ve bellek) katmanında uygulanan ve "donanım kilidi" olarak adlandırılan, işlemci tarafından sağlanan CAS (Karşılaştır ve Değiştir) talimatını kullanarak gerçekleştirilir.
Görünürlük
Atomiklik hakkında konuştuktan sonra, görünürlükten bahsedelim. Adından da anlaşılacağı gibi, çok iş parçacıklı erişimde, bir iş parçacığı paylaşılan bir değişkeni güncelledikten sonra, sonraki erişim iş parçacığı güncellenen sonucu hemen okuyabilir.
Bu duruma görünürlük denir ve paylaşılan değişkenlerde yapılan güncellemeler diğer iş parçacıkları tarafından görülebilir, aksi takdirde görünmez olarak adlandırılır.
Java'nın nasıl görünürlük elde ettiğini görelim. İlk olarak, A evresinin bir komut yürüttüğünü ve bu komutun CPU A'da çalıştığını varsayalım.
Bu komutla yazılan paylaşılan değişken, CPU A'nın kaydında saklanacaktır. Komut yürütüldüğünde, paylaşılan değişken kayıttan belleğe yazılacaktır.
Not: Aslında, kayıt yoluyla önbelleğe, ardından yazma arabelleğine ve düzensiz kuyruğa ve son olarak da belleğe.
Örnek olarak, basitleştirilmiş bir ifade kullanılmıştır. Bunun amacı, paylaşılan değişkenlere başka bir CPU'daki iş parçacıkları tarafından erişilmesine izin vermektir.
Bu nedenle, paylaşılan değişkenleri belleğe yazma eylemine "işlemci önbelleğini temizleme" denir. Yani, paylaşılan değişkenleri işlemci önbelleğinden belleğe boşaltın.
İşlemci önbelleğini temizleyin
Şu anda, B iş parçacığı B CPU üzerinde çalışıyor olur. Paylaşılan değişkenleri elde etmek için, komutun bellekteki paylaşılan değişkenlerden senkronize edilmesi gerekir.
Bu önbellek eşitleme işlemine "işlemci önbelleğini temizleme" denir. Yani, önbelleği bellekten işlemcinin kayıtlarına boşaltın.
Bu iki adımdan sonra, CPU B üzerinde çalışan iş parçacığı, CPU A üzerindeki iş parçacığı tarafından işlenen paylaşılan değişkenle eşitlenebilir. Aynı zamanda paylaşılan değişkenlerin görünürlüğünü de garanti eder.
İşlemci önbelleğini temizleyin
Düzenlilik
Görünürlük hakkında konuştuktan sonra siparişten bahsedelim. Java derleyicisi kod yürütme sırasını ayarlayacaktır.
İki işlemin yürütme sırasını değiştirmek mümkündür.Başka bir işlemcide gerçekleştirilen birden çok işlem için, diğer işlemciler açısından, talimatların yürütme sırası da tutarsız olabilir.
Java bellek modelinde, derleyici ve işlemcinin talimatları yeniden sıralamasına izin verilir, ancak yeniden sıralama süreci tek iş parçacıklı programların yürütülmesini etkilemeyecektir, ancak birden çok iş parçacığının eşzamanlı yürütülmesinin doğruluğunu etkileyecektir.
Bunun nedeni, performansla ilgili hususlar için, derleyicinin programın doğruluğunu etkilemeden kaynak kod sırasını ayarlamasıdır (tek iş parçacıklı program).
Java'da "düzenliliği" sağlamak için Volatile anahtar sözcüğünü kullanabilirsiniz. Sırayı sağlamak için Senkronize ve Kilit'i de kullanabilirsiniz. Daha sonra, bellek engelleri üzerinden sipariş vermeye başlayacağız.
Çok iş parçacıklı senkronizasyon ve kilit
Daha önce bahsedilen iş parçacığı yarışı ve iş parçacığı güvenliği, paylaşılan değişkenlere çok iş parçacıklı erişim etrafında tartışılmıştır. Tam da bu durum nedeniyle, çok iş parçacıklı geliştirme geliştirilirken bu sorunun çözülmesi gerekiyor.
İş parçacığı güvenliğini sağlamak için, birden çok iş parçacığının eşzamanlı erişimi seri erişime dönüştürülecektir. Lock, çok iş parçacıklı senkronizasyonu sağlamak için bu fikri kullanır.
Kilit, paylaşılan veri erişimi için bir lisans gibidir. Herhangi bir iş parçacığının paylaşılan verilere erişmeden önce bu kilidi alması gerekir.
Bir iş parçacığı bir kilit aldığında, kilit için geçerli olan diğer iş parçacıklarının beklemesi gerekir. Kilidi alan iş parçacığı, iş parçacığı üzerindeki görevlere göre kodu yürütecek ve kod yürütülene kadar kilit serbest bırakılmayacaktır.
Kilidin alınması ile kilidin serbest bırakılması arasında yürütülen kod alanına kritik bölüm, kritik bölümde erişilen verilere ise paylaşılan veriler denir.
İş parçacığı kilidi serbest bıraktıktan sonra, diğer iş parçacıkları kilidi elde edebilir ve ardından paylaşılan veriler üzerinde çalışabilir.
Kritik bölüme çok iş parçacıklı erişim, paylaşılan verilere erişim
Yukarıda açıklanan işlemler aynı zamanda birbirini dışlayan işlemler olarak da adlandırılır ve kilitler, bu birbirini dışlayan işlem aracılığıyla yarış koşullarının atomikliğini garanti eder.
Daha önce bahsedilen atomikliği hatırlıyor musunuz? Paylaşılan verilerdeki bir veya daha fazla işlem tamamlandı veya tamamlanmadı ve hiçbir ara durum görünmüyor.
Kritik bölümdeki kodun atomik olmadığını varsayın. Örneğin, yukarıda bahsedilen "x = x + 1" üç işlemi içerir, x'in değerini okuyun, 1 ekleme işlemini gerçekleştirin ve yeni bir değer yazın.
Birden çok iş parçacığında erişilirse, çalışma süresine bağlı olarak farklı sonuçlar elde edilecektir. Bu işleme bir kilit eklerseniz, onu "atomik" yapabilirsiniz, yani bir iş parçacığı ona eriştiğinde, diğer evreler ona erişemez.
Çok iş parçacıklı geliştirmede kilitlerin önemi hakkında konuştuktan sonra, Java'daki kilit türlerine bir göz atalım.
Dahili kilit
Dahili kilitler, yöntemleri değiştirmek için Senkronize anahtar sözcüğünü kullanan ve kritik bölümler oluşturmak için kod bloklarını kullanan monitörler olarak da adlandırılır.
Eşitlenmiş anahtar sözcük, eşitlenmiş yöntemleri, eşitlenmiş statik yöntemleri, eşitlenmiş örnek yöntemlerini ve eşitlenmiş kod bloklarını değiştirmek için kullanılabilir.
Senkronize tarafından başlatılan kod bloğu, yukarıda bahsedilen kritik bölümdür. Kilit tutamacı bir nesneye bir referanstır.Örneğin, mevcut nesneyi belirtmek için this anahtar sözcüğü olarak yazılabilir.
Kilit koluna karşılık gelen monitör, karşılık gelen senkronizasyon bloğunun kılavuz kilidi olarak adlandırılır ve karşılık gelen senkronizasyon bloğuna, kilit tarafından yönlendirilen senkronizasyon bloğu adını veririz.
Dahili kilit şeması
Kilit kolu genellikle sonlandırılır (özel son). Çünkü kilit kolu değiştiğinde, aynı kod bloğunun birden çok iş parçacığı farklı kilitler kullanır ve bu da yarış koşullarına yol açar.
Senkronize statik yöntem, geçerli sınıfı önyükleme kilidi olan senkronize edilmiş bloğa eşdeğerdir. Bir iş parçacığı kritik bir bölümde kodu yürüttüğünde, kritik bölümün önyükleme kilidini tutmalıdır.
Kritik bölüm kodu yürütüldüğünde, kritik bölümü yönlendiren kilit serbest bırakılacaktır. Dahili kilit uygulaması ve bırakma işlemi Java sanal makinesi tarafından tamamlanır.
Bu nedenle, Senkronize tarafından uygulanan kilitlere dahili kilitler denir. Bu nedenle, kilit sızmaz.Java derleyicisi kod bloğunu bayt koduna derlediğinde, kritik bölüm tarafından atılan istisnayı işler.
Java sanal makinesi, her dahili kilide, kilidi almayı bekleyen iş parçacıklarını kaydetmek için kullanılan bir giriş kümesi atar. Kilit için uygulanamayan iş parçacığı, giriş kümesinde kilit için tekrar başvurma fırsatı için bekleyecektir.
Bekleme kilidi diğer iş parçacıkları tarafından serbest bırakıldığında, giriş kümesindeki bekleyen iş parçacığı uyanacak ve kilide başvurma fırsatı elde edilecektir.
Dahili kilit mekanizması, bekleyen iş parçacıkları arasından seçilecektir.Seçim kuralları, iş parçacığı etkinliğine ve önceliğe dayalı olacaktır.Seçilen iş parçacığı, sonraki işlemler için kilidi tutacaktır.
Kilidi göster
Ekran kilidi, JDK 1.5'te tanıtılan özel bir kilittir.İş parçacığı senkronizasyon mekanizması olarak mevcuttur.İşlevi dahili kilitle aynıdır, ancak dahili kilidin sahip olmadığı bazı özellikler sağlar. Görüntü kilidi, java.util.concurrent.locks.Lock arabiriminin bir örneğidir.
Ekran kilidi gerçekleştirme adımları şunlardır:
Kilit kullanımı örnek diyagramını göster
Ekran kilidi hem adil olmayan hem de adil kilitleri destekler. Adil kilitlemede, iş parçacıkları sıkı ilk giren ilk çıkar (FIFO) sırasına göre kilit kaynaklarını alır.
Paylaşılan değişkenleri elde etmesi gereken bir "geçerli iş parçacığı" varsa, sıraya alınması gerekir. Kilit serbest bırakıldığında, kuyruktaki ilk iş parçacığı (Düğüm1) tarafından alınır ve bu böyle devam eder.
Adil kilidin şematik diyagramı
Haksız bir kilitlemede, bir iş parçacığı kilidi serbest bıraktığında, "geçerli iş parçacığı", kilit kaynakları için bekleme kuyruğundaki ilk iş parçacığı (Düğüm1) ile rekabet eder. Hangi iş parçacığının kilit kaynaklarını tuttuğunu iş parçacığı etkinliğine ve önceliğine göre belirleyin.
Haksız kilit şeması
Adil kilitler, kilit zamanlamasının adil olmasını sağlar, ancak iş parçacığı askıya alma ve uyanma olasılığını artırır, yani bağlam değiştirme maliyetini artırır. Haksız kilitler, daha iyi performansa sahip olacak ve daha yüksek verim sağlayabilecek bir rekabet mekanizması ekledi.
Elbette haksız kilitler, kilidi elde etme süresini daha belirsiz hale getirir ve bu da engelleme kuyruğundaki iş parçacıklarının uzun süre aç kalmasına neden olabilir.
İş parçacığı senkronizasyon mekanizması: bellek engeli
Paylaşılan değişkenlere çok iş parçacıklı erişimden, yarış koşullarının varlığından bahsetmişken ve ardından bu sorunu çözmek için bir kilit mekanizması tanıtın.
Yukarıda bahsedilen dahili kilitler ve ekran kilitleri, iplik senkronizasyonu problemini çözmek için ve yarış koşullarında "atomisite" problemini çözdüğünden bahsetmiştir.
Bundan sonra, "görünürlük" ve "düzen" e nasıl ulaşılacağını anlamak için bellek bariyer mekanizmasını tanıtarak.
İşte bellek engeli kavramı: Yürütme için iki CPU talimatı arasına bellek engeli yerleştirilmiştir. Düzen ve görünürlüğü sağlamak için derleyici ve işlemcinin yeniden sıralanmasını engellemek için kullanılır.
Görünürlük için, bir iş parçacığı bir kilit aldığında ve serbest bıraktığında gerçekleştirilen iki eylemden bahsettik: "işlemci önbelleğini temizle" ve "işlemci önbelleğini temizle".
İlk eylem, kilidi tutan ipliğin paylaşılan değişkeni okumasını garanti eder ve ikinci eylem, kilidi tutan ipliğin, paylaşılan değişken güncellendikten sonra sonraki iş parçacıkları tarafından görülebileceğini garanti eder.
Ayrıca engelin etkisini elde etmek için işlemcinin değeri yazıp okumadan önce ana belleğin değerini önbelleğe yazmasına ve görünürlük sağlamak için geçersiz kuyruğu temizlemesine neden olacaktır.
Düzen için. Açıklamak için bir örnek alalım, bir dizi CPU talimatı olduğunu varsayalım:
MağazaYükleme bellek bariyer diyagramı
StoreLoad bariyerinden önceki Store komutu, StoreLoad bariyerinden sonra, yani yeniden sıralama ile Load komutu ile pozisyon değiştiremez.
Ancak StoreLoad bariyerinden önceki ve sonraki talimatlar değiştirilebilir, yani Store1 Store2 ile değiştirilebilir ve Load2 Load3 ile değiştirilebilir.
4 ortak engel vardır:
Genellikle, Uçucu ve Eşitlenmiş anahtar sözcükler Java'da bellek engellerini uygulamak için yaygın olarak kullanılır ve Güvenli Olmayan aracılığıyla da uygulanabilir.
sonuç olarak
İş parçacığı tanımından ve paylaşılan değişkenlere çok iş parçacıklı erişimden başlayarak, kaynaklar için iş parçacığı rekabeti olgusu yaşanmıştır. Yarış koşulları, kaynaklara çok iş parçacıklı erişim sağlayacak, zaman geçtikçe kaynakların sonuçları kontrol edilemez.
Bu, çok iş parçacıklı programlamamız için bir zorluk oluşturmaktadır. Bu nedenle, iplik güvenliği sorununu atomiklik, görünürlük ve düzen yoluyla çözmemiz gerekiyor.
Paylaşılan kaynakların senkronizasyonu bu sorunları çözebilir, Java, çözüm olarak en iyi dahili kilit ve ekran kilidi uygulamasını sağlar.
Sonunda, iş parçacığı senkronizasyonunun altında yatan mekanizmayı tanıtır: bellek engeli. CPU talimatlarının yeniden sıralanmasını organize ederek görünürlük ve düzen sorunlarını çözer.