MERHABA! Multithreading çalışmamıza devam ediyoruz.
volatile
Bugün anahtar kelimeyi ve yöntemi öğreneceğiz yield()
. Hadi dalalım :)
uçucu anahtar kelime
Çok iş parçacıklı uygulamalar oluştururken iki ciddi sorunla karşılaşabiliriz. Birincisi, çok iş parçacıklı bir uygulama çalışırken, farklı iş parçacıkları değişkenlerin değerlerini önbelleğe alabilir (bu konudan daha önce 'Using volatile' başlıklı derste bahsetmiştik ). Bir iş parçacığının bir değişkenin değerini değiştirdiği, ancak ikinci iş parçacığının değişkenin önbelleğe alınmış kopyasıyla çalıştığı için değişikliği görmediği bir duruma sahip olabilirsiniz . Doğal olarak, sonuçlar ciddi olabilir. Diyelim ki bu sadece herhangi bir eski değişken değil, birdenbire rasgele yukarı ve aşağı zıplamaya başlayan banka hesap bakiyeniz :) Bu kulağa eğlenceli gelmiyor, değil mi? İkincisi, Java'da tüm ilkel türleri okuma ve yazma işlemleri,long
double
, atomiktir. Örneğin, bir iş parçacığındaki bir değişkenin değerini değiştirirseniz int
ve başka bir iş parçacığında değişkenin değerini okursanız, ya eski değerini alırsınız ya da yenisini, yani değişiklikten kaynaklanan değeri alırsınız. iş parçacığında 1. "Ara değerler" yoktur. long
Ancak, bu s ve s ile çalışmaz double
. Neden? Platformlar arası destek nedeniyle. Başlangıç seviyelerinde Java'nın yol gösterici ilkesinin 'bir kez yaz, her yerde çalıştır' olduğunu söylediğimizi hatırlıyor musunuz? Bu, platformlar arası destek anlamına gelir. Başka bir deyişle, bir Java uygulaması her türlü farklı platformda çalışır. Örneğin, Windows işletim sistemlerinde, Linux veya MacOS'un farklı sürümleri. Hepsinde aksamadan çalışacaktır. 64 bit tartım,long
double
Java'daki 'en ağır' ilkel öğelerdir. Ve bazı 32-bit platformlar, 64-bit değişkenlerin atomik okumasını ve yazılmasını uygulamazlar. Bu tür değişkenler iki işlemde okunur ve yazılır. Önce değişkene ilk 32 bit yazılır ve ardından 32 bit daha yazılır. Sonuç olarak, bir sorun ortaya çıkabilir. Bir iş parçacığı, bir değişkene 64 bitlik bir değer yazar X
ve bunu iki işlemde yapar. Aynı zamanda, ikinci bir iş parçacığı değişkenin değerini okumaya çalışır ve bunu iki işlem arasında yapar - ilk 32 bit yazılırken ikinci 32 bit yazılmaz. Sonuç olarak, orta, yanlış bir değer okuyor ve bir hatamız var. Örneğin böyle bir platformda 9223372036854775809 numarasına yazmaya çalışırsak bir değişkene, 64 bit kaplar. İkili biçimde, şöyle görünür: 100000000000000000000000000000000000000000000000000000000001 İlk iş parçacığı sayıyı değişkene yazmaya başlar. İlk başta, ilk 32 biti (1000000000000000000000000000000) ve ardından ikinci 32 biti (000000000000000000000000000001) yazar. Ve ikinci iş parçacığı, değişkenin halihazırda yazılmış olan ilk 32 bit olan ara değerini (10000000000000000000000000000000) okuyarak bu işlemler arasında sıkışabilir. Ondalık sistemde bu sayı 2.147.483.648'dir. Yani sadece 9223372036854775809 sayısını bir değişkene yazmak istedik ama bu işlem bazı platformlarda atomik olmadığı için elimizde birdenbire ortaya çıkan ve bilinmeyen bir etkisi olacak olan 2,147,483,648 numaralı kötü numaramız var. programı. İkinci iş parçacığı, yazılması bitmeden önce basitçe değişkenin değerini okudu, yani iş parçacığı ilk 32 biti gördü, ancak ikinci 32 biti görmedi. Elbette bu sorunlar dün ortaya çıkmadı. Java bunları tek bir anahtar kelime ile çözer: volatile
. Eğer kullanırsakvolatile
programımızda bazı değişkenleri bildirirken anahtar kelime…
public class Main {
public volatile long x = 2222222222222222222L;
public static void main(String[] args) {
}
}
…Bu demektir:
- Her zaman atomik olarak okunacak ve yazılacaktır. 64 bit
double
veyalong
. - Java makinesi onu önbelleğe almaz. Böylece 10 iş parçacığının kendi yerel kopyalarıyla çalıştığı bir durumla karşılaşmazsınız.
verim() yöntemi
Sınıfın birçok yöntemini zaten inceledikThread
, ancak sizin için yeni olacak önemli bir yöntem var. yield()
Yöntem bu . Ve tam olarak adının ima ettiği şeyi yapar! Yöntemi bir iş parçacığında çağırdığımızda yield
, aslında diğer iş parçacıklarıyla konuşur: 'Hey millet. Herhangi bir yere gitmek için özellikle acelem yok, bu yüzden herhangi birinizin işlemci süresi alması önemliyse, kabul edin - bekleyebilirim'. İşte bunun nasıl çalıştığına dair basit bir örnek:
public class ThreadExample extends Thread {
public ThreadExample() {
this.start();
}
public void run() {
System.out.println(Thread.currentThread().getName() + " yields its place to others");
Thread.yield();
System.out.println(Thread.currentThread().getName() + " has finished executing.");
}
public static void main(String[] args) {
new ThreadExample();
new ThreadExample();
new ThreadExample();
}
}
Sırayla üç iş parçacığı oluşturup başlatıyoruz: Thread-0
, Thread-1
, ve Thread-2
. Thread-0
önce başlar ve hemen diğerlerine yol verir. Sonra Thread-1
başlar ve ayrıca verir. Sonra Thread-2
başlar, bu da verir. Başka ileti dizimiz yok ve Thread-2
en sondaki yerini verdikten sonra, ileti dizisi planlayıcı, 'Hmm, başka yeni ileti dizisi yok' diyor. Sırada kimler var? Daha önce yerini kim verdi Thread-2
? Görünüşe göre öyleydi Thread-1
. Tamam, bu, çalışmasına izin vereceğimiz anlamına geliyor'. Thread-1
işini tamamlar ve ardından iş parçacığı planlayıcı koordinasyonuna devam eder: 'Tamam, Thread-1
bitti. Kuyrukta bizden başka kimse var mı?'. Thread-0 sırada: hemen önce yerini verdiThread-1
. Şimdi sırasını alıyor ve tamamlanmak üzere koşuyor. Ardından programlayıcı, dizileri koordine etmeyi bitirir: "Tamam, Thread-2
, diğer dizilere teslim oldunuz ve şimdi hepsi bitti. Son teslim olan sendin, şimdi sıra sende'. Ardından Thread-2
tamamlanmaya çalışır. Konsol çıktısı şu şekilde görünecektir: Thread-0 yerini başkalarına verir Thread-1 yerini başkalarına verir Thread-2 yerini başkalarına verir Thread-1 yürütmeyi bitirdi. Thread-0 yürütmeyi bitirdi. Thread-2'nin yürütülmesi tamamlandı. Elbette, iş parçacığı planlayıcı, iş parçacıklarını farklı bir sırayla başlatabilir (örneğin, 0-1-2 yerine 2-1-0), ancak prensip aynı kalır.
Olur-öncesi kurallar
Bugün değineceğimiz son şey ' önceden olur ' kavramıdır. Bildiğiniz gibi, Java'da iş parçacığı zamanlayıcısı, görevlerini yerine getirmek için iş parçacıklarına zaman ve kaynak ayırma işinin büyük bölümünü gerçekleştirir. Ayrıca iş parçacıklarının genellikle tahmin edilmesi imkansız olan rastgele bir sırayla nasıl yürütüldüğünü defalarca gördünüz. Ve genel olarak, daha önce yaptığımız 'sıralı' programlamadan sonra, çok iş parçacıklı programlama rastgele bir şeye benziyor. Çok iş parçacıklı bir programın akışını kontrol etmek için bir dizi yöntem kullanabileceğinize zaten inanmaya başladınız. Ancak Java'da çoklu iş parçacığı kullanımının bir ayağı daha vardır - 4 " önceden olur " kuralı. Bu kuralları anlamak oldukça basittir. İki iş parçacığımız olduğunu hayal edin -A
veB
. Bu iş parçacıklarının her biri işlemleri gerçekleştirebilir 1
ve 2
. Her kuralda, ' A, B'den önce olur ' dediğimizde , iş parçacığı tarafından A
işlemden önce yapılan tüm değişikliklerin ve bu işlemden kaynaklanan değişikliklerin, işlem yapıldığında ve sonrasında iş parçacığı 1
tarafından görülebildiğini kastediyoruz . Her kural, çok iş parçacıklı bir program yazdığınızda, belirli olayların diğerlerinden %100 önce gerçekleşeceğini ve iş parçacığının işlem sırasında iş parçacığının işlem sırasında yaptığı değişikliklerden her zaman haberdar olacağını garanti eder . Onları gözden geçirelim. B
2
2
B
A
1
Kural 1.
Bir muteksin serbest bırakılması, aynı monitör başka bir iş parçacığı tarafından alınmadan önce gerçekleşir . Bence burada her şeyi anlıyorsun. Bir nesnenin veya sınıfın muteksi bir iş parçacığı tarafından, örneğin, iş parçacığı tarafından alınırsaA
, başka bir iş parçacığı (thread B
) aynı anda onu alamaz. Mutex serbest bırakılana kadar beklemesi gerekir.
Kural 2.
YöntemThread.start()
daha önce gerçekleşir Thread.run()
. Yine, burada zor bir şey yok. Yöntemin içindeki kodu çalıştırmaya başlamak için yöntemi iş parçacığında run()
çağırmanız gerektiğini zaten biliyorsunuz. start()
Spesifik olarak, start yöntemi, run()
yöntemin kendisi değil! Bu kural, çağrılmadan önce ayarlanan tüm değişkenlerin değerlerinin, bir kez başladığında yöntem Thread.start()
içinde görünür olmasını sağlar.run()
Kural 3.
run()
Yöntemin sonu, yöntemden dönüşten önce gerçekleşirjoin()
. İki konu başlığımıza dönelim: A
ve B
. Yöntemi çağırıyoruz join()
, böylece iş parçacığı işini yapmadan önce B
iş parçacığının tamamlanmasını beklemesi garanti edilir . A
Bu, A nesnesinin run()
yönteminin sonuna kadar çalışmasının garanti edildiği anlamına gelir. run()
Ve iş parçacığı yönteminde meydana gelen verilerdeki tüm değişikliklerin, iş parçacığının kendi işine başlayabilmesi için işini bitirmesini bekleyerek bittiğinde A
iş parçacığında görünür olması yüzde yüz garantilidir .B
A
Kural 4.
volatile
Bir değişkene yazmak, aynı değişkenden okumadan önce gerçekleşir . Anahtar kelimeyi kullandığımızda volatile
aslında her zaman o anki değeri elde ederiz. Bir long
veya ile bile double
(burada olabilecek sorunlardan daha önce bahsetmiştik). Zaten anladığınız gibi, bazı başlıklarda yapılan değişiklikler her zaman diğer başlıklar tarafından görülmez. Ancak, elbette, bu tür davranışların bize uymadığı çok sık durumlar vardır. Dizideki bir değişkene bir değer atadığımızı varsayalım A
:
int z;
….
z = 555;
İş parçacığımız B
değişkenin değerini z
konsolda gösterecekse, atanan değeri bilmediği için kolaylıkla 0 görüntüleyebilir. z
Ancak Kural 4, değişkeni olarak bildirirsek volatile
, bir iş parçacığında değerinde yapılan değişikliklerin başka bir iş parçacığında her zaman görünür olacağını garanti eder. volatile
Önceki koddaki kelimeye eklersek ...
volatile int z;
….
z = 555;
B
...o zaman iş parçacığının 0 gösterebileceği durumu engelleriz. volatile
Değişkenlere yazmak, onlardan okumadan önce gerçekleşir.