İş parçacıklarının nasıl etkileşime girdiğine ilişkin ayrıntılara kısa bir genel bakış. Daha önce, iş parçacıklarının birbiriyle nasıl senkronize edildiğini inceledik. Bu sefer thread'ler etkileşime girdiğinde ortaya çıkabilecek problemlere dalacağız ve onlardan nasıl kaçınacağımız hakkında konuşacağız. Daha derinlemesine çalışma için bazı yararlı bağlantılar da sağlayacağız.
Bir JVisualVM eklentisi yüklendiğinde (Araçlar -> Eklentiler yoluyla), kilitlenmenin nerede meydana geldiğini görebiliriz:
hakkında konuştuğumuzda tartıştığımız gibi, park durumuna girer) . Bir livelock örneğini burada görebilirsiniz: Java - Thread Livelock .
Burada süper bir örnek görebilirsiniz: Java - Thread Starvation and Fairness . Bu örnek, açlık sırasında iş parçacıklarına ne olduğunu ve ile arasındaki küçük bir değişikliğin

giriiş
Yani, Java'nın iş parçacıklarına sahip olduğunu biliyoruz. Bunun hakkında Daha İyi Birlikte: Java ve İplik sınıfı başlıklı incelemede okuyabilirsiniz . Bölüm I - Yürütme konuları . Ve birlikte daha iyi: Java ve Thread sınıfı başlıklı incelememizde iş parçacıklarının birbiriyle senkronize olabileceği gerçeğini araştırdık . Kısım II — Senkronizasyon . Dizilerin birbirleriyle nasıl etkileşime girdiği hakkında konuşmanın zamanı geldi. Paylaşılan kaynakları nasıl paylaşırlar? Burada ne gibi sorunlar çıkabilir?
kilitlenme
En korkunç sorun kilitlenmedir. Kilitlenme, iki veya daha fazla iş parçacığının sonsuza kadar diğerini beklemesidir. Kilitlenmeyi açıklayan Oracle web sayfasından bir örnek alacağız :
public class Deadlock {
static class Friend {
private final String name;
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public synchronized void bow(Friend bower) {
System.out.format("%s: %s bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
}
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s bowed back to me!%n",
this.name, bower.getName());
}
}
public static void main(String[] args) {
final Friend alphonse = new Friend("Alphonse");
final Friend gaston = new Friend("Gaston");
new Thread(() -> alphonse.bow(gaston)).start();
new Thread(() -> gaston.bow(alphonse)).start();
}
}
Kilitlenme ilk kez burada oluşmayabilir, ancak programınız askıda kalırsa, çalıştırma zamanı gelmiştir jvisualvm
: 
"Thread-1" - Thread t@12
java.lang.Thread.State: BLOCKED
at Deadlock$Friend.bowBack(Deadlock.java:16)
- waiting to lock <33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
İş parçacığı 1, iş parçacığı 0'dan kilidi bekliyor. Bu neden oluyor? Thread-1
çalışmaya başlar ve Friend#bow
yöntemi yürütür. Anahtar kelime ile işaretlenmiştir , bu da (geçerli nesne) synchronized
için monitörü aldığımız anlamına gelir . this
Metodun girdisi, diğer nesneye bir referanstı Friend
. Şimdi, Thread-1
yöntemi diğerinde yürütmek istiyor Friend
ve bunu yapmak için kilidini alması gerekiyor. Ancak diğer iş parçacığı (bu durumda Thread-0
) yönteme girmeyi başardıysa bow()
, kilit zaten elde edilmiştir Thread-1
veThread-0
ve tersi. Bu çıkmaz çözümsüzdür ve biz buna kilitlenme diyoruz. Serbest bırakılamayan bir ölüm pençesi gibi, kilitlenme de kırılamayan karşılıklı engellemedir. Kilitlenmenin başka bir açıklaması için şu videoyu izleyebilirsiniz: Deadlock ve Livelock Açıklaması .
Canlı kilit
Deadlock varsa livelock da var mı? Evet var :) Canlı kilitlenme, thread'ler dışarıdan canlı gibi göründüğü halde işlerine devam etmeleri için gerekli olan koşul(lar) yerine getirilemediği için hiçbir şey yapamadıklarında gerçekleşir. Temel olarak, canlı kilit, kilitlenmeye benzer, ancak iş parçacıkları bir monitör beklerken "askıda kalmaz". Bunun yerine, sonsuza kadar bir şeyler yapıyorlar. Örneğin:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class App {
public static final String ANSI_BLUE = "\u001B[34m";
public static final String ANSI_PURPLE = "\u001B[35m";
public static void log(String text) {
String name = Thread.currentThread().getName(); // Like "Thread-1" or "Thread-0"
String color = ANSI_BLUE;
int val = Integer.valueOf(name.substring(name.lastIndexOf("-") + 1)) + 1;
if (val != 0) {
color = ANSI_PURPLE;
}
System.out.println(color + name + ": " + text + color);
try {
System.out.println(color + name + ": wait for " + val + " sec" + color);
Thread.currentThread().sleep(val * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Lock first = new ReentrantLock();
Lock second = new ReentrantLock();
Runnable locker = () -> {
boolean firstLocked = false;
boolean secondLocked = false;
try {
while (!firstLocked || !secondLocked) {
firstLocked = first.tryLock(100, TimeUnit.MILLISECONDS);
log("First Locked: " + firstLocked);
secondLocked = second.tryLock(100, TimeUnit.MILLISECONDS);
log("Second Locked: " + secondLocked);
}
first.unlock();
second.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(locker).start();
new Thread(locker).start();
}
}
Bu kodun başarısı, Java iş parçacığı zamanlayıcısının iş parçacıklarını başlatma sırasına bağlıdır. Önce başlarsa Thead-1
, canlı kilit alırız:
Thread-1: First Locked: true
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
Thread-0: Second Locked: true
Thread-0: wait for 1 sec
Thread-1: Second Locked: false
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
...
Örnekten de görebileceğiniz gibi, her iki thread de sırayla her iki kilidi de almaya çalışır, ancak başarısız olurlar. Ancak çıkmazda değiller. Dışarıdan, her şey yolunda ve işlerini yapıyorlar. JVisualVM'ye göre, uyku dönemleri ve bir bekleme süresi görüyoruz (bu, bir iş parçacığının kilit almaya çalıştığı zamandır - daha önce iş parçacığı senkronizasyonu
Açlık
Kilitlenme ve canlı kilitlenmeye ek olarak, çoklu kullanım sırasında meydana gelebilecek başka bir sorun daha vardır: açlık. Bu fenomen, önceki engelleme biçimlerinden farklıdır, çünkü iş parçacıkları engellenmez - sadece yeterli kaynağa sahip değildirler. Sonuç olarak, bazı iş parçacıkları yürütme süresinin tamamını alırken, diğerleri çalışamaz:
https://www.logicbig.com/
Thread.sleep()
yükü Thread.wait()
eşit şekilde dağıtmanıza nasıl izin verdiğini gösterir. 
Yarış koşulları
Çoklu kullanımda "yarış durumu" diye bir şey vardır. Bu fenomen, iş parçacıkları bir kaynağı paylaştığında olur, ancak kod, doğru paylaşımı garanti etmeyecek şekilde yazılır. Bir örneğe göz atın:
public class App {
public static int value = 0;
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
int oldValue = value;
int newValue = ++value;
if (oldValue + 1 != newValue) {
throw new IllegalStateException(oldValue + " + 1 = " + newValue);
}
}
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
}
}
Bu kod ilk seferinde bir hata oluşturmayabilir. Olduğunda, şöyle görünebilir:
Exception in thread "Thread-1" java.lang.IllegalStateException: 7899 + 1 = 7901
at App.lambda$main$0(App.java:13)
at java.lang.Thread.run(Thread.java:745)
Gördüğünüz gibi newValue
bir değer atanırken bir şeyler ters gitti. newValue
çok büyük. value
Yarış koşulu nedeniyle, iş parçacıklarından biri iki ifade arasındaki değişkeni değiştirmeyi başardı . Konular arasında bir yarış olduğu ortaya çıktı. Şimdi parasal işlemlerde benzer hatalara düşmemenin ne kadar önemli olduğunu bir düşünün... Örnekler ve diyagramlar burada da görülebilir: Java iş parçacığında yarış durumunu simüle eden kod .
Uçucu
Konuların etkileşiminden bahsetmişken,volatile
anahtar kelimeden bahsetmeye değer. Basit bir örneğe bakalım:
public class App {
public static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Runnable whileFlagFalse = () -> {
while(!flag) {
}
System.out.println("Flag is now TRUE");
};
new Thread(whileFlagFalse).start();
Thread.sleep(1000);
flag = true;
}
}
En ilginci, bunun işe yaramama olasılığı çok yüksek. Yeni ileti dizisi, alandaki değişikliği görmez flag
. Bunu alan için düzeltmek için flag
, anahtar kelimeyi kullanmamız gerekiyor volatile
. Nasıl ve neden? İşlemci tüm eylemleri gerçekleştirir. Ancak hesaplamaların sonuçları bir yerde saklanmalıdır. Bunun için ana bellek ve işlemcinin önbelleği vardır. Bir işlemcinin önbellekleri, ana belleğe erişirken olduğundan daha hızlı bir şekilde verilere erişmek için kullanılan küçük bir bellek parçası gibidir. Ancak her şeyin bir dezavantajı vardır: önbellekteki veriler güncel olmayabilir (yukarıdaki örnekte olduğu gibi, bayrak alanının değeri güncellenmedi). Böylecevolatile
anahtar kelime, JVM'ye değişkenimizi önbelleğe almak istemediğimizi söyler. Bu, güncel sonucun tüm başlıklarda görülmesini sağlar. Bu oldukça basitleştirilmiş bir açıklamadır. Anahtar kelimeye gelince , bu makaleyivolatile
okumanızı şiddetle tavsiye ederim . Daha fazla bilgi için Java Memory Model ve Java Volatile Keyword okumanızı da tavsiye ederim . Ek olarak, bunun değişikliklerin atomikliği ile ilgili değil, görünürlükle ilgili olduğunu hatırlamak önemlidir . "Yarış koşulları" bölümündeki koda baktığımızda, IntelliJ IDEA'da bir araç ipucu göreceğiz: Bu inceleme , 2010'da Sürüm Notlarında listelenen IDEA-61117 sayısının bir parçası olarak IntelliJ IDEA'ya eklendi .volatile

Atomiklik
Atomik işlemler bölünemeyen işlemlerdir. Örneğin, bir değişkene değer atama işlemi atomik olmalıdır. Ne yazık ki, artırma işlemi atomik değildir, çünkü artırma işlemi en fazla üç CPU işlemi gerektirir: eski değeri alın, ona bir ekleyin ve ardından değeri kaydedin. Atomiklik neden önemlidir? Arttırma işlemi ile eğer bir yarış durumu varsa o zaman paylaşılan kaynak (yani paylaşılan değer) her an aniden değişebilir. Ek olarak, örneğinlong
ve gibi 64 bit yapıları içeren işlemler double
atomik değildir. Daha fazla ayrıntı buradan okunabilir: 64 bit değerleri okurken ve yazarken atomikliği sağlayın . Atomite ile ilgili problemler bu örnekte görülebilir:
public class App {
public static int value = 0;
public static AtomicInteger atomic = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
value++;
atomic.incrementAndGet();
}
};
for (int i = 0; i < 3; i++) {
new Thread(task).start();
}
Thread.sleep(300);
System.out.println(value);
System.out.println(atomic.get());
}
}
Özel AtomicInteger
sınıf bize her zaman 30.000 verecek, ancak value
zaman zaman değişecek. Bu konuya kısa bir genel bakış vardır: Java'da Atomik Değişkenlere Giriş . "Karşılaştır ve değiştir" algoritması, atomik sınıfların kalbinde yer alır. Bununla ilgili daha fazla bilgiyi burada , JDK 7 ve 8 örneğinde kilitsiz algoritmaların karşılaştırılması - CAS ve FAA'da veya Wikipedia'daki Karşılaştır ve değiştir makalesinde okuyabilirsiniz .
http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html
Olur-önce
"Önceden olur" diye ilginç ve gizemli bir kavram var. Konu çalışmanızın bir parçası olarak, onun hakkında okumalısınız. Daha önce olan ilişkisi, ileti dizileri arasındaki eylemlerin görüleceği sırayı gösterir. Birçok yorum ve yorum var. İşte bu konudaki en son sunumlardan biri: Java "Happens-Before" Relationships .Özet
Bu incelemede, ileti dizilerinin nasıl etkileşime girdiğine ilişkin bazı ayrıntıları inceledik. Ortaya çıkabilecek sorunları ve bunları tespit edip ortadan kaldırmanın yollarını tartıştık. Konuyla ilgili ek materyallerin listesi:- İki kez kontrol edilen kilitleme
- JSR 133 (Java Bellek Modeli) SSS
- IQ 35: Kilitlenme nasıl önlenir?
- Java'da Eşzamanlılık Kavramları, Douglas Hawkins (2017)
GO TO FULL VERSION