เพื่อนหลายคนอาจเคยได้ยินคำหลักที่ผันผวนและอาจใช้มัน ก่อนหน้า Java 5 มันเป็นคำหลักที่ขัดแย้งกันเนื่องจากการใช้ในโปรแกรมมักจะส่งผลให้ผลลัพธ์ที่ไม่คาดคิด หลังจาก Java 5 คำหลักที่ผันผวนได้กลับมามีชีวิตชีวาอีกครั้ง
แม้ว่าคำหลักที่ผันผวนจะเข้าใจง่าย แต่ก็ไม่ง่ายเลยที่จะใช้งานได้ดี เนื่องจากคำหลักที่ผันผวนเกี่ยวข้องกับโมเดลหน่วยความจำของ Java ก่อนที่จะบอกคีย์ที่ผันผวนก่อนอื่นเราจึงเข้าใจแนวคิดและความรู้ที่เกี่ยวข้องกับโมเดลหน่วยความจำจากนั้นวิเคราะห์หลักการการนำไปปฏิบัติของคำหลักที่ผันผวน
นี่คือโครงร่างไดเรกทอรีของบทความนี้:
1. แนวคิดที่เกี่ยวข้องของโมเดลหน่วยความจำ
ดังที่เราทุกคนรู้เมื่อคอมพิวเตอร์ดำเนินการโปรแกรมแต่ละคำสั่งจะถูกดำเนินการใน CPU และในระหว่างการดำเนินการตามคำสั่งมันจะเกี่ยวข้องกับการอ่านและการเขียนข้อมูลอย่างหลีกเลี่ยงไม่ได้ เนื่องจากข้อมูลชั่วคราวระหว่างการดำเนินการโปรแกรมจะถูกเก็บไว้ในหน่วยความจำหลัก (หน่วยความจำกายภาพ) จึงมีปัญหาในเวลานี้ เนื่องจากความเร็วในการดำเนินการของ CPU นั้นเร็วมากกระบวนการอ่านข้อมูลจากหน่วยความจำและการเขียนข้อมูลไปยังหน่วยความจำนั้นช้ากว่าการดำเนินการตามคำสั่งของ CPU มาก ดังนั้นหากการดำเนินการข้อมูลต้องดำเนินการผ่านการโต้ตอบกับหน่วยความจำได้ตลอดเวลาความเร็วของการดำเนินการตามคำสั่งจะลดลงอย่างมาก ดังนั้นจึงมีแคชใน CPU
นั่นคือเมื่อโปรแกรมกำลังทำงานมันจะคัดลอกข้อมูลที่ต้องการโดยการดำเนินการจากหน่วยความจำหลักไปยังแคชของ CPU จากนั้นเมื่อ CPU ทำการคำนวณสามารถอ่านข้อมูลโดยตรงจากแคชและเขียนข้อมูลลงไป หลังจากการดำเนินการเสร็จสิ้นข้อมูลในแคชจะถูกล้างเข้าไปในหน่วยความจำหลัก ยกตัวอย่างง่ายๆเช่นรหัสต่อไปนี้:
i = i + 1;
เมื่อเธรดดำเนินการคำสั่งนี้มันจะอ่านค่าของ I จากหน่วยความจำหลักก่อนจากนั้นคัดลอกสำเนาลงในแคชแล้ว CPU จะดำเนินการคำสั่งเพื่อเพิ่ม 1 ถึง i จากนั้นเขียนข้อมูลลงในแคชและในที่สุดก็รีเฟรชค่าล่าสุดของ I ในแคชลงในหน่วยความจำหลัก
ไม่มีปัญหากับรหัสนี้ที่ทำงานในเธรดเดียว แต่จะมีปัญหาเมื่อทำงานในมัลติเธรด ในซีพียูหลายคอร์แต่ละเธรดอาจทำงานในซีพียูที่แตกต่างกันดังนั้นแต่ละเธรดมีแคชของตัวเองเมื่อทำงาน (สำหรับซีพียูคอร์เดี่ยวปัญหานี้จะเกิดขึ้นจริง แต่มันถูกดำเนินการแยกต่างหากในรูปแบบของการตั้งเวลาเธรด) ในบทความนี้เราใช้ CPU แบบมัลติคอร์เป็นตัวอย่าง
ตัวอย่างเช่นสองเธรดเรียกใช้รหัสนี้ในเวลาเดียวกัน หากค่าของฉันเป็น 0 ในตอนแรกเราหวังว่าค่าของฉันจะกลายเป็น 2 หลังจากสองเธรดได้ดำเนินการ แต่นี่จะเป็นกรณีนี้หรือไม่?
อาจมีหนึ่งในสถานการณ์ต่อไปนี้: ในตอนแรกสองเธรดอ่านค่าของ I และเก็บไว้ในแคชของซีพียูที่เกี่ยวข้องจากนั้นเธรด 1 จะดำเนินการเพิ่ม 1 แล้วเขียนค่าล่าสุดของ I to Memory ในเวลานี้ค่าของ I ในแคชของเธรด 2 ยังคงเป็น 0 หลังจากดำเนินการ 1 การดำเนินการค่าของฉันคือ 1 จากนั้นเธรด 2 จะเขียนค่าของ I ถึงหน่วยความจำ
ค่าของผลลัพธ์สุดท้ายฉันคือ 1 ไม่ใช่ 2 นี่คือปัญหาความสอดคล้องของแคชที่มีชื่อเสียง ตัวแปรนี้ที่เข้าถึงได้หลายเธรดมักจะเรียกว่าตัวแปรที่ใช้ร่วมกัน
กล่าวคือหากตัวแปรถูกแคชในหลายซีพียู (โดยปกติจะเกิดขึ้นในระหว่างการเขียนโปรแกรมมัลติเธรด) ดังนั้นอาจมีปัญหาของความไม่สอดคล้องกันของแคช
เพื่อแก้ปัญหาความไม่สอดคล้องกันของแคชมักจะมีสองวิธีแก้ปัญหา:
1) โดยการเพิ่มล็อค# ล็อคไปยังบัส
2) ผ่านโปรโตคอลการเชื่อมโยงแคช
สองวิธีนี้มีให้ที่ระดับฮาร์ดแวร์
ในซีพียูต้นปัญหาของความไม่สอดคล้องกันของแคชได้รับการแก้ไขโดยการเพิ่มล็อค# ล็อคลงในบัส เนื่องจากการสื่อสารระหว่าง CPU และส่วนประกอบอื่น ๆ ดำเนินการผ่านบัสหากมีการเพิ่มบัสด้วยล็อค# ล็อคหมายความว่าซีพียูอื่น ๆ จะถูกบล็อกไม่ให้เข้าถึงส่วนประกอบอื่น ๆ (เช่นหน่วยความจำ) ดังนั้น CPU เพียงตัวเดียวเท่านั้นที่สามารถใช้หน่วยความจำของตัวแปรนี้ได้ ตัวอย่างเช่นในตัวอย่างข้างต้นหากเธรดกำลังดำเนินการ i = i +1 และหากสัญญาณ LCOK# LOCK ถูกส่งบนบัสในระหว่างการดำเนินการของรหัสนี้หลังจากรอการดำเนินการโค้ดอย่างเต็มที่ซีพียูอื่น ๆ สามารถอ่านตัวแปรจากหน่วยความจำที่ตัวแปรที่ฉันอยู่ สิ่งนี้แก้ปัญหาความไม่สอดคล้องกันของแคช
แต่วิธีการข้างต้นจะมีปัญหาเนื่องจากซีพียูอื่นไม่สามารถเข้าถึงหน่วยความจำระหว่างการล็อคบัสส่งผลให้ไม่มีประสิทธิภาพ
ดังนั้นโปรโตคอลความสอดคล้องของแคชจึงปรากฏขึ้น สิ่งที่มีชื่อเสียงที่สุดคือโปรโตคอล MESI ของ Intel ซึ่งทำให้มั่นใจได้ว่าสำเนาของตัวแปรที่ใช้ร่วมกันที่ใช้ในแต่ละแคชนั้นสอดคล้องกัน แนวคิดหลักของมันคือ: เมื่อ CPU เขียนข้อมูลหากพบว่าตัวแปรที่ดำเนินการเป็นตัวแปรที่ใช้ร่วมกันนั่นคือมีสำเนาของตัวแปรในซีพียูอื่น ๆ มันจะส่งสัญญาณซีพียูอื่น ๆ เพื่อตั้งค่าแคชของตัวแปรให้อยู่ในสถานะที่ไม่ถูกต้อง ดังนั้นเมื่อซีพียูอื่น ๆ จำเป็นต้องอ่านตัวแปรนี้และพบว่าสายแคชที่แคชตัวแปรในแคชของพวกเขาไม่ถูกต้องแล้วมันจะอ่านจากหน่วยความจำอีกครั้ง
2. สามแนวคิดในการเขียนโปรแกรมพร้อมกัน
ในการเขียนโปรแกรมพร้อมกันเรามักจะพบปัญหาสามประการต่อไปนี้: ปัญหาเกี่ยวกับอะตอม, ปัญหาการมองเห็นและปัญหาที่เป็นระเบียบ มาดูแนวคิดทั้งสามนี้ก่อน:
1. atomicity
Atomicity: นั่นคือการดำเนินการหนึ่งครั้งหรือการดำเนินการหลายครั้งจะดำเนินการทั้งหมดและกระบวนการดำเนินการจะไม่ถูกขัดจังหวะโดยปัจจัยใด ๆ หรือจะไม่ถูกดำเนินการ
ตัวอย่างที่คลาสสิกมากคือปัญหาการโอนบัญชีธนาคาร:
ตัวอย่างเช่นหากคุณโอนเงิน 1,000 หยวนจากบัญชี A ไปยังบัญชี B คุณจะต้องรวมการดำเนินงาน 2 ครั้งอย่างหลีกเลี่ยงไม่ได้: ลบ 1,000 หยวนออกจากบัญชี A และเพิ่ม 1,000 หยวนลงในบัญชี B.
แค่คิดว่าผลที่ตามมาจะเกิดขึ้นหากการดำเนินการทั้งสองนี้ไม่ใช่อะตอม หาก 1,000 หยวนถูกลบออกจากบัญชี A การดำเนินการจะถูกยกเลิกทันที จากนั้น 500 หยวนถูกถอนออกจาก B และหลังจากถอน 500 หยวนจากนั้นการดำเนินการเพิ่ม 1,000 หยวนลงในบัญชี B. สิ่งนี้จะนำไปสู่ข้อเท็จจริงที่ว่าแม้ว่าบัญชี A จะลบ 1,000 หยวน แต่บัญชี B ยังไม่ได้รับโอน 1,000 หยวน
ดังนั้นการดำเนินการทั้งสองนี้จะต้องเป็นอะตอมเพื่อให้แน่ใจว่าไม่มีปัญหาที่ไม่คาดคิด
ผลลัพธ์ที่จะสะท้อนในการเขียนโปรแกรมพร้อมกันคืออะไร?
เพื่อให้ตัวอย่างที่ง่ายที่สุดลองคิดดูว่าจะเกิดอะไรขึ้นหากกระบวนการกำหนดตัวแปร 32 บิตไม่ใช่อะตอม?
i = 9;
หากเธรดดำเนินการคำสั่งนี้ฉันจะสมมติว่าการกำหนดตัวแปร 32 บิตนั้นมีสองกระบวนการ: การกำหนด 16 บิตที่ต่ำกว่าและการมอบหมายของ 16 บิตที่สูงขึ้น
จากนั้นสถานการณ์อาจเกิดขึ้น: เมื่อมีการเขียนค่า 16 บิตต่ำมันก็ถูกขัดจังหวะทันทีและในเวลานี้เธรดอื่นอ่านค่าของฉันแล้วสิ่งที่อ่านคือข้อมูลที่ไม่ถูกต้อง
2. ทัศนวิสัย
การมองเห็นหมายถึงเมื่อหลายเธรดเข้าถึงตัวแปรเดียวกันเธรดหนึ่งจะแก้ไขค่าของตัวแปรและเธรดอื่น ๆ สามารถเห็นค่าที่แก้ไขได้ทันที
สำหรับตัวอย่างง่ายๆให้ดูรหัสต่อไปนี้:
// รหัสที่ดำเนินการโดยเธรด 1 คือ int i = 0; i = 10; // รหัสที่ดำเนินการโดยเธรด 2 คือ j = i;
หากเธรดการดำเนินการ 1 คือ CPU1 และเธรดการดำเนินการ 2 คือ CPU2 จากการวิเคราะห์ข้างต้นเราจะเห็นว่าเมื่อเธรด 1 ดำเนินการประโยค i = 10 ค่าเริ่มต้นของฉันจะถูกโหลดลงในแคชของ CPU1 จากนั้นกำหนดค่า 10 จากนั้นค่าของ i ในแคชของ CPU1 จะกลายเป็น 10 แต่ไม่ได้เขียนลงในหน่วยความจำหลักทันที
ในเวลานี้เธรด 2 จะดำเนินการ j = i และมันจะไปที่หน่วยความจำหลักก่อนเพื่ออ่านค่าของ i และโหลดลงในแคชของ CPU2 โปรดทราบว่าค่าของฉันในหน่วยความจำยังคงเป็น 0 ดังนั้นค่าของ j จะเป็น 0 ไม่ใช่ 10
นี่คือปัญหาการมองเห็น หลังจากเธรด 1 แก้ไขตัวแปร I แล้วเธรด 2 จะไม่เห็นค่าที่แก้ไขโดยเธรด 1 ทันที
3. คำสั่งซื้อ
คำสั่งซื้อ: นั่นคือคำสั่งของการดำเนินการของโปรแกรมจะดำเนินการตามลำดับของรหัส สำหรับตัวอย่างง่ายๆให้ดูรหัสต่อไปนี้:
int i = 0; ธงบูลีน = false; i = 1; // คำสั่ง 1 ธง = true; // คำสั่ง 2
รหัสข้างต้นกำหนดตัวแปรประเภท int ซึ่งเป็นตัวแปรประเภทบูลีนจากนั้นกำหนดค่าให้กับตัวแปรสองตัวตามลำดับ จากมุมมองของลำดับรหัสคำสั่ง 1 คือก่อนคำสั่ง 2 ดังนั้นเมื่อ JVM เรียกใช้งานรหัสนี้จริง ๆ แล้วจะมั่นใจได้ว่าคำสั่ง 1 จะถูกดำเนินการก่อนคำสั่ง 2 หรือไม่ ไม่จำเป็นทำไม? คำแนะนำใหม่อาจเกิดขึ้นที่นี่
มาอธิบายว่าการสั่งซื้อใหม่คืออะไร โดยทั่วไปเพื่อปรับปรุงประสิทธิภาพการทำงานของโปรแกรมโปรเซสเซอร์อาจเพิ่มประสิทธิภาพรหัสอินพุต ไม่แน่ใจว่าคำสั่งการดำเนินการของแต่ละคำสั่งในโปรแกรมสอดคล้องกับคำสั่งซื้อในรหัส แต่จะทำให้มั่นใจได้ว่าผลการดำเนินการขั้นสุดท้ายของโปรแกรมและผลลัพธ์ของลำดับการดำเนินการรหัสนั้นสอดคล้องกัน
ตัวอย่างเช่นในรหัสข้างต้นซึ่งดำเนินการคำสั่ง 1 และคำสั่ง 2 ก่อนไม่มีผลต่อผลลัพธ์ของโปรแกรมสุดท้ายจากนั้นก็เป็นไปได้ว่าในระหว่างกระบวนการดำเนินการคำสั่ง 2 จะถูกดำเนินการก่อนและคำสั่ง 1 จะถูกดำเนินการในภายหลัง
แต่โปรดทราบว่าถึงแม้ว่าโปรเซสเซอร์จะสั่งซื้อคำแนะนำใหม่ แต่จะทำให้มั่นใจได้ว่าผลลัพธ์สุดท้ายของโปรแกรมจะเหมือนกับลำดับการดำเนินการรหัส แล้วสิ่งที่รับประกันได้คืออะไร? มาดูตัวอย่างต่อไปนี้:
int a = 10; // คำสั่ง 1int r = 2; // คำสั่ง 2a = a + 3; // คำสั่ง 3r = a*a; // คำสั่ง 4
รหัสนี้มี 4 คำสั่งดังนั้นคำสั่งดำเนินการที่เป็นไปได้คือ:
เป็นไปได้หรือไม่ที่จะเป็นคำสั่งดำเนินการ: คำสั่ง 2 คำสั่ง 1 คำสั่ง 4 คำสั่ง 4 3 คำสั่ง 3 3
เป็นไปไม่ได้เนื่องจากโปรเซสเซอร์จะพิจารณาการพึ่งพาข้อมูลระหว่างคำแนะนำเมื่อทำการสั่งซื้อใหม่ หากคำสั่งคำสั่ง 2 ต้องใช้ผลลัพธ์ของคำสั่ง 1 โปรเซสเซอร์จะมั่นใจได้ว่าคำสั่ง 1 จะถูกดำเนินการก่อนคำสั่ง 2
แม้ว่าการจัดลำดับใหม่จะไม่ส่งผลกระทบต่อผลลัพธ์ของการดำเนินการโปรแกรมภายในเธรดเดียว แต่สิ่งที่เกี่ยวกับ multithreading? มาดูตัวอย่างด้านล่าง:
// เธรด 1: บริบท = loadContext (); // state 1inited = true; // state 2 // เธรด 2: ในขณะที่ (! inited) {sleep ()} dosomethingwithconfig (บริบท);ในรหัสข้างต้นเนื่องจากคำสั่ง 1 และ 2 ไม่มีการพึ่งพาข้อมูลพวกเขาอาจถูกจัดลำดับใหม่ หากการจัดลำดับใหม่เกิดขึ้นคำสั่ง 2 จะถูกดำเนินการครั้งแรกในระหว่างการดำเนินการของเธรด 1 และนี่คือเธรด 2 จะคิดว่างานการเริ่มต้นเสร็จสมบูรณ์แล้วมันจะกระโดดออกจากวงในขณะที่ใช้วิธีการ DosomethingWithConfig (บริบท) ในเวลานี้บริบทไม่ได้เริ่มต้นซึ่งจะทำให้เกิดข้อผิดพลาดของโปรแกรม
ดังที่เห็นได้จากข้างต้นการสั่งซื้อการสั่งซื้อใหม่จะไม่ส่งผลกระทบต่อการดำเนินการของเธรดเดียว แต่จะส่งผลกระทบต่อความถูกต้องของการดำเนินการพร้อมกันของเธรดพร้อมกัน
กล่าวอีกนัยหนึ่งเพื่อที่จะดำเนินการโปรแกรมพร้อมกันอย่างถูกต้องอะตอม, การมองเห็นและความเป็นระเบียบต้องมั่นใจ ตราบใดที่ไม่รับประกันว่าอาจทำให้โปรแกรมทำงานไม่ถูกต้อง
3. Java Memory Model
ฉันพูดคุยเกี่ยวกับปัญหาบางอย่างที่อาจเกิดขึ้นในโมเดลหน่วยความจำและการเขียนโปรแกรมพร้อมกัน ลองมาดูโมเดลหน่วยความจำ Java และศึกษาสิ่งที่รับประกันว่าโมเดลหน่วยความจำ Java จะจัดเตรียมไว้ให้เราและวิธีการและกลไกใดที่มีอยู่ใน Java เพื่อให้แน่ใจว่าการดำเนินการโปรแกรมถูกต้องเมื่อทำการเขียนโปรแกรมแบบหลายเธรด
ในข้อกำหนดของเครื่องเสมือน Java มันพยายามที่จะกำหนดโมเดลหน่วยความจำ Java (JMM) เพื่อป้องกันความแตกต่างของการเข้าถึงหน่วยความจำระหว่างแพลตฟอร์มฮาร์ดแวร์ต่างๆและระบบปฏิบัติการเพื่อให้โปรแกรม Java บรรลุเอฟเฟกต์การเข้าถึงหน่วยความจำที่สอดคล้องกันบนแพลตฟอร์มต่างๆ ดังนั้นโมเดลหน่วยความจำ Java กำหนดอะไร? มันกำหนดกฎการเข้าถึงสำหรับตัวแปรในโปรแกรม เพื่อให้มันกว้างขึ้นจะกำหนดลำดับของการดำเนินการโปรแกรม โปรดทราบว่าเพื่อให้ได้ประสิทธิภาพการดำเนินการที่ดีขึ้นโมเดลหน่วยความจำ Java ไม่ได้ จำกัด การดำเนินการเอ็นจิ้นจากการใช้การลงทะเบียนหรือแคชของโปรเซสเซอร์เพื่อปรับปรุงความเร็วในการดำเนินการตามคำสั่งและไม่ จำกัด คอมไพเลอร์เพื่อสั่งซื้อคำสั่งใหม่ กล่าวอีกนัยหนึ่งในโมเดลหน่วยความจำ Java จะมีปัญหาความสอดคล้องของแคชและปัญหาการสั่งซื้อใหม่
โมเดลหน่วยความจำ Java กำหนดว่าตัวแปรทั้งหมดอยู่ในหน่วยความจำหลัก (คล้ายกับหน่วยความจำทางกายภาพที่กล่าวถึงข้างต้น) และแต่ละเธรดมีหน่วยความจำการทำงานของตัวเอง (คล้ายกับแคชก่อนหน้า) การดำเนินการทั้งหมดของเธรดบนตัวแปรจะต้องดำเนินการในหน่วยความจำการทำงานและไม่สามารถทำงานได้โดยตรงกับหน่วยความจำหลัก และแต่ละเธรดไม่สามารถเข้าถึงหน่วยความจำที่ใช้งานได้ของเธรดอื่น ๆ
เพื่อให้ตัวอย่างง่ายๆ: ใน Java ให้ดำเนินการคำสั่งต่อไปนี้:
i = 10;
เธรดการดำเนินการจะต้องกำหนดบรรทัดแคชซึ่งตัวแปรที่ฉันอยู่ในเธรดการทำงานของตัวเองจากนั้นเขียนลงในหน่วยความจำหลัก แทนที่จะเขียนค่า 10 ลงในหน่วยความจำหลักโดยตรง
ดังนั้นภาษาชวาที่รับประกันได้ว่าจะมีความเป็นปรมาณูทัศนวิสัยและความเป็นระเบียบอย่างไร?
1. atomicity
ใน Java การดำเนินการอ่านและการมอบหมายของตัวแปรของชนิดข้อมูลพื้นฐานคือการดำเนินการอะตอมนั่นคือการดำเนินการเหล่านี้ไม่สามารถขัดจังหวะและดำเนินการหรือไม่
แม้ว่าประโยคข้างต้นจะดูง่าย แต่ก็ไม่ง่ายที่จะเข้าใจ ดูตัวอย่างต่อไปนี้ฉัน:
โปรดวิเคราะห์ว่าการดำเนินการใดต่อไปนี้คือการดำเนินการอะตอม:
x = 10; // คำสั่ง 1y = x; // คำสั่ง 2x ++; // คำสั่ง 3x = x + 1; // คำสั่ง 4
เมื่อมองแวบแรกเพื่อนบางคนอาจบอกว่าการดำเนินการในสี่ข้อความข้างต้นเป็นการดำเนินการอะตอมทั้งหมด ในความเป็นจริงมีเพียงคำแถลง 1 คือการดำเนินการอะตอมและไม่มีคำแถลงอีกสามข้อความที่เป็นการดำเนินการอะตอม
คำสั่ง 1 กำหนดค่า 10 ถึง X โดยตรงซึ่งหมายความว่าเธรดดำเนินการคำสั่งนี้และเขียนค่า 10 ลงในหน่วยความจำการทำงานโดยตรง
คำแถลงที่ 2 มีการดำเนินการ 2 ครั้ง ก่อนอื่นต้องอ่านค่าของ X จากนั้นเขียนค่าของ X ไปยังหน่วยความจำที่ใช้งานได้ แม้ว่าการดำเนินการทั้งสองของการอ่านค่าของ X และการเขียนค่าของ X ไปยังหน่วยความจำที่ทำงานคือการดำเนินการอะตอม
ในทำนองเดียวกัน x ++ และ x = x+1 รวมถึง 3 การดำเนินการ: อ่านค่าของ x ดำเนินการของการเพิ่ม 1 และเขียนค่าใหม่
ดังนั้นเฉพาะการดำเนินการของคำสั่ง 1 ในสี่ข้อความข้างต้นเป็นอะตอม
กล่าวอีกนัยหนึ่งการอ่านและการมอบหมายง่าย ๆ เท่านั้น (และจำนวนจะต้องได้รับการกำหนดให้กับตัวแปรและการกำหนดร่วมกันระหว่างตัวแปรไม่ใช่การดำเนินการอะตอม) เป็นการดำเนินการอะตอม
อย่างไรก็ตามมีสิ่งหนึ่งที่ควรทราบที่นี่: ภายใต้แพลตฟอร์ม 32 บิตการอ่านและการกำหนดข้อมูล 64 บิตจะต้องเสร็จสิ้นผ่านการดำเนินการสองครั้งและไม่สามารถรับประกันความผิดปกติได้ อย่างไรก็ตามดูเหมือนว่าใน JDK ล่าสุด JVM ได้มั่นใจว่าการอ่านและการกำหนดข้อมูล 64 บิตก็เป็นการดำเนินการอะตอม
จากด้านบนจะเห็นได้ว่าโมเดลหน่วยความจำ Java เพียงเพื่อให้มั่นใจว่าการอ่านและการมอบหมายขั้นพื้นฐานเป็นการดำเนินการอะตอม หากคุณต้องการบรรลุความเป็นอะตอมของการดำเนินการที่มีขนาดใหญ่ขึ้นก็สามารถทำได้ผ่านการซิงโครไนซ์และล็อค เนื่องจากซิงโครไนซ์และล็อคสามารถมั่นใจได้ว่ามีเพียงหนึ่งเธรดที่ดำเนินการบล็อกรหัสได้ตลอดเวลาจึงไม่มีปัญหาเกี่ยวกับอะตอมดังนั้นจึงทำให้มั่นใจได้ว่าเป็นอะตอม
2. ทัศนวิสัย
เพื่อการมองเห็น Java ให้คำหลักที่ผันผวนเพื่อให้แน่ใจว่ามีการมองเห็น
เมื่อตัวแปรที่ใช้ร่วมกันถูกแก้ไขโดยความผันผวนจะช่วยให้มั่นใจได้ว่าค่าที่แก้ไขจะได้รับการปรับปรุงเป็นหน่วยความจำหลักทันทีและเมื่อเธรดอื่นจำเป็นต้องอ่านมันจะอ่านค่าใหม่ในหน่วยความจำ
อย่างไรก็ตามตัวแปรที่ใช้ร่วมกันทั่วไปไม่สามารถรับประกันการมองเห็นได้เนื่องจากไม่แน่นอนเมื่อตัวแปรที่ใช้ร่วมกันปกติถูกเขียนไปยังหน่วยความจำหลักหลังจากแก้ไขแล้ว เมื่อเธรดอื่นอ่านค่าเดิมอาจยังอยู่ในหน่วยความจำดังนั้นจึงไม่สามารถรับประกันการมองเห็นได้
นอกจากนี้การซิงโครไนซ์และล็อคยังสามารถตรวจสอบการมองเห็นได้ ซิงโครไนซ์และล็อคสามารถตรวจสอบให้แน่ใจว่ามีเพียงหนึ่งเธรดที่ได้รับการล็อคในเวลาเดียวกันและเรียกใช้รหัสการซิงโครไนซ์ ก่อนที่จะปล่อยล็อคการปรับเปลี่ยนของตัวแปรจะถูกรีเฟรชไปยังหน่วยความจำหลัก ดังนั้นการมองเห็นสามารถรับประกันได้
3. คำสั่งซื้อ
ในโมเดลหน่วยความจำ Java คอมไพเลอร์และโปรเซสเซอร์ได้รับอนุญาตให้จัดลำดับคำแนะนำใหม่ แต่กระบวนการจัดเรียงใหม่จะไม่ส่งผลกระทบต่อการดำเนินการของโปรแกรมเธรดเดี่ยว แต่จะส่งผลกระทบต่อความถูกต้องของการดำเนินการพร้อมกันหลายเธรด
ใน Java "คำสั่ง" บางอย่างสามารถรับรองได้ผ่านคำหลักที่ผันผวน (มีการอธิบายหลักการเฉพาะในส่วนถัดไป) นอกจากนี้ยังสามารถใช้การซิงโครไนซ์และล็อคเพื่อให้แน่ใจว่ามีการสั่งซื้อ เห็นได้ชัดว่าซิงโครไนซ์และล็อคตรวจสอบให้แน่ใจว่ามีเธรดที่ดำเนินการรหัสการซิงโครไนซ์ในแต่ละช่วงเวลาซึ่งเทียบเท่ากับการปล่อยให้เธรดดำเนินการรหัสการซิงโครไนซ์ตามลำดับ
นอกจากนี้โมเดลหน่วยความจำ Java ยังมี "ระเบียบ" โดยธรรมชาติบางอย่างนั่นคือสามารถรับประกันได้โดยไม่ต้องมีวิธีการใด ๆ ซึ่งมักจะเรียกว่าหลักการที่เกิดขึ้นก่อน หากคำสั่งการดำเนินการของการดำเนินการสองครั้งไม่สามารถได้มาจากหลักการที่เกิดขึ้นก่อนหน้านี้พวกเขาไม่สามารถรับประกันความเป็นระเบียบเรียบร้อยและเครื่องเสมือนจริงของพวกเขาสามารถสั่งซื้อใหม่ได้ตามต้องการ
มาแนะนำหลักการที่เกิดขึ้นก่อน (หลักการสำคัญที่เกิดขึ้น):
หลักการทั้ง 8 นี้ถูกตัดตอนมาจาก "ความเข้าใจในเชิงลึกของเครื่องเสมือน Java"
ในบรรดากฎ 8 ข้อนี้กฎ 4 ข้อแรกมีความสำคัญมากกว่าในขณะที่กฎ 4 ข้อสุดท้ายนั้นชัดเจนทั้งหมด
มาอธิบายกฎ 4 ข้อแรกด้านล่าง:
สำหรับกฎการสั่งซื้อโปรแกรมความเข้าใจของฉันคือการดำเนินการของรหัสโปรแกรมดูเหมือนว่าจะสั่งซื้อในเธรดเดียว โปรดทราบว่าถึงแม้ว่ากฎนี้จะระบุว่า "การดำเนินการที่เขียนไว้ด้านหน้าเกิดขึ้นก่อนในการดำเนินการที่เขียนไว้ในด้านหลัง" นี่ควรเป็นลำดับที่โปรแกรมดูเหมือนจะดำเนินการในลำดับรหัสเนื่องจากเครื่องเสมือนอาจสั่งรหัสโปรแกรมใหม่ แม้ว่าจะมีการจัดลำดับใหม่ แต่ผลการดำเนินการขั้นสุดท้ายนั้นสอดคล้องกับการดำเนินการตามลำดับของโปรแกรมและจะจัดลำดับคำแนะนำใหม่ที่ไม่มีการพึ่งพาข้อมูล ดังนั้นในเธรดเดียวการดำเนินการโปรแกรมดูเหมือนจะดำเนินการในลักษณะที่เป็นระเบียบซึ่งควรเข้าใจด้วยความระมัดระวัง ในความเป็นจริงกฎนี้ใช้เพื่อให้แน่ใจว่าความถูกต้องของผลลัพธ์การดำเนินการของโปรแกรมในเธรดเดียว แต่ไม่สามารถรับประกันความถูกต้องของโปรแกรมในลักษณะหลายเธรด
กฎข้อที่สองนั้นง่ายกว่าที่จะเข้าใจนั่นคือถ้าล็อคเดียวกันอยู่ในสถานะล็อคมันจะต้องได้รับการปล่อยตัวก่อนที่การดำเนินการล็อคจะดำเนินต่อไป
กฎข้อที่สามเป็นกฎที่ค่อนข้างสำคัญและเป็นสิ่งที่จะกล่าวถึงในภายหลัง อย่างสังหรณ์ใจถ้าเธรดเขียนตัวแปรก่อนจากนั้นเธรดจะอ่านแล้วการดำเนินการเขียนจะเกิดขึ้นก่อนในการดำเนินการอ่าน
กฎข้อที่สี่สะท้อนให้เห็นว่าหลักการที่เกิดขึ้นก่อนหน้านี้เป็นสกรรมกริยา
4. การวิเคราะห์เชิงลึกของคำหลักที่ผันผวน
ฉันเคยพูดคุยเกี่ยวกับสิ่งต่าง ๆ มากมายมาก่อน แต่จริง ๆ แล้วพวกเขากำลังปูทางที่จะบอกคำหลักที่ผันผวนดังนั้นมาถึงหัวข้อ
1. ความหมายสองชั้นของคำหลักที่ผันผวน
เมื่อตัวแปรที่ใช้ร่วมกัน (ตัวแปรสมาชิกชั้นเรียนตัวแปรสมาชิกระดับสแตติก) จะถูกแก้ไขโดยผันผวนจะมีความหมายสองชั้น:
1) ตรวจสอบให้แน่ใจว่ามีการมองเห็นเธรดที่แตกต่างกันเมื่อใช้งานตัวแปรนี้นั่นคือหนึ่งเธรดหนึ่งจะปรับเปลี่ยนค่าของตัวแปรที่แน่นอนและค่าใหม่นี้จะปรากฏขึ้นทันทีสำหรับเธรดอื่น ๆ
2) ห้ามมิให้สั่งซื้อใหม่
มาดูรหัสชิ้นหนึ่งก่อน หากเธรด 1 ถูกเรียกใช้ก่อนและเธรด 2 จะถูกดำเนินการในภายหลัง:
// เธรด 1boolean stop = false; ในขณะที่ (! หยุด) {dosomething ();} // เธรด 2stop = true;รหัสนี้เป็นโค้ดทั่วไปและหลายคนอาจใช้วิธีการมาร์กอัปนี้เมื่อขัดจังหวะเธรด แต่อันที่จริงแล้วรหัสนี้จะทำงานได้อย่างถูกต้องหรือไม่? เธรดจะถูกขัดจังหวะหรือไม่? ไม่จำเป็น บางทีเวลาส่วนใหญ่รหัสนี้สามารถขัดจังหวะเธรดได้ แต่อาจทำให้เธรดไม่ถูกขัดจังหวะ (แม้ว่าความเป็นไปได้นี้จะเล็กมากเมื่อสิ่งนี้เกิดขึ้นมันจะทำให้เกิดการวนซ้ำ)
มาอธิบายว่าทำไมรหัสนี้อาจทำให้เธรดไม่ขัดจังหวะ ตามที่อธิบายไว้ก่อนหน้านี้แต่ละเธรดมีหน่วยความจำการทำงานของตัวเองในระหว่างการทำงานดังนั้นเมื่อเธรด 1 ทำงานอยู่มันจะคัดลอกค่าของตัวแปรหยุดและใส่ไว้ในหน่วยความจำการทำงานของตัวเอง
จากนั้นเมื่อเธรด 2 เปลี่ยนค่าของตัวแปรหยุด แต่ไม่มีเวลาเขียนลงในหน่วยความจำหลักเธรด 2 จะทำสิ่งอื่น ๆ จากนั้นเธรด 1 ไม่ทราบเกี่ยวกับการเปลี่ยนแปลงของเธรด 2 ในตัวแปรหยุดดังนั้นมันจะยังคงวนเวียนอยู่ต่อไป
แต่หลังจากแก้ไขด้วยความผันผวนแล้วมันจะแตกต่างกัน:
ครั้งแรก: การใช้คำหลักที่ผันผวนจะบังคับให้ค่าที่แก้ไขจะถูกเขียนไปยังหน่วยความจำหลักทันที
ประการที่สอง: หากคุณใช้คำหลักที่ผันผวนเมื่อเธรด 2 ปรับเปลี่ยนบรรทัดแคชของตัวแปรแคชหยุดในหน่วยความจำการทำงานของเธรด 1 จะไม่ถูกต้อง (หากสะท้อนให้เห็นในชั้นฮาร์ดแวร์บรรทัดแคชที่สอดคล้องกันในแคช L1 หรือ L2 ของ CPU ไม่ถูกต้อง);
ประการที่สาม: เนื่องจากสายแคชของตัวแปรแคชหยุดในหน่วยความจำการทำงานของเธรด 1 ไม่ถูกต้องเธรด 1 จะอ่านในหน่วยความจำหลักเมื่ออ่านค่าของตัวแปรหยุดอีกครั้ง
จากนั้นเมื่อเธรด 2 ปรับเปลี่ยนค่าหยุด (แน่นอนมี 2 การดำเนินการที่นี่แก้ไขค่าในหน่วยความจำการทำงานของเธรด 2 จากนั้นเขียนค่าที่แก้ไขไปยังหน่วยความจำ) สายแคชของตัวแปรแคชหยุดในหน่วยความจำการทำงานของเธรด 1 จะไม่ถูกต้อง เมื่อเธรด 1 อ่านพบว่าสายแคชนั้นไม่ถูกต้อง มันจะรอให้ที่อยู่หน่วยความจำหลักที่สอดคล้องกันของสายแคชที่จะอัปเดตจากนั้นอ่านค่าล่าสุดในหน่วยความจำหลักที่เกี่ยวข้อง
จากนั้นเธรด 1 อ่านเป็นค่าที่ถูกต้องล่าสุด
2. การผันผวนรับประกันความเป็นอะตอมหรือไม่?
จากข้างต้นเรารู้ว่าคำหลักที่ผันผวนช่วยให้มั่นใจได้ว่าการมองเห็นของการดำเนินงาน แต่สามารถระเหยได้มั่นใจได้ว่าการดำเนินการของตัวแปรนั้นเป็นอะตอมหรือไม่?
มาดูตัวอย่างด้านล่าง:
การทดสอบระดับสาธารณะ {สาธารณะผันผวน int inc = 0; โมฆะสาธารณะเพิ่ม () {inc ++; } โมฆะคงที่สาธารณะหลัก (สตริง [] args) {ทดสอบการทดสอบขั้นสุดท้าย = การทดสอบใหม่ (); สำหรับ (int i = 0; i <10; i ++) {เธรดใหม่ () {โมฆะสาธารณะเรียกใช้ () {สำหรับ (int j = 0; j <1000; j ++) test.increase (); - }.เริ่ม(); } ในขณะที่ (thread.activeCount ()> 1) // ตรวจสอบให้แน่ใจว่าเธรดก่อนหน้านี้เสร็จสิ้น thread.yield (); System.out.println (test.inc); -ลองคิดดูว่าผลลัพธ์ของโปรแกรมนี้คืออะไร? บางทีเพื่อนบางคนคิดว่ามันคือ 10,000 แต่ในความเป็นจริงการเรียกใช้มันจะพบว่าผลลัพธ์ของการวิ่งแต่ละครั้งนั้นไม่สอดคล้องกันและมีจำนวนน้อยกว่า 10,000
เพื่อนบางคนอาจมีคำถามมันผิด ข้างต้นคือการดำเนินการในตัวเองบนตัวแปร Inc เนื่องจากความผันผวนทำให้มั่นใจได้ว่าการมองเห็นหลังจากการเพิ่มขึ้นของตัวเองของ Inc ในแต่ละเธรดค่าที่แก้ไขสามารถเห็นได้ในเธรดอื่น ๆ ดังนั้น 10 เธรดได้ดำเนินการ 1,000 การดำเนินการตามลำดับดังนั้นค่าสุดท้ายของ INC ควรเป็น 1,000*10 = 10,000
มีความเข้าใจผิดที่นี่ คำหลักที่ผันผวนสามารถมั่นใจได้ว่าการมองเห็น แต่โปรแกรมข้างต้นไม่ถูกต้องเพราะไม่สามารถรับประกันความเป็นอะตอมได้ การมองเห็นสามารถตรวจสอบให้แน่ใจว่าค่าล่าสุดจะอ่านทุกครั้ง แต่ความผันผวนไม่สามารถรับประกันความเป็นอะตอมของการทำงานของตัวแปร
ดังที่ได้กล่าวไว้ก่อนหน้านี้การดำเนินการอัตโนมัติไม่ได้เป็นอะตอม มันรวมถึงการอ่านค่าดั้งเดิมของตัวแปรดำเนินการเพิ่มเติมและการเขียนไปยังหน่วยความจำที่ใช้งานได้ กล่าวคืออาจดำเนินการแยกย่อยทั้งสามของการดำเนินการด้วยตนเองซึ่งอาจนำไปสู่สถานการณ์ต่อไปนี้:
หากค่าของ Variable Inc ในช่วงเวลาหนึ่งคือ 10
เธรด 1 ทำการดำเนินการในตัวเองบนตัวแปร เธรด 1 ก่อนอ่านค่าดั้งเดิมของตัวแปร Inc จากนั้นเธรด 1 จะถูกบล็อก;
จากนั้นเธรด 2 จะดำเนินการในการปรับตัวด้วยตนเองบนตัวแปรและเธรด 2 ยังอ่านค่าดั้งเดิมของตัวแปร Inc เนื่องจากเธรด 1 ดำเนินการอ่านเฉพาะใน Variable Inc และไม่ได้แก้ไขตัวแปรจึงไม่ทำให้สายแคชของ Cache Inc Cache Variable Inc ในเธรด 2 เป็นไม่ถูกต้อง ดังนั้นเธรด 2 จะไปที่หน่วยความจำหลักโดยตรงเพื่ออ่านค่าของ Inc เมื่อพบว่าค่าของ Inc คือ 10 จากนั้นทำการดำเนินการเพิ่ม 1 และเขียน 11 ลงในหน่วยความจำที่ใช้งานและในที่สุดก็เขียนลงในหน่วยความจำหลัก
จากนั้นเธรด 1 จากนั้นทำการดำเนินการเพิ่มเติม เนื่องจากค่าของ Inc ได้รับการอ่านโปรดทราบว่าค่าของ Inc ในเธรด 1 ยังคงเป็น 10 ในเวลานี้ดังนั้นหลังจากเธรด 1 เพิ่ม Inc ค่าของ Inc คือ 11 จากนั้นเขียน 11 เพื่อทำงานหน่วยความจำและเขียนลงในหน่วยความจำหลัก
จากนั้นหลังจากสองเธรดทำการดำเนินการในตัวเอง, Inc เพิ่มขึ้นเพียง 1
เมื่ออธิบายสิ่งนี้เพื่อนบางคนอาจมีคำถามมันผิด ไม่รับประกันว่าตัวแปรจะทำให้สายแคชเป็นโมฆะเมื่อแก้ไขตัวแปรผันผวนหรือไม่ จากนั้นเธรดอื่น ๆ จะอ่านค่าใหม่ ใช่นี่ถูกต้อง นี่คือกฎตัวแปรผันผวนในกฎที่เกิดขึ้นก่อนหน้า แต่ควรสังเกตว่าหากเธรด 1 อ่านตัวแปรและถูกบล็อกค่า INC จะไม่ถูกแก้ไข จากนั้นถึงแม้ว่าความผันผวนสามารถตรวจสอบให้แน่ใจว่าเธรด 2 อ่านค่าของตัวแปร Inc จากหน่วยความจำเธรด 1 ไม่ได้แก้ไขดังนั้นเธรด 2 จะไม่เห็นค่าที่แก้ไขเลย
สาเหตุที่แท้จริงคือการดำเนินการ autoincrement ไม่ใช่การทำงานของอะตอมและความผันผวนไม่สามารถรับประกันได้ว่าการดำเนินการใด ๆ ในตัวแปรนั้นเป็นอะตอม
เปลี่ยนรหัสด้านบนเป็นสิ่งใดต่อไปนี้สามารถบรรลุผลได้:
ใช้ซิงโครไนซ์:
การทดสอบระดับสาธารณะ {public int inc = 0; การเพิ่มโมฆะแบบซิงโครไนซ์สาธารณะ () {inc ++; } โมฆะคงที่สาธารณะหลัก (สตริง [] args) {ทดสอบการทดสอบขั้นสุดท้าย = การทดสอบใหม่ (); สำหรับ (int i = 0; i <10; i ++) {เธรดใหม่ () {โมฆะสาธารณะเรียกใช้ () {สำหรับ (int j = 0; j <1000; j ++) test.increase (); - }.เริ่ม(); } ในขณะที่ (thread.activeCount ()> 1) // ตรวจสอบให้แน่ใจว่าเธรดก่อนหน้านี้เสร็จสิ้น thread.yield (); System.out.println (test.inc); - ใช้ล็อค:
การทดสอบระดับสาธารณะ {public int inc = 0; ล็อคล็อค = ใหม่ reentrantlock (); โมฆะสาธารณะเพิ่ม () {lock.lock (); ลอง {inc ++; } ในที่สุด {lock.unlock (); }} โมฆะคงที่สาธารณะหลัก (สตริง [] args) {ทดสอบการทดสอบครั้งสุดท้าย = การทดสอบใหม่ (); สำหรับ (int i = 0; i <10; i ++) {เธรดใหม่ () {โมฆะสาธารณะเรียกใช้ () {สำหรับ (int j = 0; j <1000; j ++) test.increase (); - }.เริ่ม(); } ในขณะที่ (thread.activeCount ()> 1) // ตรวจสอบให้แน่ใจว่าเธรดก่อนหน้านี้ถูกเรียกใช้งานเธรด yield (); System.out.println (test.inc); - ใช้ Atomicinteger:
การทดสอบระดับสาธารณะ {Public Atomicinteger Inc = new Atomicinteger (); โมฆะสาธารณะเพิ่ม () {Inc.getandincrement (); } โมฆะคงที่สาธารณะหลัก (สตริง [] args) {ทดสอบการทดสอบขั้นสุดท้าย = การทดสอบใหม่ (); สำหรับ (int i = 0; i <10; i ++) {เธรดใหม่ () {โมฆะสาธารณะเรียกใช้ () {สำหรับ (int j = 0; j <1000; j ++) test.increase (); - }.เริ่ม(); } ในขณะที่ (thread.activeCount ()> 1) // ตรวจสอบให้แน่ใจว่าเธรดก่อนหน้านี้ถูกเรียกใช้งานเธรด yield (); System.out.println (test.inc); -คลาสการทำงานของอะตอมบางแห่งมีให้ภายใต้ java.util.concurrent.Atomic แพ็คเกจของ Java 1.5 กล่าวคือการเพิ่มขึ้นของตนเอง (เพิ่ม 1 การดำเนินการ), การลดลงของตนเอง (เพิ่ม 1 การดำเนินการ), การดำเนินการเพิ่มเติม (เพิ่มจำนวน) และการดำเนินการลบ Atomic ใช้ CAS เพื่อใช้การปฏิบัติการอะตอม (เปรียบเทียบและแลกเปลี่ยน) CAS ถูกนำไปใช้จริงโดยใช้คำแนะนำ CMPXCHG ที่จัดทำโดยโปรเซสเซอร์และโปรเซสเซอร์ดำเนินการคำแนะนำ CMPXCHG เป็นการดำเนินการอะตอม
3. ความผันผวนของความผันผวนหรือไม่?
ดังที่ได้กล่าวไว้ก่อนหน้านี้คำหลักที่ผันผวนสามารถห้ามการสั่งซื้อการสั่งซื้อใหม่ดังนั้นความผันผวนสามารถรับรองได้ในระดับหนึ่ง
มีความหมายสองประการที่ต้องห้ามสั่งซื้อคำหลักที่ผันผวนใหม่:
1) เมื่อโปรแกรมดำเนินการอ่านหรือเขียนการดำเนินการของตัวแปรผันผวนการเปลี่ยนแปลงทั้งหมดของการดำเนินการก่อนหน้านี้จะต้องได้รับการดำเนินการและผลลัพธ์จะปรากฏให้เห็นแล้วสำหรับการดำเนินการที่ตามมา; การดำเนินการที่ตามมาจะต้องยังไม่เกิดขึ้น
2) เมื่อดำเนินการตามคำสั่งการเพิ่มประสิทธิภาพคำสั่งที่เข้าถึงไปยังตัวแปรผันผวนไม่สามารถวางไว้ด้านหลังได้และไม่สามารถวางข้อความตามตัวแปรระเหยได้ก่อน
บางทีสิ่งที่กล่าวไว้ข้างต้นนั้นค่อนข้างสับสนดังนั้นให้ตัวอย่างง่ายๆ:
// x และ y เป็นตัวแปรที่ไม่ระเหย // ธงเป็นตัวแปรระเหยง่าย x = 2; // คำสั่ง 1y = 0; // คำสั่ง 2flag = true; // คำสั่ง 3x = 4; // คำสั่ง 4y = -1; // คำสั่ง 5
เนื่องจากตัวแปรธงเป็นตัวแปรที่ผันผวนเมื่อดำเนินการตามกระบวนการสั่งซื้อการสั่งซื้อใหม่คำสั่ง 3 จะไม่ถูกวางไว้ก่อนคำสั่ง 1 และ 2 และจะไม่ถูกวางไว้หลังจากคำสั่ง 3 และคำสั่ง 4 และ 5 อย่างไรก็ตามไม่รับประกันว่าคำสั่ง 1 และคำสั่ง 2 และคำสั่งของคำสั่ง 4 และคำสั่ง 5 ไม่รับประกัน
ยิ่งไปกว่านั้นคำหลักที่ผันผวนสามารถตรวจสอบให้แน่ใจว่าเมื่อมีการดำเนินการคำสั่ง 3 คำสั่ง 1 และคำสั่ง 2 จะต้องดำเนินการและผลการดำเนินการของคำสั่ง 1 และคำสั่ง 2 สามารถมองเห็นคำสั่ง 3, คำสั่ง 4 และคำสั่ง 5
ลองกลับไปที่ตัวอย่างก่อนหน้า:
// เธรด 1: บริบท = loadContext (); // state 1inited = true; // state 2 // เธรด 2: ในขณะที่ (! inited) {sleep ()} dosomethingwithconfig (บริบท);เมื่อฉันยกตัวอย่างนี้ฉันบอกว่าเป็นไปได้ว่าคำสั่ง 2 จะถูกดำเนินการก่อนคำสั่ง 1 ตราบใดที่อาจทำให้บริบทไม่ได้เริ่มต้นและเธรด 2 ใช้บริบทที่ไม่ได้ใช้งานทำให้เกิดข้อผิดพลาดของโปรแกรม
หากตัวแปรที่ติดตั้งจะถูกแก้ไขด้วยคำหลักที่ผันผวนปัญหานี้จะไม่เกิดขึ้นเพราะเมื่อคำสั่ง 2 ถูกดำเนินการมันจะทำให้มั่นใจได้ว่าบริบทได้เริ่มต้นอย่างแน่นอน
4. หลักการและกลไกการใช้งานของความผันผวน
คำอธิบายก่อนหน้าของการใช้คำหลักที่ผันผวนบางอย่างเกิดจาก Let’s discuss how volatile ensures visibility and prohibits instructions to reorder.
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
五.使用volatile关键字的场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
下面列举几个Java中使用volatile的几个场景。
1.状态标记量
volatile boolean flag = false; while(!flag){ doSomething();} public void setFlag() { flag = true;} volatile boolean inited = false;//线程1:context = loadContext(); inited = true; //线程2:while(!inited ){sleep()}doSomethingwithconfig(context);2.double check
class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; -参考资料:
《Java编程思想》
《深入理解Java虚拟机》
The above is all the content of this article. ฉันหวังว่ามันจะเป็นประโยชน์ต่อการเรียนรู้ของทุกคนและฉันหวังว่าทุกคนจะสนับสนุน wulin.com มากขึ้น