CodeGym /Java Blogu /Rastgele /Birlikte daha iyi: Java ve Thread sınıfı. Bölüm III - Etk...
John Squirrels
Seviye
San Francisco

Birlikte daha iyi: Java ve Thread sınıfı. Bölüm III - Etkileşim

grupta yayınlandı
İş 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. Birlikte daha iyi: Java ve Thread sınıfı.  Bölüm III - Etkileşim - 1

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? Birlikte daha iyi: Java ve Thread sınıfı.  Bölüm III - Etkileşim - 2

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: Birlikte daha iyi: Java ve Thread sınıfı.  Bölüm III - Etkileşim - 3Bir JVisualVM eklentisi yüklendiğinde (Araçlar -> Eklentiler yoluyla), kilitlenmenin nerede meydana geldiğini görebiliriz:

"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#bowyöntemi yürütür. Anahtar kelime ile işaretlenmiştir , bu da (geçerli nesne) synchronizediçin monitörü aldığımız anlamına gelir . thisMetodun girdisi, diğer nesneye bir referanstı Friend. Şimdi, Thread-1yöntemi diğerinde yürütmek istiyor Friendve 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-1veThread-0ve 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ığı senkronizasyonuBirlikte daha iyi: Java ve Thread sınıfı.  Bölüm III - Etkileşim - 4 hakkında konuştuğumuzda tartıştığımız gibi, park durumuna girer) . Bir livelock örneğini burada görebilirsiniz: Java - Thread Livelock .

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: Birlikte daha iyi: Java ve Thread sınıfı.  Bölüm III - Etkileşim - 5

https://www.logicbig.com/

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 Thread.sleep()yükü Thread.wait()eşit şekilde dağıtmanıza nasıl izin verdiğini gösterir. Birlikte daha iyi: Java ve Thread sınıfı.  Bölüm III - Etkileşim - 6

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 newValuebir değer atanırken bir şeyler ters gitti. newValueçok büyük. valueYarış 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, volatileanahtar 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öylecevolatileanahtar 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 .volatileBirlikte daha iyi: Java ve Thread sınıfı.  Bölüm III - Etkileşim - 7

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ğin longve gibi 64 bit yapıları içeren işlemler doubleatomik 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 AtomicIntegersınıf bize her zaman 30.000 verecek, ancak valuezaman 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 .Birlikte daha iyi: Java ve Thread sınıfı.  Bölüm III - Etkileşim - 9

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: Birlikte daha iyi: Java ve Thread sınıfı. Bölüm I — Yürütme konuları Birlikte daha iyi: Java ve Thread sınıfı. Bölüm II — Eşitleme Birlikte daha iyi: Java ve Thread sınıfı. Bölüm IV — Callable, Future ve arkadaşlar Birlikte daha iyi: Java ve Thread sınıfı. Bölüm V — Yürütücü, ThreadPool, Fork/Join Birlikte daha iyi: Java ve Thread sınıfı. Bölüm VI - Ateş edin!
Yorumlar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION