โค้ดยิม/จาวาบล็อก/สุ่ม/ดีกว่ากัน: Java และคลาสเธรด ส่วนที่ 3 — ปฏิสัมพันธ์
John Squirrels
ระดับ
San Francisco

ดีกว่ากัน: Java และคลาสเธรด ส่วนที่ 3 — ปฏิสัมพันธ์

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

การแนะนำ

ดังนั้นเราจึงรู้ว่า Java มีเธรด คุณสามารถอ่านเกี่ยวกับเรื่องนี้ได้ในบทวิจารณ์เรื่องBetter together: Java and the Thread class ส่วนที่ 1 — เธรดของการดำเนินการ และเราได้สำรวจข้อเท็จจริงที่ว่าเธรดสามารถซิงโครไนซ์กับเธรดอื่นได้ในบทวิจารณ์เรื่องBetter together: Java and the Thread class ส่วนที่ II — การซิงโครไนซ์ ได้เวลาพูดคุยเกี่ยวกับวิธีที่เธรดโต้ตอบกัน พวกเขาแบ่งปันทรัพยากรร่วมกันอย่างไร? ปัญหาที่อาจเกิดขึ้นที่นี่? ดีกว่ากัน: Java และคลาสเธรด  ส่วนที่ 3 — ปฏิสัมพันธ์ - 2

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

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

ความอดอยาก

นอกจากการหยุดชะงักและไลฟ์ล็อกแล้ว ยังมีปัญหาอื่นที่อาจเกิดขึ้นระหว่างการทำงานแบบมัลติเธรดได้ นั่นคือความอดอยาก ปรากฏการณ์นี้แตกต่างจากการบล็อกรูปแบบก่อนหน้านี้ตรงที่เธรดไม่ถูกบล็อก — มีเพียงทรัพยากรไม่เพียงพอ เป็นผลให้ในขณะที่บางเธรดใช้เวลาในการดำเนินการทั้งหมด แต่เธรดอื่นไม่สามารถเรียกใช้ได้: ดีกว่ากัน: Java และคลาสเธรด  ส่วนที่ 3 — ปฏิสัมพันธ์ - 5

https://www.logicbig.com/

คุณสามารถดูตัวอย่างที่ยอดเยี่ยมได้ที่นี่: Java - Thread Starvation and Fairness ตัวอย่างนี้แสดงสิ่งที่เกิดขึ้นกับเธรดระหว่างการอดอาหาร และการเปลี่ยนแปลงเพียงเล็กน้อยจากThread.sleep()เป็นThread.wait()ช่วยให้คุณกระจายโหลดได้เท่าๆ กัน ดีกว่ากัน: Java และคลาสเธรด  ส่วนที่ 3 — ปฏิสัมพันธ์ - 6

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

ในมัลติเธรด มีสิ่งเช่น "สภาวะการแข่งขัน" ปรากฏการณ์นี้เกิดขึ้นเมื่อเธรดใช้ทรัพยากรร่วมกัน แต่โค้ดถูกเขียนในลักษณะที่ไม่รับประกันการแบ่งปันที่ถูกต้อง ลองดูตัวอย่าง:
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คำหลัก ฉันขอแนะนำให้คุณอ่านบทความนี้ สำหรับข้อมูลเพิ่มเติม ฉันแนะนำให้คุณอ่าน Java Memory ModelและJava Volatile Keyword นอกจากนี้ สิ่งสำคัญคือต้องจำไว้ว่าสิ่งนั้นvolatileเกี่ยวกับการมองเห็น ไม่ใช่เกี่ยวกับปรมาณูของการเปลี่ยนแปลง เมื่อดูรหัสในส่วน "เงื่อนไขการแข่งขัน" เราจะเห็นคำแนะนำเครื่องมือใน IntelliJ IDEA: ดีกว่ากัน: Java และคลาสเธรด  ส่วนที่ 3 — ปฏิสัมพันธ์ - 7การตรวจสอบนี้ถูกเพิ่มไปยัง IntelliJ IDEA โดยเป็นส่วนหนึ่งของปัญหาIDEA-61117ซึ่งระบุไว้ในบันทึกย่อประจำรุ่นย้อนกลับไปในปี 2010

ปรมาณู

ปฏิบัติการปรมาณูเป็นปฏิบัติการที่ไม่สามารถแบ่งแยกได้ ตัวอย่างเช่น การดำเนินการกำหนดค่าให้กับตัวแปรจะต้องเป็นแบบปรมาณู ขออภัย การดำเนินการเพิ่มไม่ใช่ระดับปรมาณู เนื่องจากการเพิ่มขึ้นต้องการการดำเนินการ CPU มากถึงสามรายการ: รับค่าเก่า เพิ่มหนึ่งค่า จากนั้นบันทึกค่า ทำไมปรมาณูจึงสำคัญ? ด้วยการดำเนินการเพิ่ม หากมีสภาวะการแย่งชิง ทรัพยากรที่ใช้ร่วมกัน (เช่น มูลค่าที่ใช้ร่วมกัน) อาจเปลี่ยนแปลงทันทีเมื่อใดก็ได้ นอกจากนี้ การดำเนินการเกี่ยวกับโครงสร้าง 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());
    }
}
คลาส พิเศษAtomicIntegerจะให้เงินเรา 30,000 เสมอ แต่valueจะเปลี่ยนไปเรื่อยๆ มีภาพรวมสั้นๆ ของหัวข้อนี้: Introduction to Atomic Variables in Java อัลกอริทึม "เปรียบเทียบและแลกเปลี่ยน" เป็นหัวใจสำคัญของคลาสอะตอม คุณสามารถอ่านเพิ่มเติมได้ที่นี่ในการเปรียบเทียบอัลกอริทึมที่ปราศจากการล็อค - CAS และ FAA ในตัวอย่างของ JDK 7 และ 8หรือใน บทความ เปรียบเทียบและแลกเปลี่ยนบนวิกิพีเดีย ดีกว่ากัน: Java และคลาสเธรด  ส่วนที่ 3 — ปฏิสัมพันธ์ - 9

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

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

มีแนวคิดที่น่าสนใจและลึกลับที่เรียกว่า "เกิดขึ้นก่อน" ในส่วนหนึ่งของการศึกษาหัวข้อของคุณ คุณควรอ่านเกี่ยวกับเรื่องนี้ ความสัมพันธ์ที่จะเกิดขึ้นก่อนแสดงลำดับการดำเนินการระหว่างเธรด มีการตีความและให้ข้อคิดมากมาย นี่คือหนึ่งในงานนำ เสนอ ล่าสุดในหัวข้อนี้: Java "Happens-Before" Relationships

สรุป

ในการตรวจสอบนี้ เราได้สำรวจลักษณะเฉพาะบางประการของการโต้ตอบของเธรด เราได้หารือถึงปัญหาที่อาจเกิดขึ้นตลอดจนวิธีการระบุและกำจัดปัญหาเหล่านั้น รายการวัสดุเพิ่มเติมในหัวข้อ: ดีกว่ากัน: Java และคลาสเธรด ส่วนที่ 1 — เธรดของการดำเนินการ ที่ดีกว่าด้วยกัน: Java และคลาสเธรด ส่วนที่ II — การซิงโครไนซ์ ร่วมกันได้ดีขึ้น: Java และคลาสเธรด ตอนที่ IV — Callable, Future และผองเพื่อน อยู่ด้วยกันดีกว่า: Java และคลาส Thread ส่วนที่ V — Executor, ThreadPool, Fork/Jin Better together: Java และคลาส Thread ตอนที่ VI — ยิงออกไป!
ความคิดเห็น
  • เป็นที่นิยม
  • ใหม่
  • เก่า
คุณต้องลงชื่อเข้าใช้เพื่อแสดงความคิดเห็น
หน้านี้ยังไม่มีความคิดเห็นใด ๆ