การรับค่าจาก java.util.HashMap นั้นปลอดภัยจากหลายเธรดหรือไม่?


138

มีกรณีที่แผนที่จะถูกสร้างขึ้นและเมื่อมีการเริ่มต้นมันจะไม่ถูกแก้ไขอีกครั้ง อย่างไรก็ตามจะสามารถเข้าถึงได้ (ผ่านรับ (คีย์) เท่านั้น) จากหลายเธรด วิธีนี้ปลอดภัยไหมที่จะใช้java.util.HashMap?

(ปัจจุบันฉันใช้อย่างมีความสุขjava.util.concurrent.ConcurrentHashMapและไม่จำเป็นต้องวัดเพื่อปรับปรุงประสิทธิภาพ แต่ฉันแค่อยากรู้อยากเห็นว่าเรียบง่ายHashMapพอเพียงดังนั้นคำถามนี้ไม่ใช่ "ฉันควรใช้อันไหนดี?" และไม่ใช่คำถามประสิทธิภาพ ค่อนข้างคำถามคือ "จะปลอดภัยหรือไม่")


4
คำตอบมากมายที่นี่ถูกต้องเกี่ยวกับการแยกซึ่งกันและกันจากการรันเธรด แต่ไม่ถูกต้องเกี่ยวกับการอัพเดตหน่วยความจำ ฉันลงคะแนนตามขึ้น / ลง แต่ยังมีคำตอบที่ไม่ถูกต้องมากมายที่มีคะแนนโหวตเป็นบวก
Heath Borders

@Heath Borders หากอินสแตนซ์ a ถูกกำหนดค่าเริ่มต้นแบบคงที่ HodMap ที่ไม่สามารถแก้ไขได้แบบคงที่ควรจะปลอดภัยสำหรับการอ่านพร้อมกัน (เนื่องจากเธรดอื่นไม่สามารถพลาดการอัพเดตได้
kaqqao

ถ้ามันเริ่มต้นได้แบบคงที่และไม่เคยมีการปรับเปลี่ยนด้านนอกของบล็อกแบบคงที่แล้วมันอาจจะ ok ClassLoaderเพราะทุกการเริ่มต้นคงที่จะตรงตาม นั่นเป็นคำถามที่แยกต่างหากจากคำถามของตัวเอง ฉันยังคงซิงโครไนซ์และโปรไฟล์อย่างชัดเจนเพื่อตรวจสอบว่าเป็นสาเหตุของปัญหาประสิทธิภาพที่แท้จริง
Heath Borders

@HeathBorders - "การอัพเดตหน่วยความจำ" หมายถึงอะไร JVM เป็นรูปแบบที่เป็นทางการซึ่งกำหนดสิ่งต่าง ๆ เช่นการมองเห็นอะตอมมิกเกิดขึ้นก่อนความสัมพันธ์ แต่ไม่ใช้คำเช่น "การอัพเดตหน่วยความจำ" คุณควรชี้แจงโดยเฉพาะอย่างยิ่งการใช้คำศัพท์จาก JLS
BeeOnRope

2
@Dave - ผมถือว่าคุณไม่ได้ยังคงมองหาคำตอบหลังจาก 8 ปี แต่สำหรับบันทึกความสับสนสำคัญในเกือบทุกคำตอบก็คือพวกเขามุ่งเน้นไปที่การกระทำที่คุณใช้เวลาในวัตถุแผนที่ คุณได้อธิบายแล้วว่าคุณไม่เคยแก้ไขวัตถุดังนั้นสิ่งที่ไม่เกี่ยวข้องทั้งหมด ศักยภาพ "gotcha" ที่เป็นไปได้เพียงอย่างเดียวคือวิธีที่คุณเผยแพร่การอ้างอิงถึงสิ่งMapที่คุณไม่ได้อธิบาย หากคุณไม่ทำอย่างปลอดภัยมันจะไม่ปลอดภัย ถ้าคุณทำมันได้อย่างปลอดภัยมันเป็น รายละเอียดในคำตอบของฉัน
BeeOnRope

คำตอบ:


55

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

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

ตัวอย่างเช่นสมมติว่าคุณเผยแพร่แผนที่ดังนี้:

class SomeClass {
   public static HashMap<Object, Object> MAP;

   public synchronized static setMap(HashMap<Object, Object> m) {
     MAP = m;
   }
}

... และ ณ จุดหนึ่งsetMap()มีการเรียกใช้แผนที่และมีเธรดอื่น ๆ ที่ใช้SomeClass.MAPในการเข้าถึงแผนที่และตรวจสอบ null เช่นนี้:

HashMap<Object,Object> map = SomeClass.MAP;
if (map != null) {
  .. use the map
} else {
  .. some default behavior
}

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

หากต้องการเผยแพร่แผนที่อย่างปลอดภัยคุณต้องสร้างความสัมพันธ์ก่อนเกิดขึ้นระหว่างการเขียนการอ้างอิงถึงHashMap(เช่นสิ่งพิมพ์ ) และผู้อ่านที่ตามมาของการอ้างอิงนั้น (เช่นปริมาณการใช้) สะดวกมีเพียงไม่กี่วิธีที่ง่ายต่อการจดจำประสบความสำเร็จที่[1] :

  1. แลกเปลี่ยนการอ้างอิงผ่านช่องที่ล็อคอย่างเหมาะสม ( JLS 17.4.5 )
  2. ใช้ initializer คงที่เพื่อทำการเริ่มต้นร้านค้า ( JLS 12.4 )
  3. แลกเปลี่ยนการอ้างอิงผ่านสนามระเหย ( JLS 17.4.5 ) หรือเป็นผลมาจากกฎนี้ผ่านคลาส AtomicX
  4. เริ่มต้นค่าลงในเขตข้อมูลสุดท้าย ( JLS 17.5 )

สิ่งที่น่าสนใจที่สุดสำหรับสถานการณ์ของคุณคือ (2), (3) และ (4) โดยเฉพาะอย่างยิ่ง (3) นำไปใช้โดยตรงกับรหัสที่ฉันมีด้านบน: หากคุณเปลี่ยนการประกาศMAPเป็น:

public static volatile HashMap<Object, Object> MAP;

ดังนั้นทุกอย่างเป็นเพียว: ผู้อ่านที่เห็นค่าที่ไม่เป็นศูนย์จำเป็นต้องมีความสัมพันธ์ที่เกิดขึ้นก่อนกับร้านค้าMAPและด้วยเหตุนี้จึงเห็นร้านค้าทั้งหมดที่เกี่ยวข้องกับการเริ่มต้นแผนที่

วิธีอื่นเปลี่ยนความหมายของวิธีการของคุณเนื่องจากทั้ง (2) (โดยใช้ static initalizer) และ (4) (ใช้ขั้นสุดท้าย ) หมายความว่าคุณไม่สามารถตั้งค่าMAPแบบไดนามิกที่รันไทม์ หากคุณไม่ต้องการทำเช่นนั้นเพียงแค่ประกาศMAPว่าเป็นstatic final HashMap<>และคุณได้รับการรับรองสิ่งพิมพ์ที่ปลอดภัย

ในทางปฏิบัติกฎนั้นง่ายสำหรับการเข้าถึง "วัตถุที่ไม่เคยแก้ไข" อย่างปลอดภัย:

หากคุณกำลังเผยแพร่วัตถุที่ไม่เปลี่ยนแปลงไม่ได้โดยเนื้อแท้ (เช่นในทุกสาขาที่ประกาศfinal) และ:

  • คุณสามารถสร้างวัตถุที่จะถูกกำหนดในขณะที่มีการประกาศa : เพียงแค่ใช้finalฟิลด์ (รวมถึงstatic finalสมาชิกแบบคงที่)
  • คุณต้องการที่จะกำหนดวัตถุที่ต่อมาหลังจากการอ้างอิงที่มีอยู่แล้วสามารถมองเห็นได้: ใช้สารระเหยฟิลด์ข

แค่นั้นแหละ!

ในทางปฏิบัติมันมีประสิทธิภาพมาก static finalตัวอย่างเช่นการใช้ฟิลด์ช่วยให้ JVM ถือว่าค่าไม่เปลี่ยนแปลงสำหรับอายุการใช้งานของโปรแกรมและปรับให้เหมาะสมอย่างมาก การใช้finalเขตข้อมูลสมาชิกช่วยให้สถาปัตยกรรมส่วนใหญ่สามารถอ่านฟิลด์ในลักษณะที่เทียบเท่ากับฟิลด์ปกติที่อ่านและไม่ยับยั้งการปรับให้เหมาะสมต่อไปc

ในที่สุดการใช้งานvolatileจะมีผลกระทบบางอย่าง: ไม่จำเป็นต้องใช้ฮาร์ดแวร์กั้นในสถาปัตยกรรมจำนวนมาก (เช่น x86 โดยเฉพาะที่ไม่อนุญาตให้อ่านเพื่อส่งผ่านการอ่าน) แต่การเพิ่มประสิทธิภาพและการเรียงลำดับใหม่อาจไม่เกิดขึ้นในเวลารวบรวม ผลกระทบโดยทั่วไปมีขนาดเล็ก ในการแลกเปลี่ยนคุณจะได้รับมากกว่าที่คุณขอไม่เพียง แต่คุณจะสามารถเผยแพร่ได้อย่างปลอดภัยHashMapคุณสามารถจัดเก็บจำนวนมากที่ไม่ได้แก้ไขHashMapตามที่คุณต้องการอ้างอิงเดียวกันและมั่นใจได้ว่าผู้อ่านทุกคนจะเห็นแผนที่ที่เผยแพร่อย่างปลอดภัย .

สำหรับรายละเอียดเต็มไปด้วยเลือดเพิ่มเติมโปรดดูที่Shipilevหรือคำถามที่พบบ่อยนี้โดยแมนสันและเก๊


[1] โดยตรงข้อความจากshipilev


aนั่นฟังดูซับซ้อน แต่สิ่งที่ฉันหมายถึงคือคุณสามารถกำหนดการอ้างอิงในเวลาก่อสร้างได้ไม่ว่าจะที่จุดประกาศหรือในตัวสร้าง (เขตข้อมูลสมาชิก) หรือตัวย่อคงที่ (แบบคงที่)

bหรือคุณสามารถใช้synchronizedวิธีรับ / ตั้งค่าAtomicReferenceหรืออะไรก็ได้ แต่เรากำลังพูดถึงงานขั้นต่ำที่คุณสามารถทำได้

c สถาปัตยกรรมบางอย่างที่มีโมเดลหน่วยความจำอ่อนแอมาก (ฉันกำลังมองคุณอัลฟ่า) อาจต้องการกำแพงกั้นการอ่านบางประเภทก่อนfinalอ่าน - แต่สิ่งเหล่านี้หายากมากในวันนี้


never modify HashMapไม่ได้หมายความว่าstate of the map objectเป็นเธรดที่ปลอดภัยฉันคิดว่า พระเจ้าทรงทราบการใช้ห้องสมุดถ้าเอกสารทางการไม่ได้บอกว่ามันปลอดภัย
Jiang YD

@JiangYD - คุณอยู่ที่นั่นมีพื้นที่สีเทาในบางกรณี: เมื่อเราพูดว่า "แก้ไข" สิ่งที่เราหมายถึงคือการกระทำใด ๆ ที่ดำเนินการภายในเขียนบางอย่างที่อาจแข่งกับการอ่านหรือเขียนในกระทู้อื่น ๆ การเขียนเหล่านี้อาจเป็นรายละเอียดการใช้งานภายในดังนั้นแม้แต่การดำเนินการที่ดูเหมือนว่า "อ่านอย่างเดียว" อย่างที่get()จริงแล้วอาจทำการเขียนบางอย่างพูดอัปเดตสถิติบางอย่าง (หรือในกรณีที่มีLinkedHashMapการเข้าถึงโดยใช้คำสั่ง ดังนั้นคลาสที่มีการเขียนที่ดีควรจัดเตรียมเอกสารบางอย่างซึ่งทำให้ชัดเจนถ้า ...
BeeOnRope

... เห็นได้ชัดว่าการดำเนินการ "อ่านอย่างเดียว" นั้นเป็นแบบอ่านอย่างเดียวภายในโดยคำนึงถึงความปลอดภัยของเธรด ในไลบรารีมาตรฐาน C ++ มีกฎแบบครอบคลุมที่ฟังก์ชันสมาชิกทำเครื่องหมายconstเป็นแบบอ่านอย่างเดียวในแง่นี้ (ภายในพวกเขายังคงสามารถทำการเขียนได้ แต่สิ่งเหล่านี้จะต้องทำให้ปลอดภัยต่อเธรด) ไม่มีconstคำหลักใน Java และฉันไม่ทราบถึงการรับประกันแบบครอบคลุมเอกสารใด ๆ แต่โดยทั่วไปแล้วคลาสไลบรารีมาตรฐานจะทำงานตามที่คาดไว้และมีการจัดทำเอกสารข้อยกเว้น (ดูLinkedHashMapตัวอย่างที่getมีการกล่าวถึงRO ที่ไม่ปลอดภัยอย่างชัดเจน)
BeeOnRope

@JiangYD - ในที่สุดกลับไปที่คำถามเดิมของคุณเพราะHashMapเรามีสิทธิ์ในเอกสารเกี่ยวกับพฤติกรรมความปลอดภัยของเธรดสำหรับคลาสนี้: หากมีหลายเธรดเข้าถึงแผนที่แฮชพร้อมกันและอย่างน้อยหนึ่งเธรดจะแก้ไขแผนที่โครงสร้าง มันจะต้องทำข้อมูลให้ตรงกันจากภายนอก (การปรับเปลี่ยนโครงสร้างเป็นการดำเนินการใด ๆ ที่เพิ่มหรือลบการแมปหนึ่งรายการขึ้นไปเพียงแค่เปลี่ยนค่าที่เกี่ยวข้องกับคีย์ที่อินสแตนซ์ที่มีอยู่แล้วไม่ใช่การปรับเปลี่ยนโครงสร้าง)
BeeOnRope

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

70

Jeremy Manson พระเจ้าเมื่อมาถึง Java Memory Model มีบล็อกสามส่วนในหัวข้อนี้ - เพราะโดยพื้นฐานแล้วคุณกำลังถามคำถาม "ปลอดภัยหรือไม่ที่จะเข้าถึง HashMap ที่ไม่เปลี่ยนรูป" - คำตอบคือใช่ แต่คุณต้องตอบคำกริยาสำหรับคำถามนั้นคือ - "HashMap ของฉันไม่เปลี่ยนรูป" คำตอบอาจทำให้คุณประหลาดใจ - Java มีกฎที่ค่อนข้างซับซ้อนในการพิจารณาความไม่สามารถเปลี่ยนแปลงได้

สำหรับข้อมูลเพิ่มเติมเกี่ยวกับหัวข้ออ่านโพสต์บล็อกของ Jeremy:

ส่วนที่ 1 เกี่ยวกับความไม่สามารถเปลี่ยนแปลงได้ใน Java: http://jeremymanson.blogspot.com/2008/04/immutability-in-java.html

ส่วนที่ 2 เกี่ยวกับความไม่สามารถเปลี่ยนได้ใน Java: http://jeremymanson.blogspot.com/2008/07/immutability-in-java-part-2.html

ส่วนที่ 3 เกี่ยวกับความไม่สามารถเปลี่ยนแปลงได้ใน Java: http://jeremymanson.blogspot.com/2008/07/immutability-in-java-part-3.html


3
เป็นจุดที่ดี แต่ฉันอาศัยการเริ่มต้นคงที่ในระหว่างที่ไม่มีการอ้างอิงหลบหนีดังนั้นมันควรจะปลอดภัย
Dave L.

5
ฉันล้มเหลวที่จะดูว่านี่เป็นคำตอบที่ได้รับการจัดอันดับสูง (หรือแม้แต่คำตอบ) มันไม่ได้สำหรับหนึ่งแม้จะตอบคำถามและมันก็ไม่ได้พูดถึงหลักการที่สำคัญอย่างหนึ่งที่จะตัดสินใจว่ามันมีความปลอดภัยหรือไม่: สิ่งพิมพ์ปลอดภัย "ตอบ" เดือดลงไปที่ "มันหลอกลวง" และนี่คือลิงก์สามรายการ (คอมเพล็กซ์) ที่คุณสามารถอ่านได้
BeeOnRope

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

@ เจสซีเขาไม่ตอบคำถามท้ายประโยคแรกเขาตอบคำถาม "ปลอดภัยไหมที่จะเข้าถึงวัตถุที่ไม่เปลี่ยนรูป" ซึ่งอาจหรือไม่อาจใช้กับคำถามของ OP ในขณะที่เขาชี้ให้เห็นในประโยคถัดไป โดยพื้นฐานแล้วนี่เป็นคำตอบประเภท "ไปคิดด้วยตัวเอง" ของลิงก์อย่างเดียวซึ่งไม่ใช่คำตอบที่ดีสำหรับ SO สำหรับ upvotes ฉันคิดว่ามันเป็นหน้าที่ของอายุ 10.5 ปีขึ้นไปและเป็นหัวข้อที่ถูกสืบค้นบ่อยๆ มันได้รับ upvotes สุทธิน้อยมากในช่วงไม่กี่ปีที่ผ่านมาดังนั้นคนอาจจะมารอบ ๆ :)
BeeOnRope

35

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

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

ดูบทความของ Brian Goetz ใน Java Memory Model ใหม่สำหรับรายละเอียด


ขออภัยสำหรับคู่ที่ฮีทเธอร์ฉันสังเกตเห็นคุณหลังจากที่ฉันส่งของแล้วเท่านั้น :)
Alexander

2
ฉันแค่ดีใจที่มีคนอื่นที่นี่ที่จริงเข้าใจถึงผลกระทบของหน่วยความจำ
Heath Borders

1
อันที่จริงแม้ว่าจะไม่มีเธรดใด ๆ ที่จะเห็นวัตถุก่อนที่มันจะเริ่มต้นได้อย่างถูกต้องดังนั้นฉันจึงไม่คิดว่าเป็นปัญหาในกรณีนี้
Dave L.

1
ขึ้นอยู่กับว่าวัตถุนั้นถูกเริ่มต้นอย่างไร
Bill Michell

1
คำถามบอกว่าเมื่อ HashMap ได้รับการเริ่มต้นเขาไม่ต้องการที่จะปรับปรุงอีกต่อไป จากนั้นเขาต้องการใช้มันเป็นโครงสร้างข้อมูลแบบอ่านอย่างเดียว ฉันคิดว่ามันปลอดภัยที่จะทำเช่นนั้นข้อมูลที่เก็บไว้ในแผนที่ของเขานั้นไม่เปลี่ยนรูป
Binita Bharati

9

หลังจากดูเพิ่มอีกนิดฉันพบสิ่งนี้ในjava doc (focus mine):

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

สิ่งนี้ดูเหมือนจะบอกเป็นนัยว่ามันจะปลอดภัยโดยสมมติว่าการสนทนาของแถลงการณ์มีจริง


1
ในขณะที่นี่คือคำแนะนำที่ดีเยี่ยมเช่นเดียวกับคำตอบอื่น ๆ ของรัฐมีคำตอบที่เหมาะสมยิ่งขึ้นในกรณีของอินสแตนซ์แผนที่ที่เปลี่ยนแปลงไม่ได้และปลอดภัย แต่คุณควรทำอย่างนั้นก็ต่อเมื่อคุณรู้ว่าคุณกำลังทำอะไรอยู่
Alex Miller

1
หวังว่าด้วยคำถามเช่นเรามากกว่านี้จะรู้ว่าเรากำลังทำอะไรอยู่
Dave L.

มันไม่ถูกต้องจริงๆ ในฐานะที่เป็นคำตอบอื่น ๆ รัฐจะต้องมีสิ่งที่เกิดขึ้นก่อนระหว่างการแก้ไขครั้งล่าสุดและการอ่าน "thread safe" ที่ตามมาทั้งหมด โดยทั่วไปหมายความว่าคุณจะต้องเผยแพร่วัตถุอย่างปลอดภัยหลังจากที่มันถูกสร้างขึ้นและทำการแก้ไข ดูคำตอบที่ถูกต้องก่อนทำเครื่องหมาย
markspace

9

สิ่งหนึ่งที่ทราบคือในบางกรณีการรับ () จาก HashMap ที่ไม่ซิงโครไนซ์สามารถทำให้เกิดการวนซ้ำไม่สิ้นสุด สิ่งนี้สามารถเกิดขึ้นได้หาก put () พร้อมกันทำให้เกิดการทำแผนที่ใหม่

http://lightbody.net/blog/2005/07/hashmapget_can_cause_an_infini.html


1
ที่จริงผมได้เห็นนี้แขวน JVM โดยไม่ต้องบริโภค CPU (ซึ่งอาจจะแย่ลง)
ปีเตอร์ Lawrey

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

@AlexMiller นอกเหนือจากเหตุผลอื่น ๆ (ฉันถือว่าคุณอ้างถึงการเผยแพร่อย่างปลอดภัย) ฉันไม่คิดว่าการเปลี่ยนแปลงการใช้งานควรเป็นเหตุผลในการคลายข้อ จำกัด การเข้าถึงเว้นแต่ว่าเอกสารได้รับอนุญาตอย่างชัดเจน เมื่อมันเกิดขึ้นHashMap Javadocสำหรับ Java 8 ยังคงมีคำเตือนนี้:Note that this implementation is not synchronized. If multiple threads access a hash map concurrently, and at least one of the threads modifies the map structurally, it must be synchronized externally.
shmosel

8

มีการบิดที่สำคัญแม้ว่า มีความปลอดภัยในการเข้าถึงแผนที่ แต่โดยทั่วไปไม่รับประกันว่ากระทู้ทั้งหมดจะเห็นสถานะเดียวกัน (และค่านิยม) ของ HashMap สิ่งนี้อาจเกิดขึ้นในระบบมัลติโปรเซสเซอร์ซึ่งการแก้ไข HashMap ที่ทำโดยหนึ่งเธรด (เช่นหนึ่งที่มีประชากร) สามารถนั่งในแคชของ CPU นั้นและจะไม่เห็นโดยเธรดที่ทำงานบน CPU อื่นจนกว่าหน่วยความจำรั้วจะทำงาน ดำเนินการเพื่อให้มั่นใจว่าการเชื่อมโยงกันของแคช Java Language Specification ชัดเจนในที่นี้: การแก้ปัญหาคือการได้รับล็อค (ตรงกัน (... ())) ซึ่งส่งเสียงการดำเนินงานรั้วหน่วยความจำ ดังนั้นหากคุณแน่ใจว่าหลังจากการเติม HashMap แต่ละเธรดจะได้รับการล็อกใด ๆ แล้วก็ตกลงจากจุดนั้นในการเข้าถึง HashMap จากเธรดใด ๆ จนกว่า HashMap จะถูกแก้ไขอีกครั้ง


ฉันไม่แน่ใจว่าการเข้าถึงเธรดจะได้รับการล็อคใด ๆ แต่ฉันแน่ใจว่าพวกเขาจะไม่ได้รับการอ้างอิงไปยังวัตถุจนกว่าจะได้รับการเริ่มต้นดังนั้นฉันจึงไม่คิดว่าพวกเขาสามารถคัดลอกค้างได้
Dave L.

@Alex: การอ้างอิงถึง HashMap นั้นสามารถระเหยได้เพื่อสร้างการรับประกันการมองเห็นของหน่วยความจำเดียวกัน @Dave: เป็นไปได้ที่จะเห็นการอ้างอิงถึง objs ใหม่ก่อนที่งานของ ctor จะปรากฏให้เห็นในเธรดของคุณ
Chris Vest

@ คริสเตียนในกรณีทั่วไปอย่างแน่นอน ฉันกำลังบอกว่าในรหัสนี้มันไม่ใช่
Dave L.

การรับล็อค RANDOM ไม่ได้รับประกันว่าจะล้างแคช cpu ทั้งหมดของเธรด ขึ้นอยู่กับการใช้งาน JVM และมีโอกาสมากที่จะไม่ทำเช่นนี้
ปิแอร์

ฉันเห็นด้วยกับปิแอร์ฉันไม่คิดว่าการได้รับล็อคใด ๆ จะเพียงพอ คุณต้องซิงโครไนซ์บนล็อคเดียวกันเพื่อให้การเปลี่ยนแปลงปรากฏ
damluar

5

ตามhttp://www.ibm.com/developerworks/java/library/j-jtp03304/ # ความปลอดภัยในการเริ่มต้นคุณสามารถทำให้ HashMap ของคุณเป็นฟิลด์สุดท้ายและหลังจากตัวสร้างเสร็จสิ้นจะได้รับการเผยแพร่อย่างปลอดภัย

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


คำตอบนี้มีคุณภาพต่ำเช่นเดียวกับคำตอบจาก @taylor gauthier แต่มีรายละเอียดน้อยกว่า
Snicolas

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

ฮะ? นี่เป็นคำตอบเดียวที่ถูกต้องที่ฉันพบหลังจากเลื่อนดูคำตอบที่ได้รับคะแนนสูงกว่า คีย์ถูกเผยแพร่อย่างปลอดภัยและนี่เป็นคำตอบเดียวที่แม้จะกล่าวถึง
BeeOnRope

3

ดังนั้นสถานการณ์ที่คุณอธิบายคือคุณต้องใส่ข้อมูลจำนวนมากลงในแผนที่จากนั้นเมื่อคุณเติมข้อมูลเสร็จแล้วคุณจะถือว่ามันไม่เปลี่ยนรูป วิธีการหนึ่งที่ "ปลอดภัย" (หมายถึงคุณบังคับใช้ว่ามันได้รับการปฏิบัติอย่างไม่เปลี่ยนรูป) คือการแทนที่การอ้างอิงด้วยCollections.unmodifiableMap(originalMap)เมื่อคุณพร้อมที่จะทำให้ไม่เปลี่ยนรูป

สำหรับตัวอย่างของวิธีการแมปไม่ดีสามารถล้มเหลวหากใช้พร้อมกันและวิธีแก้ปัญหาที่แนะนำที่ฉันกล่าวถึงให้ตรวจสอบรายการข้อผิดพลาดในการแห่นี้: bug_id = 6423457


2
นี่คือ "ปลอดภัย" ในการที่บังคับใช้ไม่เปลี่ยนรูป แต่ไม่ได้แก้ไขปัญหาความปลอดภัยของเธรด หากแผนที่ปลอดภัยในการเข้าถึงด้วย wrapper UnmodifiableMap แสดงว่าแผนที่นั้นปลอดภัยโดยไม่ต้องใช้และในทางกลับกัน
Dave L.

2

คำถามนี้ได้รับการแก้ไขในหนังสือ "Java Concurrency in Practice" ของ Brian Goetz (รายการ 16.8 หน้า 350):

@ThreadSafe
public class SafeStates {
    private final Map<String, String> states;

    public SafeStates() {
        states = new HashMap<String, String>();
        states.put("alaska", "AK");
        states.put("alabama", "AL");
        ...
        states.put("wyoming", "WY");
    }

    public String getAbbreviation(String s) {
        return states.get(s);
    }
}

เนื่องจากstatesมีการประกาศเป็นfinalและการกำหนดค่าเริ่มต้นเสร็จสมบูรณ์ภายในตัวสร้างคลาสของเจ้าของเธรดใด ๆ ที่อ่านแผนที่นี้ในภายหลังจะรับประกันว่าจะเห็นเมื่อเวลาที่ตัวสร้างเสร็จสิ้นโดยที่ไม่มีเธรดอื่นใดที่จะพยายามแก้ไขเนื้อหาของแผนที่


1

ได้รับการเตือนว่าแม้ในรหัสที่มีเธรดเดียวการแทนที่ ConcurrentHashMap ด้วย HashMap อาจไม่ปลอดภัย ConcurrentHashMap ห้ามใช้ null เป็นคีย์หรือค่า HashMap ไม่ได้ห้ามพวกเขา (ไม่ต้องถาม)

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

ที่กล่าวว่าหากคุณไม่ทำสิ่งใดการอ่านจาก HashMap ที่เกิดขึ้นพร้อมกันนั้นปลอดภัย

[แก้ไข: โดย "อ่านพร้อมกัน" ฉันหมายความว่ายังไม่มีการแก้ไขพร้อมกัน

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

ความตั้งใจของฉันคือไม่ขัดต่อคำตอบที่ละเอียดยิ่งขึ้นเกี่ยวกับ Java Memory Model คำตอบนี้มีจุดมุ่งหมายเพื่อชี้ให้เห็นว่านอกเหนือจากปัญหาการเกิดพร้อมกันมีอย่างน้อยหนึ่งความแตกต่างระหว่าง API ConcurrentHashMap และ HashMap ซึ่งสามารถ scupper แม้โปรแกรมเธรดเดียวซึ่งแทนที่หนึ่งด้วยอื่น ๆ ]


ขอบคุณสำหรับคำเตือน แต่ไม่มีความพยายามที่จะใช้ปุ่มหรือค่า null
Dave L.

คิดว่าคงไม่มี nulls ในคอลเล็กชันเป็นมุมที่บ้าคลั่งของ Java
Steve Jessop

ฉันไม่เห็นด้วยกับคำตอบนี้ "การอ่านจาก HashMap พร้อมกันนั้นปลอดภัย" โดยตัวของมันเองนั้นไม่ถูกต้อง ไม่ได้ระบุว่าการอ่านเกิดขึ้นกับแผนที่ที่ไม่แน่นอนหรือไม่เปลี่ยนรูป เพื่อให้ถูกต้องคุณควรอ่าน "การอ่านพร้อมกันจาก HashMap ที่ไม่เปลี่ยนรูปแบบนั้นปลอดภัย"
Taylor Gautier

2
ไม่เป็นไปตามบทความที่คุณเชื่อมโยงด้วยตัวเอง: ข้อกำหนดคือต้องไม่เปลี่ยนแผนที่ (และการเปลี่ยนแปลงก่อนหน้านี้จะต้องปรากฏในทุกเธรดผู้อ่าน) ไม่ใช่ว่ามันไม่เปลี่ยนรูป (ซึ่งเป็นศัพท์เทคนิคใน Java และเป็น เงื่อนไขที่เพียงพอ แต่ไม่จำเป็นสำหรับความปลอดภัย)
Steve Jessop

นอกจากนี้ยังมีข้อสังเกต ... การเริ่มต้นคลาสโดยนัยบนซิงก์เดียวกัน (ใช่คุณสามารถหยุดชะงักในเขตข้อมูลเริ่มต้นแบบคงที่) ดังนั้นหากการเริ่มต้นของคุณเกิดขึ้นแบบสแตติกมันจะเป็นไปไม่ได้สำหรับคนอื่นที่จะเห็นมัน พวกเขาจะต้องถูกบล็อกในวิธี ClassLoader.loadClass ในการล็อคเดียวกันที่ได้รับ ... และถ้าคุณสงสัยเกี่ยวกับ classloaders ที่แตกต่างกันที่มีสำเนาของเขตข้อมูลเดียวกันคุณจะถูกต้อง ... แต่นั่นจะเป็นมุมฉากกับ ความคิดของสภาพการแข่งขัน; ฟิลด์สแตติกของ classloader แชร์รั้วหน่วยความจำ
Ajax

0

http://www.docjar.com/html/api/java/util/HashMap.java.html

นี่คือแหล่งข้อมูลสำหรับ HashMap อย่างที่คุณสามารถบอกได้ว่าไม่มีรหัสล็อค / mutex อยู่ที่นั่น

ซึ่งหมายความว่าในขณะที่มันโอเคที่จะอ่านจาก HashMap ในสถานการณ์แบบมัลติเธรดฉันต้องใช้ ConcurrentHashMap แน่นอนถ้ามีหลาย ๆ การเขียน

สิ่งที่น่าสนใจคือทั้ง. NET HashTable และ Dictionary <K, V> ได้สร้างขึ้นในรหัสการประสาน


2
ฉันคิดว่ามีคลาสที่การอ่านเพียงอย่างเดียวสามารถทำให้คุณเดือดร้อนได้เนื่องจากการใช้ตัวแปรอินสแตนซ์ชั่วคราวภายใน ดังนั้นหนึ่งอาจต้องตรวจสอบแหล่งที่มาอย่างรอบคอบมากกว่าการสแกนอย่างรวดเร็วสำหรับการล็อค / รหัส mutex
Dave L.

0

หากการกำหนดค่าเริ่มต้นและทุกรายการถูกซิงโครไนซ์คุณจะได้รับการบันทึก

รหัสต่อไปนี้ถูกบันทึกเนื่องจาก classloader จะดูแลการซิงโครไนซ์:

public static final HashMap<String, String> map = new HashMap<>();
static {
  map.put("A","A");

}

รหัสต่อไปนี้ถูกบันทึกเนื่องจากการเขียนของ volatile จะดูแลการซิงโครไนซ์

class Foo {
  volatile HashMap<String, String> map;
  public void init() {
    final HashMap<String, String> tmp = new HashMap<>();
    tmp.put("A","A");
    // writing to volatile has to be after the modification of the map
    this.map = tmp;
  }
}

วิธีนี้จะใช้งานได้หากตัวแปรสมาชิกถือเป็นที่สิ้นสุดเพราะสุดท้ายยังมีความผันผวน และถ้าวิธีนี้เป็นนวกรรมิก

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