CodeGym/Java Blog/Acak/Lebih baik bersama: Java dan kelas Thread. Bagian III — I...
John Squirrels
Level 41
San Francisco

Lebih baik bersama: Java dan kelas Thread. Bagian III — Interaksi

Dipublikasikan di grup Acak
anggota
Tinjauan singkat tentang cara utas berinteraksi. Sebelumnya, kami melihat bagaimana utas disinkronkan satu sama lain. Kali ini kita akan menyelami masalah yang mungkin muncul saat utas berinteraksi, dan kita akan berbicara tentang cara menghindarinya. Kami juga akan memberikan beberapa link yang berguna untuk studi lebih mendalam. Lebih baik bersama: Java dan kelas Thread.  Bagian III — Interaksi - 1

Perkenalan

Jadi, kita tahu bahwa Java memiliki utas. Anda dapat membacanya di ulasan berjudul Lebih baik bersama: Java dan kelas Thread. Bagian I — Utas eksekusi . Dan kami mengeksplorasi fakta bahwa utas dapat disinkronkan satu sama lain dalam ulasan berjudul Lebih baik bersama: Java dan kelas Utas. Bagian II — Sinkronisasi . Saatnya berbicara tentang bagaimana utas berinteraksi satu sama lain. Bagaimana mereka berbagi sumber daya bersama? Masalah apa yang mungkin muncul di sini? Lebih baik bersama: Java dan kelas Thread.  Bagian III — Interaksi - 2

Jalan buntu

Masalah paling menakutkan dari semuanya adalah kebuntuan. Kebuntuan adalah ketika dua atau lebih utas menunggu yang lain selamanya. Kami akan mengambil contoh dari halaman web Oracle yang menjelaskan kebuntuan :
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();
    }
}
Kebuntuan mungkin tidak terjadi di sini pertama kali, tetapi jika program Anda macet, maka saatnya untuk menjalankan jvisualvm: Lebih baik bersama: Java dan kelas Thread.  Bagian III — Interaksi - 3Dengan plugin JVisualVM diinstal (melalui Alat -> Plugin), kita dapat melihat di mana kebuntuan terjadi:
"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
Utas 1 sedang menunggu kunci dari utas 0. Mengapa itu terjadi? Thread-1mulai berjalan dan mengeksekusi Friend#bowmetode. Itu ditandai dengan synchronizedkata kunci, yang artinya kita memperoleh monitor untuk this(objek saat ini). Input metode adalah referensi ke Friendobjek lain. Sekarang, Thread-1ingin menjalankan metode di sisi lain Friend, dan harus mendapatkan kuncinya untuk melakukannya. Tetapi jika utas lainnya (dalam hal ini Thread-0) berhasil memasukkan bow()metode, maka kunci telah diperoleh dan Thread-1menungguThread-0, dan sebaliknya. Ini adalah kebuntuan yang tidak dapat dipecahkan, dan kami menyebutnya kebuntuan. Ibarat cengkeraman maut yang tidak bisa dilepaskan, kebuntuan saling menghalangi yang tidak bisa dipatahkan. Untuk penjelasan deadlock lainnya, Anda dapat menonton video ini: Penjelasan Deadlock dan Livelock .

Livelock

Jika ada kebuntuan, apakah ada juga livelock? Ya, ada :) Livelock terjadi ketika utas secara lahiriah tampak hidup, tetapi mereka tidak dapat melakukan apa pun, karena kondisi yang diperlukan untuk melanjutkan pekerjaan mereka tidak dapat dipenuhi. Pada dasarnya, livelock mirip dengan deadlock, tetapi utasnya tidak "menggantung" menunggu monitor. Sebaliknya, mereka selamanya 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 utas Java memulai utas. Jika Thead-1dimulai lebih dulu, maka kita 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 yang Anda lihat dari contoh, kedua utas mencoba mendapatkan kedua kunci secara bergantian, tetapi gagal. Tapi, mereka tidak menemui jalan buntu. Secara lahiriah, semuanya baik-baik saja dan mereka melakukan tugasnya. Lebih baik bersama: Java dan kelas Thread.  Bagian III — Interaksi - 4Menurut JVisualVM, kita melihat periode tidur dan periode parkir (ini adalah saat utas mencoba untuk mendapatkan kunci — ini memasuki kondisi taman, seperti yang telah kita bahas sebelumnya ketika kita berbicara tentang sinkronisasi utas ) . Anda dapat melihat contoh livelock di sini: Java - Thread Livelock .

Kelaparan

Selain kebuntuan dan livelock, ada masalah lain yang bisa terjadi selama multithreading: kelaparan. Fenomena ini berbeda dari bentuk pemblokiran sebelumnya karena utas tidak diblokir - mereka tidak memiliki sumber daya yang cukup. Akibatnya, sementara beberapa utas menghabiskan seluruh waktu eksekusi, utas lainnya tidak dapat dijalankan: Lebih baik bersama: Java dan kelas Thread.  Bagian III — Interaksi - 5

https://www.logicbig.com/

Anda dapat melihat contoh super di sini: Java - Thread Starvation and Fairness . Contoh ini menunjukkan apa yang terjadi dengan utas selama kelaparan dan bagaimana satu perubahan kecil Thread.sleep()memungkinkan Thread.wait()Anda mendistribusikan beban secara merata. Lebih baik bersama: Java dan kelas Thread.  Bagian III — Interaksi - 6

Kondisi balapan

Dalam multithreading, ada yang namanya "kondisi balapan". Fenomena ini terjadi ketika utas berbagi sumber daya, tetapi kode ditulis dengan cara yang tidak memastikan pembagian yang benar. Lihatlah 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. Ketika itu terjadi, 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, ada yang tidak beres saat newValuediberi nilai. newValueterlalu besar. Karena kondisi balapan, salah satu utas berhasil mengubah variabel valuedi antara kedua pernyataan tersebut. Ternyata ada perlombaan antar utas. Sekarang pikirkan betapa pentingnya untuk tidak melakukan kesalahan serupa dengan transaksi moneter... Contoh dan diagram juga dapat dilihat di sini: Kode untuk mensimulasikan kondisi balapan di utas Java .

Tidak stabil

Berbicara tentang interaksi utas, volatilekata kuncinya patut disebutkan. 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;
    }
}
Yang paling menarik, ini sangat mungkin tidak berhasil. Utas baru tidak akan melihat perubahan di bidang flag. Untuk memperbaikinya untuk flagbidang ini, kita perlu menggunakan volatilekata kunci. Bagaimana dan mengapa? Prosesor melakukan semua tindakan. Tetapi hasil perhitungan harus disimpan di suatu tempat. Untuk ini, ada memori utama dan ada cache prosesor. Cache prosesor seperti sepotong kecil memori yang digunakan untuk mengakses data lebih cepat daripada saat mengakses memori utama. Tetapi semuanya memiliki kelemahan: data dalam cache mungkin tidak diperbarui (seperti pada contoh di atas, ketika nilai bidang bendera tidak diperbarui). Sehinggavolatilekata kunci memberitahu JVM bahwa kita tidak ingin meng-cache variabel kita. Ini memungkinkan hasil terkini untuk dilihat di semua utas. Ini adalah penjelasan yang sangat disederhanakan. Untuk volatilekata kunci, saya sangat menyarankan Anda membaca artikel ini . Untuk informasi lebih lanjut, saya juga menyarankan Anda untuk membaca Java Memory Model dan Java Volatile Keyword . Selain itu, penting untuk diingat bahwa volatileini tentang visibilitas, dan bukan tentang atomisitas perubahan. Melihat kode di bagian "Race conditions", kita akan melihat tooltip di IntelliJ IDEA: Lebih baik bersama: Java dan kelas Thread.  Bagian III — Interaksi - 7Inspeksi ini ditambahkan ke IntelliJ IDEA sebagai bagian dari masalah IDEA-61117 , yang terdaftar di Catatan Rilis pada tahun 2010.

Atomisitas

Operasi atom adalah operasi yang tidak dapat dibagi. Misalnya, operasi pemberian nilai ke variabel harus bersifat atomik. Sayangnya, operasi penambahan tidak bersifat atomik, karena penambahan membutuhkan sebanyak tiga operasi CPU: dapatkan nilai lama, tambahkan satu ke dalamnya, lalu simpan nilainya. Mengapa atomisitas penting? Dengan operasi penambahan, jika ada kondisi balapan, maka sumber daya bersama (yaitu nilai bersama) dapat tiba-tiba berubah sewaktu-waktu. Selain itu, operasi yang melibatkan struktur 64-bit, misalnya longdan double, bukanlah atom. Detail lebih lanjut dapat dibaca di sini: Pastikan atomisitas saat membaca dan menulis nilai 64-bit . Masalah yang berkaitan dengan atomisitas dapat dilihat pada contoh ini:
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 AtomicIntegerakan selalu memberi kita 30.000, tetapi valueakan berubah dari waktu ke waktu. Ada ikhtisar singkat tentang topik ini: Pengantar Variabel Atom di Java . Algoritme "bandingkan-dan-tukar" terletak di jantung kelas atom. Anda dapat membaca selengkapnya di sini di Perbandingan algoritme bebas kunci - CAS dan FAA pada contoh JDK 7 dan 8 atau di artikel Bandingkan-dan-tukar di Wikipedia. Lebih baik bersama: Java dan kelas Thread.  Bagian III — Interaksi - 9

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

Terjadi-sebelumnya

Ada konsep menarik dan misterius yang disebut "terjadi sebelumnya". Sebagai bagian dari studi Anda tentang utas, Anda harus membacanya. Hubungan yang terjadi sebelum menunjukkan urutan tindakan di antara utas akan terlihat. Ada banyak interpretasi dan komentar. Inilah salah satu presentasi terbaru tentang hal ini: Java "Happens-Before" Relationships .

Ringkasan

Dalam ulasan ini, kami telah menjelajahi beberapa hal spesifik tentang bagaimana utas berinteraksi. Kami membahas masalah yang mungkin timbul, serta cara untuk mengidentifikasi dan menghilangkannya. Daftar bahan tambahan pada topik: Lebih baik bersama: Java dan kelas Thread. Bagian I — Utas eksekusi Lebih baik bersama: Java dan kelas Utas. Bagian II — Sinkronisasi Lebih baik bersama: Java dan kelas Thread. Bagian IV — Callable, Future, dan teman Lebih baik bersama: Java dan kelas Thread. Bagian V — Pelaksana, ThreadPool, Fork/Bergabung Lebih baik bersama-sama: Java dan kelas Thread. Bagian VI — Tembak!
Komentar
  • Populer
  • Baru
  • Lama
Anda harus login untuk memberikan komentar
Halaman ini belum memiliki komentar