JavaRush /Java Blog /Random-ID /Anda Tidak Dapat Memanjakan Java dengan Thread: Bagian II...
Viacheslav
Level 3

Anda Tidak Dapat Memanjakan Java dengan Thread: Bagian III - Interaksi

Dipublikasikan di grup Random-ID
Tinjauan singkat tentang fitur interaksi utas. Sebelumnya, kita melihat bagaimana thread disinkronkan satu sama lain. Kali ini kita akan mendalami masalah yang dapat muncul saat thread berinteraksi dan membahas cara menghindarinya. Kami juga akan memberikan beberapa tautan yang berguna untuk studi lebih dalam. Anda tidak dapat merusak Java dengan utas: Bagian III - interaksi - 1

Perkenalan

Jadi, kita tahu bahwa ada thread di Java, yang dapat Anda baca di ulasan “ Thread Can't Spoil Java: Part I - Threads ” dan thread tersebut dapat disinkronkan satu sama lain, yang telah kita bahas di ulasan “ Thread Tidak Dapat Merusak Java ” Spoil: Bagian II - Sinkronisasi ." Saatnya berbicara tentang bagaimana thread berinteraksi satu sama lain. Bagaimana mereka berbagi sumber daya yang sama? Masalah apa yang mungkin timbul dalam hal ini?

Jalan buntu

Masalah yang paling buruk adalah Deadlock. Ketika dua atau lebih thread menunggu satu sama lain selamanya, ini disebut Deadlock. Mari kita ambil contoh dari website Oracle dari deskripsi 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();
    }
}
Deadlock di sini mungkin tidak muncul pertama kali, tetapi jika eksekusi program Anda terhenti, saatnya dijalankan jvisualvm: Anda tidak dapat merusak Java dengan utas: Bagian III - interaksi - 2Jika ada plugin yang diinstal di JVisualVM (melalui Tools -> Plugins), kita dapat melihat di mana deadlock tersebut terjadi:
"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 menunggu lock dari thread 0. Kenapa bisa terjadi? Thread-1memulai eksekusi dan menjalankan metode Friend#bow. Ditandai dengan kata kunci synchronizedyaitu kita mengambil monitor dengan this. Saat masuk ke metode ini, kami menerima tautan ke metode Friend. Sekarang, thread Thread-1ingin mengeksekusi metode lain Friend, sehingga mendapatkan kunci darinya juga. Namun jika thread lain (dalam hal ini Thread-0) berhasil memasukkan metode tersebut bow, maka kuncinya sudah sibuk dan Thread-1menunggu Thread-0, begitu pula sebaliknya. Pemblokiran itu tidak dapat dipecahkan, jadi Mati, yaitu mati. Baik cengkeraman maut (yang tidak dapat dilepaskan) maupun blok mati yang tidak dapat dilepaskan darinya. Mengenai topik deadlock, Anda dapat menonton video: " Deadlock - Concurrency #1 - Advanced Java ".

kunci hidup

Jika ada Deadlock, apakah ada Livelock? Ya, ada) Livelock adalah benang yang tampak hidup secara lahiriah, tetapi pada saat yang sama tidak dapat berbuat apa-apa, karena... kondisi di mana mereka mencoba untuk melanjutkan pekerjaan mereka tidak dapat dipenuhi. Intinya, Livelock mirip dengan deadlock, tetapi threadnya tidak “menggantung” pada sistem menunggu monitor, tetapi selalu melakukan sesuatu. Misalnya:
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();
    }
}
Keberhasilan kode ini bergantung pada urutan penjadwal thread Java memulai thread. Jika dimulai terlebih dahulu Thead-1, kita akan mendapatkan 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 dapat dilihat dari contoh, kedua utas secara bergantian mencoba menangkap kedua kunci, tetapi gagal. Selain itu, mereka tidak berada dalam kebuntuan, yaitu secara visual semuanya baik-baik saja dan mereka melakukan tugasnya. Anda tidak dapat merusak Java dengan utas: Bagian III - interaksi - 3Menurut JVisualVM, kita melihat periode tidur dan periode parkir (ini adalah saat sebuah thread mencoba menempati kunci, ia masuk ke status taman, seperti yang telah kita bahas sebelumnya ketika berbicara tentang sinkronisasi thread ). Pada topik livelock, Anda dapat melihat contoh: " Java - Thread Livelock ".

Kelaparan

Selain pemblokiran (deadlock dan livelock), ada masalah lain saat bekerja dengan multithreading - Starvation, atau “starvation”. Fenomena ini berbeda dengan pemblokiran karena thread tidak diblokir, namun thread tersebut tidak memiliki sumber daya yang cukup untuk semua orang. Oleh karena itu, meskipun beberapa thread mengambil alih seluruh waktu eksekusi, thread lainnya tidak dapat dieksekusi: Anda tidak dapat merusak Java dengan utas: Bagian III - interaksi - 4

https://www.logicbig.com/

Contoh super dapat ditemukan di sini: " Java - Thread Starvation and Fairness ". Contoh ini menunjukkan cara kerja thread di Starvation dan bagaimana satu perubahan kecil dari Thread.sleep ke Thread.wait dapat mendistribusikan beban secara merata. Anda tidak dapat merusak Java dengan utas: Bagian III - interaksi - 5

Kondisi balapan

Saat bekerja dengan multithreading, ada yang namanya "kondisi balapan". Fenomena ini terletak pada kenyataan bahwa utas berbagi sumber daya tertentu di antara mereka sendiri dan kode ditulis sedemikian rupa sehingga tidak menyediakan pengoperasian yang benar dalam kasus ini. Mari kita lihat sebuah 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();
    }
}
Kode ini mungkin tidak menghasilkan kesalahan saat pertama kali. Dan mungkin terlihat 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 lihat, saat ditugaskan, newValueada yang tidak beres dan newValuemasih banyak lagi. Beberapa alur dalam race state berhasil berubah valueantara kedua tim ini. Seperti yang bisa kita lihat, perlombaan antar thread telah muncul. Sekarang bayangkan betapa pentingnya untuk tidak melakukan kesalahan serupa dengan transaksi uang... Contoh dan diagram juga dapat ditemukan di sini: “ Kode untuk mensimulasikan kondisi balapan di thread Java ”.

Tidak stabil

Berbicara tentang interaksi utas, perlu diperhatikan secara khusus kata kuncinya volatile. Mari kita lihat contoh sederhana:
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;
    }
}
Hal yang paling menarik adalah dengan tingkat kemungkinan yang tinggi hal itu tidak akan berhasil. Thread baru tidak akan melihat perubahannya flag. Untuk memperbaikinya, flagAnda perlu menentukan kata kunci untuk bidang tersebut volatile. Bagaimana dan mengapa? Semua tindakan dilakukan oleh prosesor. Namun hasil perhitungannya perlu disimpan di suatu tempat. Untuk tujuan ini, terdapat memori utama dan cache perangkat keras pada prosesor. Cache prosesor ini seperti sepotong kecil memori untuk mengakses data lebih cepat dibandingkan mengakses memori utama. Namun semuanya juga memiliki kelemahan: data dalam cache mungkin tidak terkini (seperti pada contoh di atas, ketika nilai flag tidak diperbarui). Jadi, kata kuncinya volatilememberi tahu JVM bahwa kita tidak ingin menyimpan variabel kita dalam cache. Ini memungkinkan Anda melihat hasil sebenarnya di semua thread. Ini adalah formulasi yang sangat disederhanakan. Mengenai topik ini, volatilesangat disarankan untuk membaca terjemahan " FAQ JSR 133 (Java Memory Model) ". Saya juga menyarankan Anda untuk membaca lebih lanjut tentang materi “ Java Memory Model ” dan “ Java Volatile Keyword ”. Selain itu, penting untuk diingat bahwa volatileini adalah tentang visibilitas, dan bukan tentang atomisitas perubahan. Jika kita mengambil kode dari "Kondisi Balapan", kita akan melihat petunjuk di IntelliJ Idea: Anda tidak dapat merusak Java dengan utas: Bagian III - interaksi - 6Inspeksi ini (Inspeksi) ditambahkan ke IntelliJ Idea sebagai bagian dari edisi IDEA-61117 , yang tercantum dalam Catatan Rilis pada tahun 2010.

atomisitas

Operasi atom adalah operasi yang tidak dapat dibagi. Misalnya, operasi pemberian nilai ke suatu variabel bersifat atomik. Sayangnya, kenaikan bukanlah operasi atomik, karena sebuah kenaikan memerlukan sebanyak tiga operasi: mendapatkan nilai lama, menambahkan satu ke dalamnya, dan menyimpan nilai. Mengapa atomisitas penting? Dalam contoh kenaikan, jika kondisi balapan terjadi, sumber daya bersama (yaitu nilai bersama) dapat tiba-tiba berubah kapan saja. Selain itu, penting bahwa struktur 64-bit juga tidak bersifat atomik, misalnya longdan double. Anda dapat membaca lebih lanjut di sini: " Pastikan atomisitas saat membaca dan menulis nilai 64-bit ". Contoh permasalahan atomisitas dapat dilihat pada 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 khusus untuk bekerja dengan atom Integerakan selalu menunjukkan kepada kita 30.000, tetapi valueakan berubah dari waktu ke waktu. Ada gambaran singkat tentang topik ini " Pengantar Variabel Atom di Java ". Atomic didasarkan pada algoritma Bandingkan-dan-Tukar. Anda dapat membaca lebih lanjut tentang ini di artikel di 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 dapat merusak Java dengan utas: Bagian III - interaksi - 8

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

Terjadi Sebelumnya

Ada hal yang menarik dan misterius – Terjadi Sebelumnya. Berbicara tentang arus, ada baiknya juga membacanya. Relasi Happens Before menunjukkan urutan tindakan antar thread yang akan terlihat. Ada banyak penafsiran dan penafsiran. Salah satu laporan terbaru tentang topik ini adalah laporan ini:
Mungkin lebih baik video ini tidak menceritakan apa pun tentang hal itu. Jadi saya tinggalkan saja link videonya. Anda dapat membaca " Java - Pemahaman Terjadi-sebelum Hubungan ".

Hasil

Dalam ulasan ini, kami melihat fitur interaksi utas. Kami membahas masalah yang mungkin timbul dan cara mendeteksi dan menghilangkannya. Daftar materi tambahan tentang topik tersebut: #Viacheslav
Komentar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION