เมื่อเร็ว ๆ นี้ฉันอ่านรหัสเลเยอร์ Android Framework และเห็นคลาส ThreadLocal ฉันไม่คุ้นเคยเล็กน้อยดังนั้นฉันจึงอ่านบล็อกที่เกี่ยวข้องต่าง ๆ ทีละคน จากนั้นฉันศึกษาซอร์สโค้ดและพบว่าความเข้าใจของฉันแตกต่างจากโพสต์บล็อกที่ฉันอ่านมาก่อนดังนั้นฉันจึงตัดสินใจเขียนบทความเพื่อพูดคุยเกี่ยวกับความเข้าใจของฉันโดยหวังว่ามันจะมีบทบาทต่อไปนี้:
- สามารถล้างผลการวิจัยและความเข้าใจของคุณให้ลึกซึ้งยิ่งขึ้น
- มันสามารถมีบทบาทในการดึงดูดหยกและช่วยให้นักเรียนที่สนใจล้างความคิดของพวกเขา;
- แบ่งปันประสบการณ์การเรียนรู้ของคุณและสื่อสารและเรียนรู้กับทุกคน
1. Threadlocal คืออะไร
Threadlocal เป็นคลาสพื้นฐานของไลบรารีคลาส Java ภายใต้แพ็คเกจ java.lang;
คำอธิบายอย่างเป็นทางการมีดังนี้:
ใช้ที่เก็บเธรดท้องถิ่นนั่นคือตัวแปรที่แต่ละเธรดมีค่าของตัวเอง เธรดทั้งหมดแบ่งปันวัตถุ ThreadLocal เดียวกัน แต่แต่ละรายการจะเห็นค่าที่แตกต่างกันเมื่อเข้าถึงและการเปลี่ยนแปลงที่ทำโดยหนึ่งเธรดจะไม่ส่งผลกระทบต่อเธรดอื่น ๆ การใช้งานสนับสนุนค่า NULL
ความหมายทั่วไปคือ:
สามารถใช้กลไกการจัดเก็บข้อมูลในท้องถิ่นของเธรดได้ ตัวแปร ThreadLocal เป็นตัวแปรที่สามารถมีค่าที่แตกต่างกันในเธรดที่แตกต่างกัน เธรดทั้งหมดสามารถแชร์วัตถุ ThreadLocal เดียวกันได้ แต่เธรดที่แตกต่างกันสามารถรับค่าที่แตกต่างกันเมื่อเข้าถึงและการเปลี่ยนแปลงของเธรดใด ๆ ที่จะไม่ส่งผลกระทบต่อเธรดอื่น ๆ การใช้งานคลาสรองรับค่า NULL (ค่า NULL สามารถส่งผ่านและเข้าถึงได้ในวิธีการตั้งค่าและรับ)
โดยสรุปมีสามลักษณะ:
- รับค่าที่แตกต่างกันเมื่อเข้าถึงโดยเธรดที่แตกต่างกัน
- เธรดใด ๆ ที่เปลี่ยนแปลงไปจะไม่ส่งผลกระทบต่อเธรดอื่น ๆ
- สนับสนุน Null
ต่อไปนี้เป็นตัวอย่างของคุณสมบัติเหล่านี้ ก่อนอื่นเรากำหนดคลาสทดสอบ ในชั้นเรียนนี้เราตรวจสอบคุณสมบัติทั้งสามที่กล่าวถึงข้างต้น คำจำกัดความของคลาสมีดังนี้:
test.java
การทดสอบระดับสาธารณะ {// กำหนด ThreadLocal ส่วนตัวคงที่ด้ายคงที่ชื่อ; โมฆะคงที่สาธารณะหลัก (สตริง [] args) โยนข้อยกเว้น {name = new ThreadLocal (); // define Thread Athread a = thread ใหม่ () {public void run () {system.out.println ( a "); system.out.println (" หลังจากชุดเรียกใช้ค่าคือ: "+name.get ());}}; // กำหนดเธรด bthread b = เธรดใหม่ () {โมฆะสาธารณะเรียกใช้ () {system.out.println (" ก่อนที่จะเรียกใช้ : "+name.get ());}}; // ไม่เรียกใช้ชุด, พิมพ์ค่าเป็น nullsystem.out.println (name.get ()); // revoke set เพื่อเติม valuename.set (" เธรดหลัก”); // เริ่มเธรด AA.start (); bb.start (); b.join (); // พิมพ์ค่าหลังจากเปลี่ยนค่าโดยเธรด bsystem.out.println (name.get ())}}การวิเคราะห์รหัส:
จากคำจำกัดความเราจะเห็นว่ามีการประกาศวัตถุเธรดเดียวเพียงหนึ่งวัตถุและอีกสามเธรด (เธรดหลักเธรด A และเธรด B) แบ่งปันวัตถุเดียวกัน จากนั้นค่าของวัตถุจะถูกแก้ไขในเธรดที่แตกต่างกันและค่าของวัตถุถูกเข้าถึงในเธรดที่แตกต่างกันและผลลัพธ์คือเอาต์พุตเพื่อดูในคอนโซล
ดูผลลัพธ์:
จากผลลัพธ์ของเอาต์พุตคอนโซลคุณจะเห็นว่ามีเอาต์พุตโมฆะสามรายการ นี่เป็นเพราะวัตถุไม่ได้รับการกำหนดก่อนเอาต์พุตซึ่งตรวจสอบคุณสมบัติของการสนับสนุน null นอกจากนี้ยังสามารถพบได้ว่าในแต่ละเธรดฉันได้แก้ไขค่าของวัตถุ แต่เมื่อเธรดอื่นเข้าถึงวัตถุมันไม่ใช่ค่าที่แก้ไข แต่ค่าเธรดท้องถิ่น; นอกจากนี้ยังตรวจสอบคุณสมบัติอีกสองคุณสมบัติ
2. บทบาทของ Threadlocal
ทุกคนรู้ว่าสถานการณ์การใช้งานส่วนใหญ่เป็นโปรแกรมหลายเธรด สำหรับฟังก์ชั่นเฉพาะของมันคุณพูดอย่างนั้นได้อย่างไร? ฉันคิดว่าสิ่งนี้สามารถกำหนดได้ในลักษณะทั่วไปเท่านั้นเพราะคุณลักษณะการทำงานของสิ่งต่าง ๆ จะ จำกัด การคิดของทุกคนหลังจากที่กำหนดไว้ ตัวอย่างเช่นมีดครัวใช้ในการตัดผักและหลายคนจะไม่ใช้มันเพื่อตัดแตงโม
ที่นี่ฉันจะพูดถึงความเข้าใจของฉันเกี่ยวกับฟังก์ชั่นสำหรับการอ้างอิงเท่านั้นและหวังว่ามันจะเป็นประโยชน์ มาอธิบายด้วยวิธีนี้ เมื่อโปรแกรมแบบมัลติเธรดจำเป็นต้องห่อหุ้มบางส่วนของเธรดส่วนใหญ่ (นั่นคือบางรหัสในวิธีการเรียกใช้) เธรดสามารถใช้ในการห่อตัวแปรสมาชิกที่เกี่ยวข้องกับเธรดในตัวห่อหุ้มเพื่อให้แน่ใจว่าการเข้าถึงเธรดพิเศษและเธรดทั้งหมดสามารถแบ่งปันวัตถุห่อหุ้ม; คุณสามารถอ้างถึง Looper ใน Android โปรแกรมเมอร์ที่ไม่สามารถอธิบายปัญหาเกี่ยวกับรหัสไม่ใช่โปรแกรมเมอร์ที่ดี
ดูที่รหัส: เครื่องมือสำหรับการระบุรหัสที่ใช้เวลานานของเธรด (สร้างขึ้นเพื่อแสดงปัญหา)
StatisticCostTime.java
// คลาสที่สถิติค่าใช้จ่ายในระดับ Timepublic Class StatisticCostTime {// บันทึกเวลาเริ่มต้น // starttime threadlocal ส่วนตัว = ใหม่ ThreadLocal (); เวลาเริ่มต้นมานาน; // ค่าใช้จ่ายส่วนตัว threadlocal = new Threadlocal () InstanceFactory.Instance;} คลาสคงที่คลาสสแตติกอินสแตนซ์อินสแตนซ์อินสแตนซ์ {ส่วนตัวคงที่สถิติสุดท้ายสถิติครั้งสุดท้าย = statisticCostTime ();} // startPublic void เริ่มต้น () {// startTime.set (ระบบ nanotime ()) {cost. ) {cost. ) starttime.get ()); costtime = system.nanotime () - starttime;} public long getStarttime () {return starttime; // return starttime.get ();} public long getCostTime () {// return costtime.get (); return costtime;โอเคการออกแบบเครื่องมือเสร็จสมบูรณ์ตอนนี้เราใช้มันเพื่อนับเธรดที่ใช้เวลานานแล้วลอง:
Main.java
คลาสสาธารณะหลัก {โมฆะคงที่สาธารณะหลัก (สตริง [] args) พ่นข้อยกเว้น {// กำหนดเธรด athread a = thread ใหม่ () {public void run () {ลอง {// เริ่มบันทึก timestatisticcosttime.shareinstance () เริ่มต้น (); sleep (200); System.out.println ("A-starttime:"+StatisticCostTime.ShareInstance (). getStartTime ()); // สิ้นสุด recordStatisticCostTime.ShareInstance (). end (); // พิมพ์เวลาของ System.out.println ("A:" e) {}}}; // เริ่ม aa.start (); // define เธรด bthread b = เธรดใหม่ () {public void run () {ลอง {// บันทึกเวลาเริ่มต้นของ b1statisticcosttime.shareinstance () เริ่มต้น (); Consolesystem.out.println ("B1-starttime:"+StatisticCostTime.ShareInstance (). getStartTime ()); // สิ้นสุดการบันทึกเวลาเริ่มต้นของ B1StatisticCostTime.ShareInstance (). สิ้นสุด (); b1system.out.println ("b1:"+statisticcosttime.shareinstance (). getCostTime ()); // เริ่มบันทึกเวลาของ B2StatisticCostTime.ShareInstance (). start (); sleep (100); b2system.out.println ("B2-starttime:"+StatisticCostTime.ShareInstance (). getStartTime ()); // เวลาบันทึกสิ้นสุดเวลา B2StatisticCostTime.ShareInstance (). สิ้นสุด (); b2system.out.println ("b2:"+statisticcosttime.shareinstance (). getCostTime ());} catch (Exception e) {}}}; b.start ();}}}หลังจากเรียกใช้รหัสผลลัพธ์ผลลัพธ์มีดังนี้: ความแม่นยำของผลลัพธ์ผลลัพธ์คือนาโนวินาที
ขึ้นอยู่กับว่าผลลัพธ์นั้นแตกต่างจากที่เราคาดไว้หรือไม่ ฉันพบว่าผลลัพธ์ของ A ควรเท่ากับ B1+B2 ทำไมมันถึงเหมือน B2? คำตอบคือเมื่อเรากำหนดตัวแปรเริ่มต้นและเวลาต้นทุนความตั้งใจดั้งเดิมไม่ควรแชร์ แต่ควรเป็นเอกสิทธิ์ของเธรด ที่นี่ตัวแปรจะถูกแชร์กับซิงเกิลตันดังนั้นเมื่อคำนวณค่าของ A จะมีการแก้ไขจริงโดย B2 ดังนั้นผลลัพธ์เดียวกับ B2 คือเอาต์พุต
ตอนนี้เรามาเปิดส่วนที่แสดงความคิดเห็นใน StatisticCostTime แล้วลองโดยการเปลี่ยนเป็นวิธีการประกาศของ ThreadLocal
ดูผลลัพธ์:
อ่า สิ่งนี้บรรลุผลที่คาดหวัง ในเวลานี้นักเรียนบางคนจะบอกว่านี่ไม่ใช่การเข้าถึงแบบแพร่กระจายแบบเธรดและฉันสามารถมั่นใจได้ถึงความปลอดภัยของด้ายตราบใดที่ฉันใช้ ThreadLocal หรือไม่? คำตอบคือไม่! ก่อนอื่นเราสามารถหาได้ว่าทำไมมีปัญหาด้านความปลอดภัยของด้าย แต่มีเพียงสองสถานการณ์:
1. คุณได้แบ่งปันทรัพยากรที่ไม่ควรแบ่งปันระหว่างเธรด
2. คุณไม่รับประกันการเข้าถึงทรัพยากรที่ใช้ร่วมกันระหว่างเธรดอย่างเป็นระเบียบ
อดีตสามารถแก้ไขได้ด้วยวิธี "เวลาแลกเปลี่ยนเชิงพื้นที่" โดยใช้ ThreadLocal (คุณสามารถประกาศตัวแปรท้องถิ่นโดยตรง) และวิธีหลังสามารถแก้ไขได้ด้วยวิธี "เวลาแลกเปลี่ยนเชิงพื้นที่" ซึ่งไม่เห็นได้ชัดว่า ThreadLocal สามารถทำได้
3. หลักการ Threadlocal
หลักการดำเนินการเป็นเรื่องง่ายมาก ทุกครั้งที่การดำเนินการอ่านและเขียนไปยังวัตถุ ThreadLocal นั้นจริง ๆ แล้วการดำเนินการอ่านและเขียนไปยังวัตถุค่าของเธรด ที่นี่เราชี้แจงว่าไม่มีการสร้างสำเนาของตัวแปรเนื่องจากพื้นที่หน่วยความจำที่จัดสรรโดยตัวแปรไม่ได้ใช้เพื่อจัดเก็บวัตถุ T แต่ค่าของเธรดใช้ในการจัดเก็บวัตถุ T; ทุกครั้งที่เราเรียกเมธอด threadlocal set ในเธรดจริง ๆ แล้วเป็นกระบวนการของการเขียนวัตถุไปยังวัตถุค่าที่สอดคล้องกันของเธรด เมื่อเรียกเมธอด ThreadLocal Get มันเป็นกระบวนการของการรับวัตถุจากวัตถุค่าที่สอดคล้องกันของเธรด
ดูซอร์สโค้ด:
ชุดตัวแปรสมาชิก ThreadLocal
/*** ตั้งค่าของตัวแปรนี้สำหรับเธรดปัจจุบัน หากตั้งค่าเป็น * {@code null} ค่าจะถูกตั้งค่าเป็น null และรายการพื้นฐานจะยังคงอยู่ * * @param ค่าค่าใหม่ของตัวแปรสำหรับเธรดผู้โทร */โมฆะสาธารณะตั้งค่า (ค่า t) {เธรด currentThread = tread.currentThread (); ค่าค่า = ค่า (currentThread); if (values == null) {value = initializeValues (currentThread); } values.put (นี้ค่า);}วิธีการสมาชิก treadlocal รับ
/*** ส่งคืนค่าของตัวแปรนี้สำหรับเธรดปัจจุบัน หากรายการ * ยังไม่มีอยู่สำหรับตัวแปรนี้ในเธรดนี้วิธีนี้จะ * สร้างรายการเติมค่าด้วยผลลัพธ์ของ * {@link #initialValue ()} * * @return ค่าปัจจุบันของตัวแปรสำหรับเธรดการเรียก */@suppresswarnings ("ไม่ได้ตรวจสอบ") สาธารณะ t get () {// ปรับให้เหมาะสมสำหรับเส้นทางที่รวดเร็ว เธรด currentThread = thread.currentthread (); ค่าค่า = ค่า (currentThread); if (ค่า! = null) {object [] table = values.table; INT INDEX = hash & values.mask; if (this.reference == ตาราง [index]) {return (t) ตาราง [ดัชนี + 1]; }} else {values = initializeValues (currentThread); } return (t) value.getaftermiss (this);}เมธอดสมาชิก threadlocal initializevalues
/*** สร้างอินสแตนซ์ค่าสำหรับเธรดและประเภทตัวแปรนี้ */ค่าเริ่มต้น (กระแสเธรด) {return current.localValues = ค่าใหม่ ();}ค่าเมธอดสมาชิก threadlocal
/*** รับอินสแตนซ์ค่าสำหรับเธรดนี้และประเภทตัวแปร */ค่าค่า (เธรดปัจจุบัน) {return current.localValues;}แล้วคุณจะอ่านและเขียนวัตถุในค่านี้ได้อย่างไร?
ค่ามีอยู่เป็นคลาสภายในของ ThreadLocal; ค่านี้รวมถึงวัตถุอาร์เรย์ที่สำคัญ [] ซึ่งเป็นส่วนสำคัญของการตอบคำถาม มันถูกใช้เพื่อเก็บตัวแปร treadlocal ประเภทต่าง ๆ ในเธรด ดังนั้นคำถามคือคุณจะมั่นใจได้อย่างไรว่าคุณไม่ได้รับค่าประเภทอื่น ๆ เมื่อใช้ตัวแปรประเภทที่แน่นอน โดยทั่วไปแผนที่จะถูกแมปตามค่าคีย์; ใช่ความคิดคือความคิดนี้ แต่ไม่ได้นำมาใช้กับแผนที่ที่นี่มันเป็นกลไกแผนที่ที่ใช้กับวัตถุ []; อย่างไรก็ตามหากคุณต้องการใช้แผนที่เพื่อทำความเข้าใจมันเป็นไปไม่ได้เพราะกลไกนั้นเหมือนกัน คีย์จริง ๆ แล้วสอดคล้องกับการอ้างอิงที่อ่อนแอของ ThreadLocal และค่าสอดคล้องกับวัตถุที่เราผ่านเข้ามา
มาอธิบายวิธีใช้วัตถุ [] เพื่อใช้กลไกแผนที่ (ดูรูปที่ 1) มันใช้ความเท่าเทียมกันของตัวห้อยอาร์เรย์เพื่อแยกความแตกต่างของคีย์และค่านั่นคือตารางด้านล่างเก็บคีย์ที่ตำแหน่งสม่ำเสมอและหมายเลขคี่เก็บค่าไว้ นี่คือวิธีการทำ หากนักเรียนที่สนใจต้องการทราบการใช้อัลกอริทึมพวกเขาสามารถศึกษาได้ในเชิงลึกฉันจะไม่อธิบายรายละเอียดที่นี่
จากตัวอย่างแรกก่อนหน้านี้สถานการณ์การจัดเก็บจะถูกวิเคราะห์:
เมื่อโปรแกรมถูกดำเนินการมีสามเธรด A, B และ Main เมื่อ Name.set () ถูกเรียกในเธรดพื้นที่หน่วยความจำที่เหมือนกันสามช่องจะถูกจัดสรรในพื้นที่ฮีปสำหรับสามอินสแตนซ์เธรดในเวลาเดียวกันเพื่อจัดเก็บวัตถุค่าโดยใช้การอ้างอิงชื่อเป็นคีย์และวัตถุเฉพาะจะถูกเก็บไว้เป็นค่าในสามวัตถุที่แตกต่างกัน [] (ดูรูปด้านล่าง):
4. สรุป
ThreadLocal ไม่สามารถแก้ปัญหาพร้อมกันได้อย่างสมบูรณ์ในระหว่างการเขียนโปรแกรมแบบมัลติเธรด ปัญหานี้ยังต้องใช้วิธีแก้ปัญหาที่แตกต่างกันในการเลือกตามสถานการณ์ที่แตกต่างกัน "พื้นที่สำหรับเวลา" หรือ "เวลาสำหรับพื้นที่"
ฟังก์ชั่นที่ใหญ่ที่สุดของ ThreadLocal คือการแปลงตัวแปรที่แบ่งปันเธรดเป็นตัวแปรเธรดท้องถิ่นเพื่อให้ได้การแยกระหว่างเธรด
ข้างต้นเป็นเรื่องเกี่ยวกับการทำความเข้าใจ Threadlocal อย่างรวดเร็วใน Java ฉันหวังว่ามันจะเป็นประโยชน์กับทุกคน หากมีข้อบกพร่องใด ๆ โปรดฝากข้อความไว้เพื่อชี้ให้เห็น ขอบคุณเพื่อนที่ให้การสนับสนุนเว็บไซต์นี้