ทำไมสตรีมแบบขนานกับแลมบ์ดาในตัวเริ่มต้นแบบคงที่ทำให้เกิดการชะงักงัน


86

ฉันเจอสถานการณ์แปลก ๆ ที่การใช้สตรีมคู่ขนานกับแลมบ์ดาในตัวเริ่มต้นแบบคงที่ดูเหมือนตลอดไปโดยไม่มีการใช้งาน CPU นี่คือรหัส:

class Deadlock {
    static {
        IntStream.range(0, 10000).parallel().map(i -> i).count();
        System.out.println("done");
    }
    public static void main(final String[] args) {}
}

นี่ดูเหมือนจะเป็นกรณีทดสอบขั้นต่ำสำหรับการจำลองพฤติกรรมนี้ ถ้าฉัน:

  • วางบล็อกในวิธีการหลักแทนที่จะเป็นตัวเริ่มต้นแบบคงที่
  • ลบการขนานหรือ
  • ถอดแลมด้า

รหัสจะเสร็จสมบูรณ์ทันที ใครสามารถอธิบายพฤติกรรมนี้? มันเป็นจุดบกพร่องหรือตั้งใจ?

ฉันใช้ OpenJDK เวอร์ชัน 1.8.0_66-internal


4
ด้วยช่วง (0, 1) โปรแกรมจะสิ้นสุดตามปกติ ด้วยแฮงค์ (0, 2) หรือสูงกว่า
Laszlo Hirdi

5
คำถามที่คล้ายกัน: stackoverflow.com/questions/34222669/…
Alex - GlassEditor.com

2
อันที่จริงมันเป็นคำถาม / ปัญหาเดียวกันกับ API ที่แตกต่างกัน
Didier L

3
คุณกำลังพยายามใช้คลาสในเธรดพื้นหลังเมื่อคุณยังเริ่มต้นคลาสไม่เสร็จดังนั้นจึงไม่สามารถใช้ในเธรดพื้นหลังได้
Peter Lawrey

4
ความลับของ @ Solomonoff i -> iไม่ใช่การอ้างอิงวิธีการที่static methodใช้ในคลาส Deadlock หากแทนที่i -> iด้วยFunction.identity()รหัสนี้จะไม่เป็นไร
Peter Lawrey

คำตอบ:


71

ฉันพบรายงานข้อบกพร่องของกรณีที่คล้ายกันมาก ( JDK-8143380 ) ซึ่ง Stuart Marks ปิดว่า "ไม่ใช่ปัญหา":

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

ควรเปลี่ยนโปรแกรมทดสอบเพื่อย้ายลอจิกสตรีมคู่ขนานไปนอกคลาสสแตติก initializer ปิดว่าไม่ใช่ปัญหา


ฉันสามารถค้นหารายงานข้อบกพร่องอื่นของสิ่งนั้น ( JDK-8136753 ) และปิดเป็น "ไม่ใช่ปัญหา" โดย Stuart Marks:

นี่คือการหยุดชะงักที่เกิดขึ้นเนื่องจากตัวเริ่มต้นแบบคงที่ของ Fruit enum โต้ตอบกับการเริ่มต้นคลาสไม่ดี

ดูข้อกำหนดภาษา Java ส่วน 12.4.2 สำหรับรายละเอียดเกี่ยวกับการเริ่มต้นคลาส

http://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.4.2

สรุปสิ่งที่เกิดขึ้นมีดังต่อไปนี้

  1. เธรดหลักอ้างอิงคลาส Fruit และเริ่มกระบวนการเริ่มต้น ตั้งค่าแฟล็กที่อยู่ระหว่างการเตรียมใช้งานและรันตัวเริ่มต้นแบบคงที่บนเธรดหลัก
  2. ตัวเริ่มต้นแบบคงที่รันโค้ดบางส่วนในเธรดอื่นและรอให้โค้ดเสร็จสิ้น ตัวอย่างนี้ใช้สตรีมแบบขนาน แต่ไม่เกี่ยวข้องกับสตรีมต่อซี การรันโค้ดในเธรดอื่นด้วยวิธีการใด ๆ และรอให้โค้ดนั้นเสร็จสิ้นจะมีผลเช่นเดียวกัน
  3. โค้ดในเธรดอื่นอ้างอิงคลาส Fruit ซึ่งตรวจสอบแฟล็กที่อยู่ระหว่างการเตรียมใช้งาน ซึ่งทำให้เธรดอื่นบล็อกจนกว่าจะล้างแฟล็ก (ดูขั้นตอนที่ 2 ของ JLS 12.4.2)
  4. เธรดหลักถูกบล็อกเพื่อรอให้เธรดอื่นยุติดังนั้นตัวเริ่มต้นแบบคงที่จะไม่เสร็จสมบูรณ์ เนื่องจากแฟล็กที่อยู่ระหว่างการกำหนดค่าเริ่มต้นจะไม่ถูกล้างจนกว่าจะเสร็จสิ้นการเริ่มต้นแบบคงที่เธรดจึงหยุดชะงัก

เพื่อหลีกเลี่ยงปัญหานี้ตรวจสอบให้แน่ใจว่าการเตรียมใช้งานแบบคงที่ของคลาสเสร็จสิ้นอย่างรวดเร็วโดยไม่ทำให้เธรดอื่นรันโค้ดที่ต้องการให้คลาสนี้เสร็จสิ้นการเตรียมใช้งาน

ปิดว่าไม่ใช่ปัญหา


โปรดทราบว่าFindBugs มีปัญหาในการเพิ่มคำเตือนสำหรับสถานการณ์นี้


20
"นี่ก็ถือว่าเมื่อเราได้รับการออกแบบคุณลักษณะ"และ"เรารู้ว่าสิ่งที่ทำให้เกิดปัญหานี้ แต่ไม่ได้เป็นวิธีที่จะแก้ไขได้"ทำไม่ได้หมายความว่า"นี้ไม่ได้เป็นปัญหา" นี่คือบั๊กอย่างแน่นอน
BlueRaja - Danny Pflughoeft

13
@ bayou.io ปัญหาหลักคือการใช้เธรดภายในตัวเริ่มต้นแบบคงที่ไม่ใช่ lambdas
Stuart Marks

5
BTW Tunaki ขอบคุณสำหรับการขุดรายงานข้อผิดพลาดของฉัน :-)
Stuart Marks

13
@ bayou.io: มันก็เหมือนกันในระดับคลาสเหมือนในคอนสตรัคเตอร์ปล่อยให้thisหนีระหว่างการสร้างอ็อบเจกต์ กฎพื้นฐานคืออย่าใช้การดำเนินการแบบมัลติเธรดในตัวเริ่มต้น ฉันไม่คิดว่าเรื่องนี้จะเข้าใจยาก ตัวอย่างของคุณในการลงทะเบียนฟังก์ชันที่ใช้แลมด้าในรีจิสตรีเป็นสิ่งที่แตกต่างกันมันไม่ได้สร้างการหยุดชะงักเว้นแต่คุณจะรอเธรดพื้นหลังที่ถูกบล็อกเหล่านี้ อย่างไรก็ตามฉันไม่แนะนำอย่างยิ่งที่จะดำเนินการดังกล่าวในตัวเริ่มต้นคลาส ไม่ใช่สิ่งที่พวกเขามีไว้สำหรับ
Holger

9
ฉันเดาว่าบทเรียนรูปแบบการเขียนโปรแกรมคือ: ทำให้ตัวเหนี่ยวนำคงที่เรียบง่าย
Raedwald

16

สำหรับผู้ที่สงสัยว่าเธรดอื่น ๆ ที่อ้างถึงDeadlockคลาสนั้นอยู่ที่ไหนJava lambdas จะทำงานเหมือนที่คุณเขียนไว้

public class Deadlock {
    public static int lambda1(int i) {
        return i;
    }
    static {
        IntStream.range(0, 10000).parallel().map(new IntUnaryOperator() {
            @Override
            public int applyAsInt(int operand) {
                return lambda1(operand);
            }
        }).count();
        System.out.println("done");
    }
    public static void main(final String[] args) {}
}

ด้วยคลาสที่ไม่ระบุชื่อปกติจะไม่มีการหยุดชะงัก:

public class Deadlock {
    static {
        IntStream.range(0, 10000).parallel().map(new IntUnaryOperator() {
            @Override
            public int applyAsInt(int operand) {
                return operand;
            }
        }).count();
        System.out.println("done");
    }
    public static void main(final String[] args) {}
}

5
@ Solomonoff'sSecret เป็นทางเลือกในการใช้งาน รหัสในแลมบ์ดาต้องไปที่ไหนสักแห่ง Javac รวบรวมเป็นวิธีการแบบคงที่ในคลาสที่มี (คล้ายกับlambda1i ตัวอย่างนี้) การใส่แลมด้าแต่ละตัวในคลาสของตัวเองจะมีราคาแพงกว่ามาก
Stuart Marks

1
@StuartMarks ระบุว่าแลมบ์ดาสร้างคลาสที่ใช้อินเทอร์เฟซการทำงานมันจะไม่มีประสิทธิภาพเท่าที่จะนำการใช้แลมด้าไปใช้ในการใช้แลมด้าของอินเทอร์เฟซที่ใช้งานได้เหมือนในตัวอย่างที่สองของโพสต์นี้หรือไม่? นั่นเป็นวิธีที่ชัดเจนในการทำสิ่งต่างๆ แต่ฉันแน่ใจว่ามีเหตุผลว่าทำไมพวกเขาถึงทำอย่างที่เป็นอยู่
คืนสถานะ Monica

6
@ Solomonoff'sSecret lambda อาจสร้างคลาสที่รันไทม์ (ผ่านjava.lang.invoke.LambdaMetafactory ) แต่ต้องวางเนื้อแลมด้าไว้ที่ไหนสักแห่งในเวลาคอมไพล์ คลาสแลมบ์ดาสามารถใช้ประโยชน์จากเวทย์มนตร์ VM บางอย่างเพื่อให้มีราคาถูกกว่าคลาสปกติที่โหลดจากไฟล์. class
Jeffrey Bosboom

1
@ Solomonoff'sSecret ใช่คำตอบของ Jeffrey Bosboom ถูกต้อง หากในอนาคต JVM สามารถเพิ่มเมธอดลงในคลาสที่มีอยู่ได้ metafactory อาจทำเช่นนั้นแทนที่จะปั่นคลาสใหม่ (การเก็งกำไรที่บริสุทธิ์)
Stuart Marks

3
@ ความลับของ Solomonoff: ไม่ได้ตัดสินโดยดูที่การแสดงออกแลมบ์ดาเช่นเล็กน้อยเช่นของคุณi -> i; พวกเขาจะไม่เป็นบรรทัดฐาน นิพจน์แลมบ์ดาอาจใช้สมาชิกทั้งหมดของคลาสที่อยู่รอบ ๆ รวมทั้งคลาสprivateและนั่นทำให้คลาสที่กำหนดเองเป็นสถานที่ตามธรรมชาติ การปล่อยให้กรณีการใช้งานทั้งหมดเหล่านี้ต้องทนทุกข์ทรมานจากการนำไปใช้งานที่ปรับให้เหมาะสมสำหรับกรณีพิเศษของตัวเริ่มต้นคลาสที่มีการใช้นิพจน์แลมบ์ดาแบบหลายเธรดโดยไม่ใช้สมาชิกของคลาสที่กำหนดไม่ใช่ทางเลือกที่เป็นไปได้
Holger

14

มีคำอธิบายที่ยอดเยี่ยมเกี่ยวกับปัญหานี้โดยAndrei Panginซึ่งลงวันที่ 07 เมษายน 2015 มีให้ที่นี่แต่เขียนเป็นภาษารัสเซีย (ฉันขอแนะนำให้ตรวจสอบตัวอย่างโค้ด - เป็นแบบสากล) ปัญหาทั่วไปคือการล็อกระหว่างการเริ่มต้นคลาส

นี่คือคำพูดบางส่วนจากบทความ:


ตามJLSทุกคลาสมีการล็อกการเริ่มต้นที่ไม่ซ้ำกันซึ่งถูกจับระหว่างการเริ่มต้น เมื่อเธรดอื่นพยายามเข้าถึงคลาสนี้ในระหว่างการเริ่มต้นเธรดจะถูกบล็อกในการล็อกจนกว่าการเตรียมใช้งานจะเสร็จสิ้น เมื่อเริ่มต้นคลาสพร้อมกันมันเป็นไปได้ที่จะหยุดชะงัก

ฉันเขียนโปรแกรมง่ายๆที่คำนวณผลรวมของจำนวนเต็มมันควรพิมพ์อะไร?

public class StreamSum {
    static final int SUM = IntStream.range(0, 100).parallel().reduce((n, m) -> n + m).getAsInt();

    public static void main(String[] args) {
        System.out.println(SUM);
    }
} 

ตอนนี้ลบparallel()หรือแทนที่ lambda ด้วยInteger::sumcall - จะเปลี่ยนอะไร

ที่นี่เราเห็นการหยุดชะงักอีกครั้ง [มีตัวอย่างการหยุดชะงักในคลาส initializers ก่อนหน้านี้ในบทความ] เนื่องจากการparallel()ดำเนินการสตรีมทำงานในเธรดพูลแยกต่างหาก เธรดเหล่านี้พยายามเรียกใช้ lambda body ซึ่งเขียนด้วย bytecode เป็นprivate staticวิธีการภายในStreamSumคลาส แต่วิธีนี้ไม่สามารถดำเนินการได้ก่อนที่การเริ่มต้นแบบคงที่ของคลาสจะเสร็จสิ้นซึ่งรอผลของการสตรีมเสร็จสิ้น

สิ่งที่ทำให้นึกถึงได้มากขึ้น: โค้ดนี้ทำงานแตกต่างกันในสภาพแวดล้อมที่แตกต่างกัน มันจะทำงานได้อย่างถูกต้องบนเครื่องซีพียูเครื่องเดียวและส่วนใหญ่มักจะค้างบนเครื่อง CPU หลายตัว ความแตกต่างนี้มาจากการใช้งานพูล Fork-Join คุณสามารถตรวจสอบได้เองว่าเปลี่ยนพารามิเตอร์-Djava.util.concurrent.ForkJoinPool.common.parallelism=N

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.