JavaRush /จาวาบล็อก /Random-TH /คุณไม่สามารถสปอย Java ด้วย Thread: ตอนที่ III - การโต้ตอบ...
Viacheslav
ระดับ

คุณไม่สามารถสปอย Java ด้วย Thread: ตอนที่ III - การโต้ตอบ

เผยแพร่ในกลุ่ม
ภาพรวมโดยย่อของคุณลักษณะของการโต้ตอบกับเธรด ก่อนหน้านี้ เราได้ดูว่าเธรดซิงโครไนซ์กันอย่างไร คราวนี้เราจะเจาะลึกถึงปัญหาที่อาจเกิดขึ้นเมื่อเธรดโต้ตอบกัน และพูดคุยเกี่ยวกับวิธีหลีกเลี่ยง นอกจากนี้เรายังมีลิงก์ที่เป็นประโยชน์สำหรับการศึกษาเชิงลึกอีกด้วย คุณไม่สามารถทำลาย Java ด้วยเธรดได้: ตอนที่ III - การโต้ตอบ - 1

การแนะนำ

ดังนั้นเราจึงรู้ว่ามีเธรดใน Java ซึ่งคุณสามารถอ่านได้ในรีวิว “ Thread Can't Spoil Java: Part I - Threads ” และเธรดนั้นสามารถซิงโครไนซ์ซึ่งกันและกันได้ ซึ่งเราจัดการในการรีวิว “ เธรดไม่สามารถสปอย Java ” สปอยล์: ตอนที่ II - การซิงโครไนซ์ " ถึงเวลาที่จะพูดถึงว่าเธรดมีปฏิสัมพันธ์กันอย่างไร พวกเขาแบ่งปันทรัพยากรร่วมกันอย่างไร? จะมีปัญหาอะไรเกิดขึ้นบ้าง?

การหยุดชะงัก

ปัญหาที่เลวร้ายที่สุดคือการหยุดชะงัก เมื่อเธรดตั้งแต่สองเธรดขึ้นไปรอกันและกันตลอดไป สิ่งนี้เรียกว่า Deadlock ลองยกตัวอย่างจากเว็บไซต์ Oracle จากคำอธิบายแนวคิดของ " 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();
    }
}
การหยุดชะงักที่นี่อาจไม่ปรากฏขึ้นในครั้งแรก แต่ถ้าการทำงานของโปรแกรมของคุณติดขัด ก็ถึงเวลาที่ต้องรันjvisualvm: คุณไม่สามารถทำลาย Java ด้วยเธรด: ตอนที่ III - การโต้ตอบ - 2หากมีการติดตั้งปลั๊กอินใน JVisualVM (ผ่านเครื่องมือ -> ปลั๊กอิน) เราจะเห็นว่าการหยุดชะงักเกิดขึ้นที่ใด:
"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
เธรด 1 กำลังรอการล็อคจากเธรด 0 เหตุใดสิ่งนี้จึงเกิดขึ้น Thread-1เริ่มดำเนินการและดำเนินการตามวิธีFriend#bowการ มันถูกทำเครื่องหมายด้วยคำหลักsynchronizedนั่นคือเรารับจอภาพthisด้วย ที่ทางเข้าวิธีการ เราได้รับลิงก์ไปยังอีกวิธีFriendหนึ่ง ตอนนี้ เธรดThread-1ต้องการดำเนินการวิธีการกับอีกวิธีหนึ่งFriendดังนั้นจึงได้รับการล็อคจากเขาเช่นกัน แต่หากเธรดอื่น (ในกรณีนี้Thread-0) จัดการเพื่อเข้าสู่วิธีการ แสดงbowว่าการล็อคไม่ว่างและThread-1กำลังรออยู่Thread-0และในทางกลับกัน การปิดกั้นนั้นแก้ไม่ได้จึงตายนั่นคือตาย ทั้งการจับความตาย (ซึ่งไม่สามารถปล่อยออกมาได้) และอุปสรรคที่ไม่อาจหลบหนีได้ ในหัวข้อการหยุดชะงัก คุณสามารถชมวิดีโอ: " Deadlock - Concurrency #1 - Java ขั้นสูง "

ไลฟ์ล็อค

หากมี Deadlock แล้วจะมี Livelock หรือไม่? ใช่ มี) Livelock คือ เธรดดูเหมือนจะมีชีวิตภายนอก แต่ในขณะเดียวกันก็ไม่สามารถทำอะไรได้เลย เพราะ... ไม่สามารถปฏิบัติตามเงื่อนไขที่พวกเขาพยายามทำงานต่อไปได้ โดยพื้นฐานแล้ว Livelock นั้นคล้ายกับการหยุดชะงัก แต่เธรดจะไม่ "ค้าง" ในระบบเพื่อรอจอภาพ แต่มักจะทำอะไรบางอย่างอยู่เสมอ ตัวอย่างเช่น:
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();
    }
}
ความสำเร็จของโค้ดนี้ขึ้นอยู่กับลำดับที่ตัวกำหนดเวลาเธรด Java เริ่มต้นเธรด ถ้ามันเริ่มก่อนThead-1เราจะได้ 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
...
ดังที่เห็นได้จากตัวอย่าง ทั้งสองเธรดสลับกันพยายามจับล็อคทั้งสอง แต่ล้มเหลว ยิ่งกว่านั้นพวกเขาไม่ได้หยุดชะงักนั่นคือทุกอย่างดูดีสำหรับพวกเขาและพวกเขาก็กำลังทำงานของพวกเขา คุณไม่สามารถทำลาย Java ด้วยเธรด: ตอนที่ III - การโต้ตอบ - 3จากข้อมูลของ JVisualVM เราเห็นช่วงเวลาสลีปและระยะเวลาการพัก (นี่คือเมื่อเธรดพยายามครอบครองการล็อค และจะเข้าสู่สถานะการพัก ตามที่เราได้กล่าวไว้ก่อนหน้านี้เมื่อพูดถึงการซิงโครไนซ์เธรด ) ในหัวข้อ livelock คุณสามารถดูตัวอย่าง: " Java - Thread Livelock "

ความอดอยาก

นอกเหนือจากการบล็อก (deadlock และ livelock) ยังมีปัญหาอีกประการหนึ่งเมื่อทำงานกับมัลติเธรด - ความอดอยากหรือ "ความอดอยาก" ปรากฏการณ์นี้แตกต่างจากการบล็อกตรงที่เธรดไม่ถูกบล็อก แต่มีทรัพยากรไม่เพียงพอสำหรับทุกคน ดังนั้น แม้ว่าบางเธรดจะใช้เวลาดำเนินการทั้งหมด แต่เธรดอื่นๆ ก็ไม่สามารถดำเนินการได้: คุณไม่สามารถทำลาย Java ด้วยเธรด: ตอนที่ III - การโต้ตอบ - 4

https://www.logicbig.com/

ตัวอย่างที่ยอดเยี่ยมสามารถพบได้ที่นี่: " Java - Thread Starvation and Fairness " ตัวอย่างนี้แสดงวิธีการทำงานของเธรดใน Starvation และการเปลี่ยนแปลงเล็กน้อยจาก Thread.sleep เป็น Thread.wait สามารถกระจายโหลดอย่างเท่าเทียมกันได้อย่างไร คุณไม่สามารถทำลาย Java ด้วยเธรด: ตอนที่ III - การโต้ตอบ - 5

สภาพการแข่งขัน

เมื่อทำงานกับมัลติเธรดจะเกิด "สภาวะการแข่งขัน" ปรากฏการณ์นี้อยู่ในความจริงที่ว่าเธรดแบ่งปันทรัพยากรบางอย่างระหว่างกันและโค้ดถูกเขียนในลักษณะที่ไม่ได้จัดเตรียมไว้สำหรับการดำเนินการที่ถูกต้องในกรณีนี้ ลองดูตัวอย่าง:
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();
    }
}
รหัสนี้อาจไม่สร้างข้อผิดพลาดในครั้งแรก และอาจมีลักษณะเช่นนี้:
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)
อย่างที่คุณเห็น ในขณะที่กำลังได้รับมอบหมายnewValueมีบางอย่างผิดพลาดและnewValueยังมีอีกมาก เธรดบางส่วนในสภาพการแข่งขันสามารถเปลี่ยนแปลงได้valueระหว่างสองทีมนี้ ดังที่เราเห็น การแข่งขันระหว่างเธรดได้ปรากฏขึ้น ทีนี้ลองจินตนาการดูว่าการไม่ทำผิดพลาดแบบเดียวกันนี้กับธุรกรรมทางการเงินนั้นสำคัญขนาดไหน... สามารถดูตัวอย่างและไดอะแกรมได้ที่นี่: “ โค้ดเพื่อจำลองสภาพการแข่งขันในเธรด Java

ระเหย

เมื่อพูดถึงปฏิสัมพันธ์ของเธรด คำหลักนั้นควรค่าแก่การสังเกตเป็นvolatileพิเศษ ลองดูตัวอย่างง่ายๆ:
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;
    }
}
สิ่งที่น่าสนใจที่สุดคือมีโอกาสสูงที่มันจะไม่ทำงาน เธรดใหม่จะไม่เห็นการflagเปลี่ยนแปลง ในการแก้ไขปัญหานี้flagคุณจะต้องระบุคำหลัก สำหรับ volatileฟิลด์ อย่างไรและทำไม? การดำเนินการทั้งหมดดำเนินการโดยโปรเซสเซอร์ แต่ต้องเก็บผลการคำนวณไว้ที่ไหนสักแห่ง เพื่อจุดประสงค์นี้จึงมีหน่วยความจำหลักและแคชฮาร์ดแวร์บนโปรเซสเซอร์ แคชโปรเซสเซอร์เหล่านี้เปรียบเสมือนหน่วยความจำชิ้นเล็กๆ สำหรับการเข้าถึงข้อมูลได้เร็วกว่าการเข้าถึงหน่วยความจำหลัก แต่ทุกอย่างก็มีข้อเสียเช่นกัน: ข้อมูลในแคชอาจไม่เป็นปัจจุบัน (ดังตัวอย่างด้านบนเมื่อไม่ได้อัปเดตค่าแฟล็ก) ดังนั้นคีย์เวิร์ดvolatileจะบอก JVM ว่าเราไม่ต้องการแคชตัวแปรของเรา ซึ่งจะทำให้คุณสามารถเห็นผลลัพธ์ที่แท้จริงในทุกเธรด นี่เป็นสูตรที่ง่ายมาก ในหัวข้อนี้volatileขอแนะนำอย่างยิ่งให้อ่านคำแปลของ " JSR 133 (Java Memory Model) FAQ " ฉันยังแนะนำให้คุณอ่านเพิ่มเติมเกี่ยวกับวัสดุ “ Java Memory Model ” และ “ Java Volatile Keyword ” นอกจากนี้ สิ่งสำคัญคือต้องจำไว้ว่าvolatileสิ่งนี้เกี่ยวกับการมองเห็น ไม่ใช่เกี่ยวกับอะตอมมิกของการเปลี่ยนแปลง หากเราใช้โค้ดจาก "สภาพการแข่งขัน" เราจะเห็นคำใบ้ใน IntelliJ Idea: คุณไม่สามารถทำลาย Java ด้วยเธรด: ตอนที่ III - การโต้ตอบ - 6การตรวจสอบ (การตรวจสอบ) นี้ถูกเพิ่มใน IntelliJ Idea โดยเป็นส่วนหนึ่งของปัญหาIDEA-61117ซึ่งแสดงอยู่ในบันทึกประจำรุ่นเมื่อปี 2010

อะตอมมิกซิตี

การดำเนินการของอะตอมเป็นการดำเนินการที่ไม่สามารถแบ่งแยกได้ ตัวอย่างเช่น การดำเนินการกำหนดค่าให้กับตัวแปรเป็นแบบอะตอมมิก น่าเสียดายที่การเพิ่มขึ้นไม่ใช่การดำเนินการแบบอะตอมมิก เนื่องจาก การเพิ่มขึ้นต้องใช้การดำเนินการมากถึงสามรายการ: รับค่าเก่า เพิ่มหนึ่งรายการเข้าไป และบันทึกค่า เหตุใดอะตอมมิกซิตีจึงมีความสำคัญ ในตัวอย่างที่เพิ่มขึ้น หากสภาวะการแข่งขันเกิดขึ้น ทรัพยากรที่ใช้ร่วมกัน (เช่น ค่าที่ใช้ร่วมกัน) อาจเปลี่ยนแปลงทันทีเมื่อใดก็ได้ นอกจากนี้ สิ่งสำคัญคือโครงสร้าง 64 บิตต้องไม่ใช่อะตอมมิกด้วย เช่นlongและdouble. คุณสามารถอ่านเพิ่มเติมได้ที่นี่: " ตรวจสอบความเป็นอะตอมมิกเมื่ออ่านและเขียนค่า 64 บิต " ตัวอย่างของปัญหาเกี่ยวกับอะตอมมิกซิตีสามารถดูได้ในตัวอย่างต่อไปนี้:
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());
    }
}
คลาสพิเศษสำหรับการทำงานกับอะตอมIntegerจะแสดงให้เราเห็น 30,000 เสมอ แต่valueจะมีการเปลี่ยนแปลงเป็นครั้งคราว มีภาพรวมโดยย่อในหัวข้อ " บทนำเกี่ยวกับตัวแปรอะตอมใน Java " Atomic ขึ้นอยู่กับอัลกอริธึมการเปรียบเทียบและสลับ คุณสามารถอ่านเพิ่มเติมได้ในบทความเรื่องHabré " การเปรียบเทียบอัลกอริธึมที่ปราศจากการล็อค - CAS และ FAA โดยใช้ตัวอย่างของ JDK 7 และ 8 " หรือบน Wikipedia ในบทความเกี่ยวกับ " การเปรียบเทียบกับการแลกเปลี่ยน " คุณไม่สามารถทำลาย Java ด้วยเธรด: ตอนที่ III - การโต้ตอบ - 8

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

เกิดขึ้นก่อน

มีสิ่งที่น่าสนใจและลึกลับเกิดขึ้นก่อน เมื่อพูดถึงกระแส ก็ควรค่าแก่การอ่านเช่นกัน ความสัมพันธ์ Happens Before ระบุลำดับที่จะเห็นการดำเนินการระหว่างเธรด มีการตีความและการตีความมากมาย หนึ่งในรายงานล่าสุดในหัวข้อนี้คือรายงานนี้:
คงจะดีกว่าถ้าวิดีโอนี้ไม่ได้บอกอะไรเกี่ยวกับเรื่องนี้ ฉันจะทิ้งลิงก์ไปยังวิดีโอไว้ คุณสามารถอ่าน " Java - การทำความเข้าใจสิ่งที่เกิดขึ้นก่อนความสัมพันธ์ "

ผลลัพธ์

ในการทบทวนนี้ เราได้ดูคุณลักษณะของการโต้ตอบกับเธรด เราได้หารือเกี่ยวกับปัญหาที่อาจเกิดขึ้นและวิธีการตรวจจับและกำจัดปัญหาเหล่านั้น รายการวัสดุเพิ่มเติมในหัวข้อ: #เวียเชสลาฟ
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION