บทความนี้ช่วยให้ทุกคนเข้าใจ ThreadLocal โดยใช้กรณีการเพิ่มประสิทธิภาพ SimpleDateFormat ที่ไม่ปลอดภัยในสภาพแวดล้อมที่เกิดขึ้นพร้อมกัน
เมื่อเร็ว ๆ นี้ฉันแยกโครงการของ บริษัท และพบว่ามีสิ่งเลวร้ายมากมายที่เขียนเช่นต่อไปนี้:
DateUtil ชั้นเรียนสาธารณะ {ส่วนตัวสุดท้ายคงที่ simpledateFormat sdfyhm = new SimpledateFormat ("yyyymmdd"); PARSEYMDHMS (แหล่งที่มาของสตริง) {ลอง {return sdfyhm.parse (แหล่งที่มา); } catch (parseexception e) {e.printstacktrace (); ส่งคืนวันที่ใหม่ (); - ก่อนอื่นมาวิเคราะห์:
ฟังก์ชั่น parseymdhms () ณ จุดนี้ใช้การปรับเปลี่ยนแบบซิงโครไนซ์ซึ่งหมายความว่าการดำเนินการเป็นเธรดที่ไม่ปลอดภัยดังนั้นจึงต้องซิงโครไนซ์ Thread-unsafe สามารถเป็นวิธีการแยกวิเคราะห์ () ของ simpledateFormat เท่านั้น ตรวจสอบซอร์สโค้ด มีตัวแปรทั่วโลกใน SimpledateFormat
ปฏิทินปฏิทินที่ได้รับการป้องกันวันที่ parse () {calendar.clear (); ... // ดำเนินการบางอย่างเพื่อกำหนดวันที่ปฏิทินและปฏิทินอื่น ๆ getTime (); // รับเวลาของปฏิทิน}การดำเนินการที่ชัดเจน () จะทำให้เธรดไม่ปลอดภัย
นอกจากนี้การใช้คำหลักที่ซิงโครไนซ์มีผลกระทบอย่างมากต่อประสิทธิภาพโดยเฉพาะอย่างยิ่งเมื่อมีการเรียกใช้มัลติเธรดทุกครั้งที่มีการเรียกใช้วิธี ParseymdHMS การตัดสินการซิงโครไนซ์จะทำและการซิงโครไนซ์นั้นมีราคาแพงมากดังนั้นนี่จึงเป็นทางออกที่ไม่มีเหตุผล
วิธีการปรับปรุง
เธรดที่ไม่ปลอดภัยเกิดจากการใช้ตัวแปรที่ใช้ร่วมกันโดยหลายเธรดดังนั้นที่นี่เราใช้ ThreadLocal <MimpleDateFormat> เพื่อสร้างตัวแปรคัดลอกสำหรับแต่ละเธรดแยกกัน ก่อนอื่นให้รหัสแล้ววิเคราะห์สาเหตุของปัญหานี้เพื่อแก้ปัญหา
/** * คลาสเครื่องมือวันที่ (ThreadLocal ใช้เพื่อรับ SimpledateFormat และวิธีอื่น ๆ สามารถคัดลอกได้โดยตรงโดย Common-Lang) * @author niu li * @date 2016/11/19 */คลาสสาธารณะวันที่ Logger Logger แบบคงที่ส่วนตัว = loggerFactory.getLogger (dateutil.class); สตริงคงสุดท้ายสาธารณะ mdhmss = "MMDDHHMMSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS สตริงคงสุดท้ายสาธารณะ ymdhms = "yyymmddhhmmss"; สตริงคงที่สาธารณะสุดท้าย ymdhms_ = "yyyy-mm-dd hh: mm: ss"; สตริงคงที่สาธารณะสุดท้าย ymd = "yyyymmdd"; สตริงคงที่สาธารณะสุดท้าย ymd_ = "yyyy-mm-dd"; สตริงคงที่สาธารณะสุดท้าย hms = "HHMMSS"; / *** รับอินสแตนซ์ SDF ของเธรดที่เกี่ยวข้องตามคีย์ในแผนที่* @param รูปแบบคีย์ในแผนที่* @return อินสแตนซ์นี้*/ ส่วนตัวคงที่ simpledateFormat getSdf (รูปแบบสตริงสุดท้าย) {ThreadLocal <ImpledateFormat> SDFTHREAD = SDFMAP.get (รูปแบบ); if (sdfthread == null) {// ตรวจสอบอีกครั้งเพื่อป้องกันไม่ให้ SDFMap ใส่ค่าเป็นหลายครั้งและเหตุผลสำหรับการล็อคสองครั้งเดียวกับซิงโครไนซ์ (dateutil.class) {sdfthread = sdfmap.get (รูปแบบ); if (sdfthread == null) {logger.debug ("ใส่ SDF ใหม่ของรูปแบบ" + pattern + "ไปยังแผนที่"); SDFTHREAD = ใหม่ ThreadLocal <MimpledateFormat> () {@Override ป้องกัน simpledateFormat initialValue () {logger.debug ("เธรด:" + thread.currentthread () + "รูปแบบ init:" + รูปแบบ); ส่งคืน simpledateFormat ใหม่ (รูปแบบ); - sdfmap.put (รูปแบบ, sdfthread); }}} ส่งคืน sdfthread.get (); } / *** แยกวิเคราะห์วันที่ตามรูปแบบที่ระบุ* @param วันที่วันที่ที่จะแยก* @param วันที่รูปแบบระบุรูปแบบ* @return parsed วันที่อินสแตนซ์* / วันที่สาธารณะ parsedate (วันที่สตริง, รูปแบบสตริง) {ถ้า (วันที่ == null) } ลอง {return getsdf (รูปแบบ) .parse (วันที่); } catch (parseexception e) {e.printstacktrace (); logger.error ("รูปแบบที่แยกวิเคราะห์ไม่รองรับ:"+รูปแบบ); } return null; } / *** วันที่รูปแบบตามรูปแบบที่ระบุ* วันที่ @param วันที่ที่จะจัดรูปแบบ* @param รูปแบบระบุรูปแบบ* @@Return รูปแบบที่แยกวิเคราะห์* / รูปแบบสตริงคงที่สาธารณะ (วันที่วันที่, รูปแบบสตริง) {ถ้า (วันที่ == null) } else {return getsdf (รูปแบบ) .format (วันที่); -ทดสอบ
ดำเนินการหนึ่งในเธรดหลักและอีกสองในเธรดลูกทั้งสองใช้รูปแบบเดียวกัน
โมฆะคงที่สาธารณะหลัก (สตริง [] args) {dateutil.formatdate (วันที่ใหม่ (), mdhmss); เธรดใหม่ (()-> {dateutil.formatdate (วันที่ใหม่ (), mdhmss);}). start (); เธรดใหม่ (()-> {dateutil.formatdate (วันที่ใหม่ (), mdhmss);}). start (); -การวิเคราะห์บันทึก
ใส่ SDF ใหม่ของรูปแบบ mmddhhmmsssss ลงใน mapthread: เธรด [หลัก, 5, หลัก] รูปแบบ init: mmddhhmmsssssthread: Thread [Thread-0,5, Main] รูปแบบเริ่มต้น: MMDDHMSSSSSSSTHREAD: Thread [Thread-1,5
วิเคราะห์
จะเห็นได้ว่า SDFMAP ใส่ครั้งเดียวในขณะที่ SimpledateFormat เป็นใหม่สามครั้งเพราะมีสามเธรดในรหัส แล้วทำไมถึงเป็นเช่นนี้?
สำหรับแต่ละเธรดเธรดจะมีการอ้างอิงตัวแปรส่วนกลางไปยัง ThreadLocal.threadLocalMap ThreadLocals มี threadlocal.threadlocalmap ที่เก็บ threadlocal และค่าที่สอดคล้องกัน ภาพหนึ่งดีกว่าหนึ่งพันคำ แผนภาพโครงสร้างมีดังนี้:
ดังนั้นสำหรับ SDFMAP แผนภาพโครงสร้างจะเปลี่ยนไป
1. First Execute DateUtil.FormatDate (วันที่ใหม่ (), MDHMSS);
// ครั้งแรกที่ฉันดำเนินการ DateUtil.FormatDate (วันที่ใหม่ (), MDHMSS) การวิเคราะห์ส่วนตัวแบบคงที่ simpleDateFormat getSdf (รูปแบบสตริงสุดท้าย) {ThreadLocal <ImpleDateFormat> SDFTHREAD = SDFMAP.get (รูปแบบ); // sdfthread ที่ได้รับคือ null ให้ป้อนถ้าคำสั่งถ้า (sdfthread == null) {ซิงโครไนซ์ (dateutil.class) {sdfthread = sdfmap.get (รูปแบบ); // sdfthread ยังคงเป็นโมฆะให้ป้อนถ้าคำสั่งถ้า (sdfthread == null) {// พิมพ์ logger.debug ("ใส่ SDF ใหม่ของรูปแบบ" + รูปแบบ + "เพื่อทำแผนที่"); // สร้างอินสแตนซ์ threadlocal และแทนที่เมธอด intialValue sdfThread = new ThreadLocal <MimpledateFormat> () {@Override ป้องกัน SimpleDateFormat InitialValue () {logger.debug ("เธรด:" + เธรด ส่งคืน simpledateFormat ใหม่ (รูปแบบ); - // ตั้งค่าเป็น sdfmap sdfmap.put (รูปแบบ, sdfthread); }}} ส่งคืน sdfthread.get (); - ในเวลานี้มีคนถามวิธีการตั้งค่าเธรดไม่ได้ถูกเรียกที่นี่ดังนั้นคุณจะตั้งค่าให้เข้าร่วมได้อย่างไร
สิ่งนี้ต้องการการใช้งาน sdfthread.get ():
สาธารณะ t get () {เธรด t = thread.currentthread (); threadlocalMap map = getMap (t); if (map! = null) {threadlocalmap.entry e = map.getEntry (นี่); if (e! = null) {@SuppressWarnings ("ไม่ถูกตรวจสอบ") t result = (t) e.value; ผลการกลับมา; }} return setInitialValue (); -กล่าวคือเมื่อไม่มีค่าวิธีการ setinitialValue () จะถูกเรียกซึ่งจะเรียกวิธีการเริ่มต้น () ซึ่งเป็นวิธีที่เราแทนที่
การพิมพ์บันทึกที่สอดคล้องกัน
ใส่ SDF ใหม่ของรูปแบบ mmddhhmmsssss ไปยัง MapThread: เธรด [Main, 5, Main] รูปแบบ init: MMDDHHMMSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS
2. ดำเนินการ dateutil.formatdate (วันที่ใหม่ (), mdhmss) บนเธรดลูกเป็นครั้งที่สอง
// ดำเนินการ `dateutil.formatdate (วันที่ใหม่ (), mdhmss);` private private static simpledateFormat getSdf (รูปแบบสตริงสุดท้าย) {ThreadLocal <ImpleDateFormat> SDFTHREAD = SDFMAP.get (รูปแบบ); // sdfthread ที่ได้รับที่นี่ไม่ใช่ null, ข้ามถ้าบล็อกถ้า (sdfthread == null) {ซิงโครไนซ์ (dateutil.class) {sdfthread = sdfmap.get (รูปแบบ); if (sdfthread == null) {logger.debug ("ใส่ SDF ใหม่ของรูปแบบ" + pattern + "ไปยังแผนที่"); SDFTHREAD = ใหม่ ThreadLocal <MimpledateFormat> () {@Override ป้องกัน simpledateFormat initialValue () {logger.debug ("เธรด:" + thread.currentthread () + "รูปแบบ init:" + รูปแบบ); ส่งคืน simpledateFormat ใหม่ (รูปแบบ); - sdfmap.put (รูปแบบ, sdfthread); }}} // เรียก sdfthread.get () โดยตรงเพื่อส่งคืน sdfthread.get (); -การวิเคราะห์ sdfthread.get ()
// ดำเนินการ `dateutil.formatdate (วันที่ใหม่ (), mdhmss);` สาธารณะ t get () {เธรด t = thread.currentThread (); // รับแผนที่ด้ายของ ThreadLocalMap ปัจจุบัน = getMap (t); // แผนที่ที่ได้รับในเธรดลูกเป็นโมฆะข้ามถ้าบล็อกถ้า (แผนที่! = null) {threadlocalmap.entry e = map.getEntry (นี่); if (e! = null) {@SuppressWarnings ("ไม่ถูกตรวจสอบ") t result = (t) e.value; ผลการกลับมา; }} // ดำเนินการเริ่มต้นโดยตรงนั่นคือเรียกใช้วิธีการเริ่มต้น () เราเขียนทับ SetInitialValue (); -บันทึกที่สอดคล้องกัน:
Thread [Thread-1,5, Main] รูปแบบ init: MMDDHHMMSSSSSSSSS
สรุป
สถานการณ์ใดที่เหมาะสำหรับการใช้ Threadlocal ในสถานการณ์ใด มีคนให้คำตอบที่ดีเกี่ยวกับ Stackoverflow
ฉันควรใช้ตัวแปร ThreadLocal เมื่อใดและอย่างไร
การใช้งานหนึ่งที่เป็นไปได้ (และทั่วไป) คือเมื่อคุณมีวัตถุบางอย่างที่ไม่ปลอดภัยกับเธรด แต่คุณต้องการหลีกเลี่ยงการซิงโครไนซ์การเข้าถึงวัตถุนั้น (ฉันกำลังมองหาคุณ SimpledateFormat) ให้แต่ละเธรดอินสแตนซ์ของวัตถุเองแทน
รหัสอ้างอิง:
https://github.com/nl101531/util-demo ภายใต้ Javaweb
ข้อมูลอ้างอิง:
เรียนรู้ Java Threadlocal ในแบบที่เข้าใจง่าย
ปัญหาความปลอดภัยและการแก้ปัญหาของ SimpledateFormat
ข้างต้นเป็นเนื้อหาทั้งหมดของบทความนี้ ฉันหวังว่ามันจะเป็นประโยชน์ต่อการเรียนรู้ของทุกคนและฉันหวังว่าทุกคนจะสนับสนุน wulin.com มากขึ้น