JavaRush /Blog Java /Random-MS /Anda Tidak Boleh Merosakkan Java dengan Benang: Bahagian ...

Anda Tidak Boleh Merosakkan Java dengan Benang: Bahagian III - Interaksi

Diterbitkan dalam kumpulan
Gambaran keseluruhan ringkas tentang ciri interaksi benang. Sebelum ini, kami melihat bagaimana benang menyegerak antara satu sama lain. Kali ini kita akan menyelami masalah yang boleh timbul apabila benang berinteraksi dan bercakap tentang cara ia boleh dielakkan. Kami juga akan menyediakan beberapa pautan berguna untuk kajian yang lebih mendalam. Anda tidak boleh merosakkan Java dengan benang: Bahagian III - interaksi - 1

pengenalan

Jadi, kami tahu bahawa terdapat benang di Jawa, yang boleh anda baca dalam ulasan " Benang Tidak Boleh Merosakkan Java: Bahagian I - Benang " dan benang itu boleh disegerakkan antara satu sama lain, yang kami uruskan dalam ulasan " Benang Tidak Boleh Merosakkan Java ” Merosakkan: Bahagian II - Penyegerakan ." Sudah tiba masanya untuk bercakap tentang cara benang berinteraksi antara satu sama lain. Bagaimanakah mereka berkongsi sumber yang sama? Apakah masalah yang boleh berlaku dengan ini?

Jalan buntu

Masalah yang paling teruk ialah Deadlock. Apabila dua atau lebih utas menunggu selama-lamanya antara satu sama lain, ini dipanggil Deadlock. Mari kita ambil contoh dari laman web Oracle dari penerangan konsep " Deadlock ":
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 has bowed to me!%n",
                    this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s has 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();
    }
}
Kebuntuan di sini mungkin tidak muncul buat kali pertama, tetapi jika pelaksanaan program anda tersekat, sudah tiba masanya untuk dijalankan jvisualvm: Anda tidak boleh merosakkan Java dengan benang: Bahagian III - interaksi - 2Jika pemalam dipasang dalam JVisualVM (melalui Alat -> Pemalam), kita boleh melihat di mana kebuntuan berlaku:
"Thread-1" - Thread t@12
   java.lang.Thread.State: BLOCKED
    at Deadlock$Friend.bowBack(Deadlock.java:16)
    - waiting to lock &lt33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
Thread 1 sedang menunggu kunci dari thread 0. Mengapa ini berlaku? Thread-1memulakan pelaksanaan dan melaksanakan kaedah Friend#bow. Ia ditandakan dengan kata kunci synchronized, iaitu, kami mengambil monitor dengan this. Di pintu masuk kaedah, kami menerima pautan ke yang lain Friend. Sekarang, utas itu Thread-1mahu melaksanakan kaedah pada yang lain Friend, dengan itu mendapatkan kunci daripadanya juga. Tetapi jika benang lain (dalam kes ini Thread-0) berjaya memasuki kaedah bow, maka kunci sudah sibuk dan Thread-1menunggu Thread-0, dan sebaliknya. Penyekatan tidak dapat diselesaikan, jadi ia adalah Mati, iaitu, mati. Kedua-dua cengkaman maut (yang tidak boleh dilepaskan) dan blok mati yang tidak dapat dilepaskan oleh seseorang. Mengenai topik kebuntuan, anda boleh menonton video: " Deadlock - Concurrency #1 - Advanced Java ".

Livelock

Jika terdapat Deadlock, adakah Livelock? Ya, ada) Livelock ialah benang kelihatan hidup secara luaran, tetapi pada masa yang sama mereka tidak boleh berbuat apa-apa, kerana... keadaan di mana mereka cuba meneruskan kerja mereka tidak dapat dipenuhi. Pada dasarnya, Livelock adalah serupa dengan kebuntuan, tetapi benang tidak "bergantung" pada sistem menunggu monitor, tetapi sentiasa melakukan sesuatu. Sebagai contoh:
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();
    }
}
Kejayaan kod ini bergantung pada susunan di mana penjadual benang Java memulakan benang. Jika ia bermula dahulu Thead-1, kami akan mendapat Livelock:
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
...
Seperti yang dapat dilihat daripada contoh, kedua-dua benang cuba untuk menangkap kedua-dua kunci secara bergantian, tetapi ia gagal. Lebih-lebih lagi, mereka tidak berada dalam kebuntuan, iaitu, secara visual semuanya baik-baik saja dengan mereka dan mereka melakukan tugas mereka. Anda tidak boleh merosakkan Java dengan benang: Bahagian III - interaksi - 3Menurut JVisualVM, kita melihat tempoh tidur dan tempoh taman (ini adalah apabila benang cuba menduduki kunci, ia masuk ke keadaan taman, seperti yang kita bincangkan sebelum ini apabila bercakap tentang penyegerakan benang ). Mengenai topik livelock, anda boleh melihat contoh: " Java - Thread Livelock ".

Kebuluran

Selain menyekat (kebuntuan dan hidupan), terdapat masalah lain apabila bekerja dengan multithreading - Kebuluran, atau "kebuluran". Fenomena ini berbeza daripada menyekat kerana benang tidak disekat, tetapi mereka tidak mempunyai sumber yang mencukupi untuk semua orang. Oleh itu, sementara beberapa utas mengambil alih semua masa pelaksanaan, yang lain tidak boleh dilaksanakan: Anda tidak boleh merosakkan Java dengan benang: Bahagian III - interaksi - 4

https://www.logicbig.com/

Contoh hebat boleh didapati di sini: " Java - Thread Starvation and Fairness ". Contoh ini menunjukkan cara utas berfungsi dalam Kebuluran dan cara satu perubahan kecil daripada Thread.sleep ke Thread.wait boleh mengagihkan beban secara sama rata. Anda tidak boleh merosakkan Java dengan benang: Bahagian III - interaksi - 5

Keadaan bangsa

Apabila bekerja dengan multithreading, terdapat perkara seperti "keadaan perlumbaan". Fenomena ini terletak pada fakta bahawa benang berkongsi sumber tertentu antara mereka sendiri dan kod itu ditulis sedemikian rupa sehingga ia tidak menyediakan operasi yang betul dalam kes ini. Mari lihat contoh:
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();
    }
}
Kod ini mungkin tidak menghasilkan ralat pada kali pertama. Dan ia mungkin kelihatan seperti ini:
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)
Seperti yang anda boleh lihat, semasa ia ditugaskan, newValuesesuatu telah berlaku dan newValuemasih banyak lagi. Beberapa benang dalam keadaan perlumbaan berjaya berubah valueantara kedua-dua pasukan ini. Seperti yang kita dapat lihat, perlumbaan antara benang telah muncul. Sekarang bayangkan betapa pentingnya untuk tidak membuat kesilapan yang sama dengan urus niaga wang... Contoh dan rajah juga boleh dilihat di sini: “ Kod untuk mensimulasikan keadaan perlumbaan dalam benang Java ”.

Tidak menentu

Bercakap tentang interaksi urutan, perlu diperhatikan terutamanya kata kunci volatile. Mari lihat contoh mudah:
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;
    }
}
Perkara yang paling menarik ialah dengan kebarangkalian yang tinggi ia tidak akan berfungsi. Urutan baru tidak akan melihat perubahan flag. Untuk membetulkannya, flaganda perlu menentukan kata kunci untuk medan volatile. Bagaimana dan mengapa? Semua tindakan dilakukan oleh pemproses. Tetapi hasil pengiraan perlu disimpan di suatu tempat. Untuk tujuan ini, terdapat memori utama dan cache perkakasan pada pemproses. Cache pemproses ini adalah seperti sekeping kecil memori untuk mengakses data lebih cepat daripada mengakses memori utama. Tetapi semuanya juga mempunyai kelemahan: data dalam cache mungkin tidak terkini (seperti dalam contoh di atas, apabila nilai bendera tidak dikemas kini). Jadi, kata kunci volatilememberitahu JVM bahawa kami tidak mahu cache pembolehubah kami. Ini membolehkan anda melihat hasil sebenar dalam semua urutan. Ini adalah formulasi yang sangat mudah. Mengenai topik ini, volatilesangat disyorkan untuk membaca terjemahan " Soalan Lazim JSR 133 (Java Memory Model) ". Saya juga menasihati anda untuk membaca lebih lanjut mengenai bahan " Model Memori Java " dan " Kata Kunci Meruap Java ". Di samping itu, adalah penting untuk diingat bahawa volatileini adalah mengenai keterlihatan, dan bukan mengenai keatomisan perubahan. Jika kita mengambil kod daripada "Keadaan Perlumbaan", kita akan melihat petunjuk dalam IntelliJ Idea: Anda tidak boleh merosakkan Java dengan benang: Bahagian III - interaksi - 6Pemeriksaan (Pemeriksaan) ini telah ditambahkan pada IntelliJ Idea sebagai sebahagian daripada isu IDEA-61117 , yang disenaraikan dalam Nota Keluaran pada tahun 2010.

Atomiti

Operasi atom ialah operasi yang tidak boleh dibahagikan. Sebagai contoh, operasi memberikan nilai kepada pembolehubah adalah atom. Malangnya, kenaikan bukan operasi atom, kerana kenaikan memerlukan sebanyak tiga operasi: dapatkan nilai lama, tambah satu padanya dan simpan nilai. Mengapakah atomiti penting? Dalam contoh kenaikan, jika keadaan perlumbaan berlaku, pada bila-bila masa sumber yang dikongsi (iaitu, nilai yang dikongsi) mungkin tiba-tiba berubah. Di samping itu, adalah penting bahawa struktur 64-bit juga bukan atom, contohnya longdan double. Anda boleh membaca lebih lanjut di sini: " Pastikan atomicity semasa membaca dan menulis nilai 64-bit ". Contoh masalah dengan atomicity boleh dilihat dalam contoh berikut:
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());
    }
}
Kelas khas untuk bekerja dengan atom Integerakan sentiasa menunjukkan kepada kita 30000, tetapi valueia akan berubah dari semasa ke semasa. Terdapat gambaran ringkas mengenai topik ini " Pengenalan kepada Pembolehubah Atom di Jawa ". Atom adalah berdasarkan algoritma Bandingkan-dan-Tukar. Anda boleh membaca lebih lanjut mengenainya dalam artikel tentang Habré " Perbandingan algoritma bebas Kunci - CAS dan FAA menggunakan contoh JDK 7 dan 8 " atau di Wikipedia dalam artikel tentang " Perbandingan dengan pertukaran ". Anda tidak boleh merosakkan Java dengan benang: Bahagian III - interaksi - 8

http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html

Berlaku Sebelum

Terdapat satu perkara yang menarik dan misteri - Berlaku Sebelum. Bercakap tentang aliran, ia juga patut dibaca tentangnya. Hubungan Berlaku Sebelum menunjukkan susunan tindakan antara utas akan dilihat. Terdapat banyak tafsiran dan tafsiran. Salah satu laporan terkini mengenai topik ini ialah laporan ini:
Mungkin lebih baik video ini tidak memberitahu apa-apa tentangnya. Jadi saya akan tinggalkan pautan kepada video tersebut. Anda boleh membaca " Java - Understanding Happens-before relationships ".

Keputusan

Dalam ulasan ini, kami melihat ciri interaksi benang. Kami membincangkan masalah yang mungkin timbul dan cara untuk mengesan dan menghapuskannya. Senarai bahan tambahan mengenai topik: #Viacheslav
Komen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION