กลับไปหน้าบทความ

อ่าน 9 นาที

Java ConcurrentMap

Java Collections Framework เวอร์ชัน 1.5 และเวอร์ชันต่อมา ของ ภาษาการเขียนโปรแกรม Java กำหนดและใช้งานแผนที่แบบเธรดเดียวปกติแบบดั้งเดิม...

Java ConcurrentMap

Java Collections Framework เวอร์ชัน 1.5 และเวอร์ชันต่อมา ของภาษาการเขียนโปรแกรม Java กำหนดและใช้งานแผนที่แบบเธรดเดียวปกติแบบดั้งเดิม รวมถึงแผนที่แบบเธรดปลอดภัยใหม่ที่ใช้งานอินเทอร์เฟซ รวมถึงอินเทอร์เฟซแบบขนานอื่นๆ[ 1 ] ใน Java 1.6 อินเทอร์เฟซถูกเพิ่มเข้ามาโดยขยายและอินเทอร์เฟซถูกเพิ่มเข้ามาเป็นการรวมกันของซับอินเทอร์เฟซ java.util.concurrent.ConcurrentMapjava.util.NavigableMapjava.util.SortedMapjava.util.concurrent.ConcurrentNavigableMap

อินเทอร์เฟซแผนที่ Java

แผนภาพอินเทอร์เฟ ซเวอร์ชัน 1.8 java.util.Map<K, V>มีรูปร่างดังด้านล่าง เซตสามารถพิจารณาได้ว่าเป็นกรณีย่อยของแผนที่ที่สอดคล้องกัน โดยที่ค่าต่างๆ จะเป็นค่าคงที่เฉพาะที่สามารถละเลยได้ แม้ว่าjava.util.Set<E>API จะใช้วิธีการที่สอดคล้องกันแต่มีชื่อแตกต่างกันก็ตาม ที่ด้านล่างสุดคือjava.util.concurrent.ConcurrentNavigableMap<K, V>ซึ่งเป็นการสืบทอดแบบหลายทาง

  • java.util.Collection
    • java.util.Map
      • java.util.SortedMap
        • java.util.NavigableMap
          • java.util.concurrent.ConcurrentNavigableMap
      • java.util.concurrent.ConcurrentMap
        • java.util.concurrent.ConcurrentNavigableMap

การนำไปใช้

ConcurrentHashMap

สำหรับการเข้าถึงแบบไม่เรียงลำดับตามที่กำหนดไว้ในjava.util.Map<K, V>อินเทอร์เฟซjava.util.concurrent.ConcurrentHashMap<K, V>จะใช้การใช้งานjava.util.concurrent.ConcurrentMap<K, V>[ 2 ] กลไกคือการเข้าถึงแฮชไปยังตารางแฮชที่มีรายการของรายการต่างๆ โดยแต่ละรายการจะเก็บคีย์ ค่า แฮช และการอ้างอิงถัดไป ก่อน Java 8 มีการล็อกหลายตัวซึ่งแต่ละตัวจะเรียงลำดับการเข้าถึง 'ส่วน' ของตาราง ใน Java 8 จะใช้การซิงโครไนซ์แบบเนทีฟกับส่วนหัวของรายการเอง และรายการสามารถกลายร่างเป็นต้นไม้ขนาดเล็กได้เมื่อมีแนวโน้มที่จะเติบโตใหญ่เกินไปเนื่องจากการชนกันของแฮชที่ไม่พึงประสงค์ นอกจากนี้ Java 8 ยังใช้พรีมิทีฟ compare-and-set แบบมองโลกในแง่ดีเพื่อวางส่วนหัวเริ่มต้นในตาราง ซึ่งเร็วมาก ประสิทธิภาพคือแต่บางครั้งอาจเกิดความล่าช้าเมื่อจำเป็นต้องทำการแฮชใหม่ หลังจากที่ตารางแฮชขยายแล้ว จะไม่หดตัวลงอีก ซึ่งอาจนำไปสู่ ​​'การรั่วไหล' ของหน่วยความจำหลังจากลบรายการออก

ConcurrentSkipListMap

สำหรับการเข้าถึงตามลำดับที่กำหนดโดยjava.util.NavigableMap<K, V>อินเทอร์เฟซjava.util.concurrent.ConcurrentSkipListMap<K, V>ได้ถูกเพิ่มใน Java 1.6 [ 1 ]และใช้งานjava.util.concurrent.ConcurrentMap<K, V>และเช่นกันjava.util.concurrent.ConcurrentNavigableMap<K, V>เป็นรายการข้ามที่ใช้เทคนิคแบบไม่ต้องล็อกเพื่อสร้างต้นไม้ ประสิทธิภาพคือ

ปัญหาการแก้ไขพร้อมกัน

หนึ่งในปัญหาที่ได้รับการแก้ไขโดยแพ็กเกจ Java 1.5 java.util.concurrent<K, V>คือปัญหาการแก้ไขพร้อมกัน คลาสคอลเลกชันที่แพ็กเกจนี้จัดให้สามารถใช้งานได้อย่างน่าเชื่อถือโดยผู้ใช้หลายjava.lang.Threadราย

แผนที่และคอลเลกชันอื่นๆ ที่ใช้เธรดร่วมกันและไม่ทำงานพร้อมกัน จำเป็นต้องใช้การล็อกแบบชัดเจน เช่น การซิงโครไนซ์แบบเนทีฟ เพื่อป้องกันการแก้ไขพร้อมกัน หรือไม่ก็ต้องมีวิธีพิสูจน์จากตรรกะของโปรแกรมว่าการแก้ไขพร้อมกันจะไม่เกิดขึ้น การแก้ไขพร้อมกันของแผนที่java.lang.Map<K, V>โดยหลายเธรดบางครั้งจะทำลายความสอดคล้องภายในของโครงสร้างข้อมูลภายในแผนที่java.lang.Map<K, V>ทำให้เกิดบั๊กซึ่งปรากฏขึ้นไม่บ่อยหรือคาดเดาไม่ได้ และตรวจจับและแก้ไขได้ยาก นอกจากนี้ การแก้ไขพร้อมกันโดยเธรดหนึ่งที่มีสิทธิ์อ่านโดยเธรดอื่นหรือหลายเธรด บางครั้งอาจให้ผลลัพธ์ที่คาดเดาไม่ได้แก่ผู้อ่าน แม้ว่าความสอดคล้องภายในของแผนที่จะไม่ถูกทำลายก็ตาม การใช้ตรรกะของโปรแกรมภายนอกเพื่อป้องกันการแก้ไขพร้อมกันจะเพิ่มความซับซ้อนของโค้ดและสร้างความเสี่ยงที่ไม่สามารถคาดเดาได้ของข้อผิดพลาดในโค้ดที่มีอยู่และในอนาคต แม้ว่าจะช่วยให้สามารถใช้คอลเลกชันที่ไม่ทำงานพร้อมกันได้ก็ตาม อย่างไรก็ตาม ทั้งการล็อกหรือตรรกะของโปรแกรมไม่สามารถประสานงานเธรดภายนอกที่อาจเข้ามาสัมผัสกับแผนที่java.util.Collection<E>ได้

ตัวนับการแก้ไข

เพื่อช่วยแก้ไขปัญหาการแก้ไขพร้อมกันjava.lang.Map<K, V>การใช้งานแบบไม่พร้อมกันและjava.util.Collection<E>แบบอื่นๆ จะใช้ตัวนับการแก้ไขภายในซึ่งจะถูกตรวจสอบก่อนและหลังการอ่านเพื่อเฝ้าดูการเปลี่ยนแปลง: ผู้เขียนจะเพิ่มตัวนับการแก้ไข การแก้ไขพร้อมกันควรจะถูกตรวจพบโดยกลไกนี้ โดยจะโยนjava.util.ConcurrentModificationException[ 3 ] แต่ไม่รับประกันว่าจะเกิดขึ้นในทุกกรณีและไม่ควรพึ่งพา การบำรุงรักษาตัวนับยังเป็นตัวลดประสิทธิภาพอีกด้วย ด้วยเหตุผลด้านประสิทธิภาพ ตัวนับจึงไม่เปลี่ยนแปลง ดังนั้นจึงไม่รับประกันว่าการเปลี่ยนแปลงใดๆ จะถูกส่งต่อThreadระหว่าง

Collections.synchronizedMap()

วิธีแก้ปัญหาการแก้ไขพร้อมกันวิธีหนึ่งคือการใช้คลาส wrapper เฉพาะที่จัดเตรียมโดย factory ในjava.util.Collections : public static <K, V> Map<K, V> synchronizedMap(Map<K, V> m)ซึ่งห่อหุ้ม non-thread-safe ที่มีอยู่Mapด้วยเมธอดที่ซิงโครไนซ์บน mutex ภายใน[ 4 ]นอกจากนี้ยังมี wrapper สำหรับ s ประเภทอื่นๆ ด้วยjava.util.Collection<E>นี่เป็นวิธีแก้ปัญหาบางส่วน เนื่องจากยังคงเป็นไปได้ ที่ s ที่เก็บหรือได้รับอ้างอิงที่ไม่ได้ห่อหุ้มjava.util.Map<K, V>จะเข้าถึง s พื้นฐานโดยไม่ได้ตั้งใจ นอกจากนี้ s ทั้งหมดจะใช้แต่ s ที่ห่อหุ้มด้วย synchronized และs ที่ห่อหุ้มอื่นๆ ไม่ได้จัดเตรียม iterator ที่ซิงโครไนซ์ ดังนั้นการซิงโครไนซ์จึงขึ้นอยู่กับโค้ดไคลเอ็นต์ ซึ่งช้าและมีแนวโน้มที่จะเกิดข้อผิดพลาด และไม่สามารถคาดหวังว่าจะถูกทำซ้ำโดยผู้บริโภคอื่นๆ ของ synchronized ได้ระยะเวลาทั้งหมดของการวนซ้ำจะต้องได้รับการป้องกันเช่นกัน ยิ่งไปกว่านั้น a ที่ถูกห่อหุ้มสองครั้งในที่ต่างๆ จะมีออบเจ็กต์ mutex ภายในที่แตกต่างกันซึ่งการซิงโครไนซ์ทำงาน ทำให้เกิดการทับซ้อนกันได้ การมอบหมายสิทธิ์ทำให้ประสิทธิภาพลดลง แต่คอมไพเลอร์แบบ just-in-time สมัยใหม่มักจะทำการ inline อย่างหนัก ทำให้การลดประสิทธิภาพมีจำกัด ต่อไปนี้คือวิธีการทำงานของการห่อหุ้มภายใน wrapper - mutex เป็นเพียง final และเป็น final ที่ถูกห่อหุ้ม: java.lang.Threadjava.util.Collection<E>java.lang.Iterablejava.util.Map<K, V>java.util.Collection<E>java.util.Map<K, V>java.util.Map<K, V>java.util.Objectmjava.util.Map<K, V>

public V put ( K key , V value ) { synchronized ( mutex ) { return m . put ( key , value ); } }

แนะนำให้ซิงโครไนซ์การวนซ้ำดังต่อไปนี้ อย่างไรก็ตาม การซิงโครไนซ์นี้จะอยู่บนตัวห่อหุ้มแทนที่จะเป็นบนมิวเท็กซ์ภายใน ทำให้เกิดการทับซ้อนกันได้: [ 5 ]

import java.util.Collections ; import java.util.Map ;Map < String , String > wrappedMap = Collections . synchronizedMap ( map );synchronized ( wrappedMap ) { for ( String s : wrappedMap . keySet ()) { // การดำเนินการที่อาจใช้เวลานาน ซึ่งอาจถูกเรียกใช้หลายครั้ง ทำให้การเข้าถึงอื่นๆ ทั้งหมดล่าช้า} }

การซิงโครไนซ์แบบเนทีฟ

สามารถใช้ Any java.util.Map<K, V>ได้อย่างปลอดภัยในระบบมัลติเธรด โดยตรวจสอบให้แน่ใจว่าการเข้าถึงทั้งหมดได้รับการจัดการโดยกลไกการซิงโครไนซ์ของ Java:

import java.util.HashMap ; import java.util.Map ;Map < String , String > map = new HashMap <> ();// เธรด A // ใช้แผนที่เองเป็นตัวล็อก สามารถใช้วัตถุใดๆ ที่ตกลงกันไว้แทนได้synchronized ( map ) { map . put ( "key" , "value" ); }// เธรด B ซิงโครไนซ์( map ) { String result = map . get ( "key" ); // ... }// เธรด C ซิงโครไนซ์( map ) { สำหรับ( Map . Entry < String , String > s : map . entrySet ()) { /*  * การดำเนินการบางอย่างที่อาจช้า ทำให้การดำเนินการอื่นๆ ที่ควรจะเร็วทั้งหมดล่าช้า * ไม่สามารถซิงโครไนซ์ในการวนซ้ำแต่ละครั้งได้ */ // ... } }

รีเอนแทรนต์อ่านเขียนล็อค

โค้ดที่ใช้ a java.util.concurrent.ReentrantReadWriteLockคล้ายกับโค้ดสำหรับการซิงโครไนซ์แบบเนทีฟ อย่างไรก็ตาม เพื่อความปลอดภัย ควรใช้ล็อกในบล็อก try/finally เพื่อให้การออกจากโปรแกรมก่อนกำหนด เช่นjava.lang.Exceptionการโยนหรือ break/continue จะต้องผ่านการปลดล็อก เทคนิคนี้ดีกว่าการใช้การซิงโครไนซ์[ 6 ]เนื่องจากการอ่านสามารถทับซ้อนกันได้ จึงมีปัญหาใหม่ในการตัดสินใจว่าจะจัดลำดับความสำคัญของการเขียนอย่างไรเมื่อเทียบกับการอ่าน เพื่อความเรียบง่ายjava.util.concurrent.ReentrantLockสามารถใช้ a แทนได้ ซึ่งจะไม่แยกความแตกต่างระหว่างการอ่านและการเขียน การดำเนินการกับล็อกมีมากกว่าการซิงโครไนซ์เช่น tryLock()และtryLock(long timeout, TimeUnit unit)

import java.util.Map ; import java.util.concurrent.locks.ReadWriteLock ; import java.util.concurrent.locks.ReentrantReadWriteLock ;final ReentrantReadWriteLock lock = new ReentrantReadWriteLock ( ); final ReadWriteLock readLock = lock.readLock ( ); final ReadWriteLock writeLock = lock.writeLock ( ) ;// เธรด A ลอง{ writeLock.lock ( ); map.put ( " key" , " value" ) ; } สุดท้าย{ writeLock.unlock ( ); }// เธรด B ลอง{ readLock.lock ( ); String s = map.get ( " key" ) ; } สุดท้าย{ readLock.unlock ( ) ; }// เธรด C ลอง{ readLock.lock (); สำหรับ( Map.Entry < String , String > s : map.entrySet ( ) ) { /* * การดำเนินการบางอย่าง ที่ อาจช้า ทำให้การดำเนินการอื่นๆ ที่ควรจะเร็วทั้งหมดล่าช้า* ไม่ สามารถซิงโคร ไน ซ์ในการวนซ้ำแต่ละครั้งได้ */ // ... } } สุดท้าย{ readLock.unlock (); }

ขบวนรถ

การกีดกันร่วมกัน (Mutual exclusion)มี ปัญหา เรื่องการแย่งชิงล็อก (lock convoy problem) ซึ่งอาจทำให้เธรดจำนวนมากแย่งกันใช้ล็อก ทำให้ JVM ต้องดูแลคิวของผู้รอ (waiters queues) ที่มีค่าใช้จ่ายสูง และต้อง "พัก" java.lang.Threadเธรดที่รออยู่ การพักและยกเลิกการพักเธรดนั้นมีค่าใช้จ่ายสูงjava.lang.Threadและ อาจทำให้เกิด การสลับบริบทที่ ช้า การสลับบริบทใช้เวลาตั้งแต่ไมโครวินาทีถึงมิลลิวินาที ในขณะที่การทำงานพื้นฐานของ map เองโดยปกติใช้เวลานาโนวินาที ประสิทธิภาพอาจลดลงเหลือเพียงเศษเสี้ยวเล็กๆ ของjava.lang.Threadปริมาณงานของเธรดเดียวเมื่อการแย่งชิงเพิ่มขึ้น เมื่อไม่มีการแย่งชิงล็อกหรือมีการแย่งชิงล็อกน้อย ประสิทธิภาพก็จะได้รับผลกระทบน้อย อย่างไรก็ตาม ยกเว้นการทดสอบการแย่งชิงล็อก JVM รุ่นใหม่ๆ จะรวมโค้ดล็อกส่วนใหญ่ไว้ในบรรทัดเดียว ลดเหลือเพียงไม่กี่คำสั่ง ทำให้กรณีที่ไม่มีการแย่งชิงล็อกยังคงทำงานได้เร็วมาก เทคนิคการเรียกซ้ำ (reentrant techniques) เช่น การซิงโครไนซ์แบบเนทีฟ (native synchronization) หรือjava.util.concurrent.locks.ReentrantReadWriteLockอื่นๆ มีภาระเพิ่มเติมที่ลดประสิทธิภาพในการดูแลความลึกของการเรียกซ้ำ ซึ่งส่งผลกระทบต่อกรณีที่ไม่มีการแย่งชิงล็อกเช่นกัน ปัญหาเรื่องขบวนการดูเหมือนจะคลี่คลายลงใน JVM รุ่นใหม่ๆ แต่ปัญหานี้อาจถูกซ่อนไว้ด้วยการสลับบริบทที่ช้า: ในกรณีนี้ ความหน่วงจะเพิ่มขึ้น แต่ปริมาณงานจะยังคงอยู่ในระดับที่ยอมรับได้ สำหรับข้อมูลจำนวนหลายร้อยjava.lang.Threadรายการ เวลาในการสลับบริบท 10 มิลลิวินาที จะทำให้เกิดความหน่วงในระดับวินาที

หลายแกน

โซลูชันการกีดกันร่วมกัน (Mutual exclusion) ไม่สามารถใช้ประโยชน์จากพลังการประมวลผลทั้งหมดของระบบมัลติคอร์ได้ เนื่องจากjava.lang.Threadอนุญาตให้มีเพียงหนึ่งคอร์เท่านั้นที่เข้าถึงjava.util.Map<K, V>โค้ดได้ในแต่ละครั้ง การใช้งานแผนที่แบบขนาน (Concurrent map) ที่จัดทำโดย Java Collections Framework และอื่นๆ บางครั้งใช้ประโยชน์จากมัลติคอร์โดยใช้เทคนิคการเขียนโปรแกรมแบบไร้ล็อก (Lock-freecompareAndSet() programming techniques) เทคนิคแบบไร้ล็อกใช้การดำเนินการเช่น เมธอด intrinsic ที่มีอยู่ในคลาส Java หลายคลาส เพื่อAtomicReferenceทำการอัปเดตแบบมีเงื่อนไขของโครงสร้างภายในของ Map บางส่วนแบบอะตอมิก การดำเนินการcompareAndSet()พื้นฐานนี้ได้รับการเสริมด้วยโค้ดเนทีฟในคลาส JCF ที่สามารถทำการ compareAndSet บนส่วนภายในพิเศษของวัตถุบางอย่างสำหรับอัลกอริทึมบางอย่าง (โดยใช้การเข้าถึงแบบ 'unsafe') เทคนิคเหล่านี้มีความซับซ้อน มักอาศัยกฎของการสื่อสารระหว่างเธรดที่จัดทำโดยตัวแปร volatile ความสัมพันธ์ happens-before และ 'retry loops' แบบไร้ล็อกชนิดพิเศษ (ซึ่งไม่เหมือนกับ spin locks ตรงที่มันจะสร้างความคืบหน้าเสมอ) และcompareAndSet()อาศัยคำสั่งเฉพาะของโปรเซสเซอร์ โค้ด Java ใดๆ ก็สามารถนำcompareAndSet()เมธอดในคลาสที่ทำงานพร้อมกันได้หลายคลาสไปใช้เพื่อวัตถุประสงค์อื่นๆ เพื่อให้ได้การทำงานพร้อมกันแบบไร้ล็อกหรือแม้แต่ไร้การรอคอย ซึ่งให้ความหน่วงแฝงที่จำกัด เทคนิคไร้ล็อกนั้นง่ายในหลายกรณีทั่วไปและกับคอลเลกชันง่ายๆ บางอย่าง เช่น สแต็ก

แผนภาพแสดงให้เห็นว่าการซิงโครไนซ์โดยใช้Collections.synchronizedMap(java.util.Map)การห่อหุ้มแบบปกติHashMap(สีม่วง) อาจไม่ได้มีประสิทธิภาพดีเท่ากับConcurrentHashMap(สีแดง) ส่วนแบบอื่นๆ คือjava.util.concurrent.ConcurrentNavigableMap<K, V>แบบ เรียงลำดับ java.util.concurrent.AirConcurrentMap<K, V>(สีน้ำเงิน) และjava.util.concurrent.ConcurrentSkipListMap<K, V>(CSLM สีเขียว) (จุดที่ราบเรียบอาจเกิดจากการสร้างตารางที่มีขนาดใหญ่กว่าตารางเริ่มต้น และjava.util.concurrent.ConcurrentHashMap<K, V>ใช้พื้นที่มากขึ้น โปรดสังเกตว่าแกน y ควรระบุว่า 'puts K' ระบบคือ i7 8 คอร์ 2.5 GHz พร้อม -Xms5000m เพื่อป้องกันการเก็บขยะ) การเก็บขยะและการขยายกระบวนการของ JVM เปลี่ยนแปลงเส้นโค้งอย่างมาก และเทคนิค lock-Free ภายในบางอย่างสร้างขยะเมื่อเกิดการแย่งชิงทรัพยากร

ตารางแฮชทั้งสองนั้นทำงานได้เร็ว
ตารางแฮชทั้งสองนั้นทำงานได้เร็ว

เฉพาะแผนที่ที่เรียงลำดับไว้เท่านั้นที่จะปรับขนาด ส่วนแผนที่ที่ซิงโครไนซ์ไว้จะปรับขนาดกลับแผนที่ที่ซิงโครไนซ์แล้วได้กลับมามีลักษณะคล้ายกับแผนที่ที่เรียงลำดับตามมาตราส่วนอีกครั้ง

ความหน่วงที่คาดการณ์ได้

ปัญหาอีกประการหนึ่งของวิธีการกีดกันร่วมกันคือ การสมมติว่าโค้ดแบบเธรดเดียวบางส่วนมีการทำงานแบบอะตอมิกอย่างสมบูรณ์ ทำให้เกิดความล่าช้าระหว่างเธรดที่ยาวนานเกินไปในสภาพแวดล้อมแบบขนาน โดยเฉพาะอย่างยิ่ง ตัววนซ้ำและการดำเนินการแบบกลุ่ม เช่นputAll()และอื่นๆ อาจใช้เวลานานตามสัดส่วนของjava.util.Map<K, V>ขนาด ทำให้เธรดอื่นๆ ที่คาดหวังความหน่วงต่ำที่คาดการณ์ได้สำหรับการดำเนินการที่ไม่ใช่แบบกลุ่มเกิดความล่าช้า ตัวอย่างเช่น เว็บเซิร์ฟเวอร์java.lang.Threadแบบมัลติเธรดไม่สามารถอนุญาตให้การตอบสนองบางอย่างล่าช้าเนื่องจากการวนซ้ำที่ใช้เวลานานของเธรดอื่นๆ ที่กำลังดำเนินการคำขออื่นๆ ที่กำลังค้นหาค่าเฉพาะได้ ที่เกี่ยวข้องกับเรื่องนี้คือ เธรดที่ล็อกนั้นไม่มีข้อกำหนดใดๆ ที่จะต้องปล่อยล็อก และลูปอนันต์ในเธรดที่เป็นเจ้าของอาจทำให้เกิดการบล็อกถาวรไปยังเธรดอื่นๆ เธรดที่เป็นเจ้าของที่ทำงานช้าอาจถูกขัดจังหวะได้ แผนที่แฮชก็อาจเกิดความล่าช้าโดยไม่คาดคิดระหว่างการรีแฮชเช่นกัน java.lang.Threadjava.util.Map<K, V>Threadjava.lang.Threadjava.lang.Thread

ความสอดคล้องที่อ่อนแอ

วิธีแก้ปัญหาของ แพ็กjava.util.concurrentเกจสำหรับปัญหาการแก้ไขพร้อมกัน ปัญหาขบวน ปัญหาความหน่วงที่คาดการณ์ได้ และปัญหามัลติคอร์นั้นรวมถึงทางเลือกทางสถาปัตยกรรมที่เรียกว่าความสอดคล้องแบบอ่อน ทางเลือกนี้หมายความว่าการอ่านget(java.lang.Object)จะไม่บล็อกแม้ในขณะที่การอัปเดตกำลังดำเนินการอยู่ และอนุญาตให้การอัปเดตทับซ้อนกันเองและกับการอ่านได้ ความสอดคล้องแบบอ่อนช่วยให้เนื้อหาของjava.util.concurrent.ConcurrentMap<K, V>สามารถเปลี่ยนแปลงได้ในระหว่างการวนซ้ำโดยตัวเดียวjava.lang.Thread[ 7 ] ตัววนซ้ำได้รับการออกแบบให้ใช้งานทีละjava.lang.Threadตัว ดังนั้น ตัวอย่างเช่นMapที่มีสองรายการที่ขึ้นอยู่กันอาจถูกมองในลักษณะที่ไม่สอดคล้องกันโดยผู้อ่านThreadในระหว่างการแก้ไขโดยตัวอื่นjava.lang.Threadการอัปเดตที่ควรจะเปลี่ยนคีย์ของเป็น อะตอ Map.Entry(k1, v)มิMap.Entry(k2, v)กจะต้องทำremove(k1)และจากนั้นput(k2, v)ทำ ในขณะที่การวนซ้ำอาจพลาดรายการหรือเห็นรายการในสองที่ การดึงข้อมูลจะส่งคืนค่าสำหรับคีย์ที่กำหนดซึ่งสะท้อนถึง การอัปเดต ที่เสร็จสมบูรณ์ก่อนหน้านี้สำหรับคีย์นั้น ดังนั้นจึงมีความสัมพันธ์แบบ 'เกิดขึ้นก่อน'

ไม่มีทางที่java.util.concurrent.ConcurrentMap<K, V>s จะล็อกตารางทั้งหมดได้ ไม่มีความเป็นไปได้ที่จะjava.util.ConcurrentModificationExceptionเกิดการแก้ไขพร้อมกันโดยไม่ได้ตั้งใจ เหมือนกับการแก้ไข java.util.Map<K, V>s ที่ไม่พร้อมกัน size()วิธีการนี้อาจใช้เวลานาน ต่างจากjava.util.Map<K, V>s ที่ไม่พร้อมกันและคอลเลกชันอื่นๆ ที่มักจะมีฟิลด์ขนาดสำหรับการเข้าถึงที่รวดเร็ว เนื่องจากอาจต้องสแกนทั้งหมดjava.util.Map<K, V>ในบางวิธี เมื่อมีการแก้ไขพร้อมกันเกิดขึ้น ผลลัพธ์จะสะท้อนสถานะของ s java.util.Map<K, V>ในช่วงเวลาใดเวลาหนึ่ง แต่ไม่จำเป็นต้องเป็นสถานะที่สอดคล้องกันเพียงสถานะเดียว ดังนั้น s จึงsize()อาจเหมาะสมที่สุดสำหรับการตรวจสอบเท่านั้น isEmpty()containsValue(java.lang.Object)

ConcurrentMap1.5 วิธีการ

มีการดำเนินการบางอย่างjava.util.concurrent.ConcurrentMap<K, V>ที่ไม่มีอยู่ในjava.util.Map<K, V>(ซึ่งสืบทอดมาจาก) เพื่อให้การแก้ไขเป็นไปอย่างเป็นอะตอมิกreplace(K, v1, v2)จะตรวจสอบการมีอยู่ของv1ในjava.util.Map.Entry<K, V>ที่ระบุโดยKและเฉพาะเมื่อพบเท่านั้น จึงv1จะแทนที่ ด้วย อย่างเป็นv2อะตอมิก การดำเนินการใหม่replace(k, v)จะทำ ก็put(k, v)ต่อเมื่อkมี อยู่ใน แล้ว เท่านั้น java.util.Map<K, V>นอกจากนี้putIfAbsent(k, v)จะดำเนินการ ก็put(k, v)ต่อเมื่อkยังไม่มีอยู่ในjava.util.Map<K, V>และremove(k, v)จะลบjava.util.Map.Entry<K, V>สำหรับ ก็ต่อvเมื่อvมี อยู่เท่านั้น ความเป็นอะตอมิกนี้อาจมีความสำคัญสำหรับกรณีการใช้งานแบบมัลติเธรดบางกรณี แต่ไม่เกี่ยวข้องกับข้อจำกัดความสอดคล้องที่อ่อนแอ

สำหรับjava.util.concurrent.ConcurrentMap<K, V>s นั้น สิ่งต่อไปนี้เป็นอะตอม

m.putIfAbsent(k, v)เป็นอะตอม แต่เทียบเท่ากับ:

ถ้า( k == null หรือv == null ) ให้โยนข้อ ยกเว้น NullPointerException ออกมาถ้า( ! m . containsKey ( k )) ให้คืนค่าm . put ( k , v ) มิเช่นนั้นให้คืนค่าm . get ( k )

m.replace(k, v) เป็นคำสั่งอะตอมิก แต่เทียบเท่ากับ:

ถ้า( k == null หรือv == null ) ให้โยนข้อ ยกเว้น NullPointerException ออกมาถ้า( m.containsKey ( k ) ) ให้คืนค่าm.put ( k , v ) มิเช่นนั้นให้คืนค่าnull

m.replace(k, v1, v2) เป็นคำสั่งอะตอมิก แต่เทียบเท่ากับ:

ถ้า( k == null || v1 == null || v2 == null ) { throw new NullPointerException (); }ถ้า( m.containsKey ( k ) && Objects.equals ( m.get ( k ) , v1 ) ) { m.put ( k , v2 ) ; return true ; } else return false ; }

m.remove(k, v) เป็นคำสั่งอะตอมิก แต่เทียบเท่ากับ:

// หาก Map ไม่รองรับคีย์หรือค่าที่เป็นค่าว่าง (โดยแยกจากกันอย่างชัดเจน) หาก( k == null หรือv == null ) { throw new NullPointerException (); }ถ้า( m.containsKey ( k ) && Objects.equals ( m.get ( k ) , v ) ) { m.remove ( k ) ; return true ; } else return false ; }

ConcurrentMap1.8 วิธีการ

เนื่องจาก `interface` java.util.Map<K, V>และ `interface` เป็นอินเทอร์เฟซ จึงjava.util.concurrent.ConcurrentMap<K, V>ไม่สามารถเพิ่มเมธอดใหม่เข้าไปได้โดยไม่ทำให้การใช้งานเดิมเสียหาย อย่างไรก็ตาม Java 1.8 ได้เพิ่มความสามารถในการใช้งานอินเทอร์เฟซแบบเริ่มต้น (default interface implementations) และได้เพิ่มMapการใช้งานแบบเริ่มต้นของเมธอดใหม่บางส่วนgetOrDefault(Object, V)เช่นforEach(BiConsumer)` replaceAll(BiFunction)initialize`, ... และ`initialize` ลงในอินเทอร์เฟซ การใช้งานแบบเริ่มต้นใน `interface` ไม่รับประกันความเป็นอะตอมิก (atomicity) แต่ในการเขียน ทับ แบบเริ่มต้น (overriding defaults) จะใช้ เทคนิค แบบไร้ล็อก (lock-free techniques) เพื่อให้ได้ความเป็นอะตอมิก และการใช้งานที่มีอยู่แล้วจะเป็นอะตอมิกโดยอัตโนมัติ เทคนิคแบบไร้ล็อกอาจช้ากว่าการเขียนทับในคลาสที่เป็นรูปธรรม ดังนั้นคลาสที่เป็นรูปธรรมอาจเลือกที่จะใช้งานแบบอะตอมิกหรือไม่ก็ได้ และบันทึกคุณสมบัติการทำงานพร้อมกัน (concurrency properties) ไว้ computeIfAbsent(K, Function)computeIfPresent(K, BiFunction)compute(K,BiFunction)merge(K, V, BiFunction)java.util.Map<K, V>java.util.concurrent.ConcurrentMap<K, V>java.util.concurrent.ConcurrentMap<K, V>

ความเป็นอะตอมแบบไม่ต้องล็อก

เป็นไปได้ที่จะใช้เทคนิคแบบไร้การล็อกjava.util.concurrent.ConcurrentMap<K, V> กับ s เนื่องจาก s มีเมธอดที่มีจำนวนฉันทามติสูงเพียงพอ นั่นคืออนันต์ซึ่งหมายความว่าสามารถประสานงาน s จำนวนเท่าใดก็ได้java.lang.Threadตัวอย่างนี้สามารถนำไปใช้กับ Java 8 ได้merge()แต่แสดงให้เห็นถึงรูปแบบไร้การล็อกโดยรวม ซึ่งมีความเป็นทั่วไปมากกว่า ตัวอย่างนี้ไม่เกี่ยวข้องกับกลไกภายในของ s java.util.concurrent.ConcurrentMap<K, V>แต่เกี่ยวข้องกับการใช้งาน s ในโค้ดฝั่งไคลเอ็นต์java.util.concurrent.ConcurrentMap<K, V>ตัวอย่างเช่น หากเราต้องการคูณค่าใน Map ด้วยค่าคงที่แบบNอะตอมิก:

นำเข้าjava.util.concurrent.ConcurrentMap ;static final long N = 10 ;void atomicMultiply ( ConcurrentMap < Long , Long > map , long key ) { while ( true ) { Long oldValue = map . get ( key ); // สมมติว่า oldValue ไม่เป็นค่าว่าง นี่คือการดำเนินการ 'payload' และไม่ควรมีผลข้างเคียงเนื่องจากการคำนวณซ้ำที่อาจเกิดขึ้นเมื่อเกิดข้อขัดแย้งLong newValue = oldValue * N ; if ( map . replace ( key , oldValue , newValue )) { break ; } } }

วิธีนี้putIfAbsent(k, v)ยังมีประโยชน์เมื่ออนุญาตให้เว้นช่องสำหรับคีย์ไว้ว่างเปล่าได้ ตัวอย่างนี้สามารถนำไปใช้กับ Java 8 ได้compute()แต่แสดงให้เห็นถึงรูปแบบ lock-free โดยรวม ซึ่งมีความเป็นทั่วไปมากกว่า ฟังก์ชัน replace(k, v1, v2)นี้ไม่ยอมรับพารามิเตอร์ที่เป็นค่าว่าง ดังนั้นบางครั้งจึงจำเป็นต้องใช้พารามิเตอร์หลายตัวรวมกัน กล่าวคือ ถ้าv1พารามิเตอร์เป็นค่าว่าง ฟังก์ชันputIfAbsent(k, v2)จะถูกเรียกใช้ มิฉะนั้น ฟังก์ชันreplace(k, v1, v2)จะถูกเรียกใช้

นำเข้าjava.util.concurrent.ConcurrentMap ;void atomicMultiplyNullable ( ConcurrentMap < Long , Long > map , long key ) { while ( true ) { long oldValue = map . get ( key ); // นี่คือการดำเนินการ 'payload' และไม่ควรมีผลข้างเคียงเนื่องจากการคำนวณใหม่ที่อาจเกิดขึ้นเมื่อเกิดข้อขัดแย้งlong newValue = oldValue == null ? INITIAL_VALUE : oldValue * N ; if ( replaceNullable ( map , key , oldValue , newValue )) { break ; } } }static boolean replaceNullable ( ConcurrentMap < Long , Long > map , long key , long v1 , long v2 ) { return v1 == null ? map . putIfAbsent ( key , v2 ) == null : map . replace ( key , v1 , v2 ); }

ประวัติศาสตร์

เฟรมเวิร์กคอลเลกชันของ Java ได้รับการออกแบบและพัฒนาโดยJoshua Bloch เป็นหลัก และเปิดตัวในJDK 1.2 [ 8 ] คลาสการทำงานพร้อมกันดั้งเดิมมาจากแพ็กเกจคอลเลกชัน ของDoug Lea [ 9 ]

ดูเพิ่มเติม

การอ้างอิง

  1. ^ a b Goetz et al. 2006 , หน้า 84–85, §5.2 การรวบรวมพร้อมกัน
  2. ^ Goetz et al. 2006 , หน้า 85–86, §5.2.1 ConcurrentHashMap.
  3. ^ Goetz et al. 2006 , หน้า 82–83, §5.1.2 ตัววนซ้ำและ ConcurrentModificationException
  4. ^ Goetz et al. 2006 , หน้า 84–85, §5.2.1 ConcurrentHashMap.
  5. ^ "java.util.Collections.synchronizedMap" . Java / Java SE / 11 / API / java.base. ศูนย์ช่วยเหลือของ Oracle . 19 กันยายน 2018 . สืบค้นเมื่อ 17 กรกฎาคม 2020 .
  6. ^ Goetz et al. 2006 , หน้า 95–98, §13.5 ล็อกแบบอ่าน-เขียน
  7. ^ Goetz et al. 2006 , หน้า 85–86, §5.21 ConcurrentHashMap.
  8. ^ Vanhelsuwé, Laurence (1 มกราคม 1999). "การต่อสู้ของเฟรมเวิร์กคอนเทนเนอร์: คุณควรใช้อันไหน?" . JavaWorld . สืบค้นเมื่อ17 กรกฎาคม 2020 .
  9. ^ Lea, Doug . "ภาพรวมของแพ็คเกจ util.concurrent เวอร์ชัน 1.3.4" . สืบค้นเมื่อ2011-01-01 .
  • บทเรียนการสะสม
  • บทช่วยสอนคอลเลกชัน Java 6 — โดย Jakob Jenkov, Kadafi Kamphulusa
  • การฝึกเสือ: กรอบงานการรวบรวมข้อมูล
  • 'เฟรมเวิร์กการจัดการคอลเลกชัน' (เอกสารประกอบ Oracle Java SE 8)
  • 'บทเรียน Java - ชุดรวม' โดย Josh Bloch
  • ควรใช้ Java Collection แบบใด? — แผนผังขั้นตอนง่ายๆ เพื่อช่วยให้การเลือก Collection ง่ายขึ้น
  • 'ควรใช้ Java Collection ตัวไหนดี?' — โดย Janeve George
ดึงข้อมูลมาจาก " https://en.wikipedia.org/w/index.php?title=Java_ConcurrentMap&oldid=1358033925 "

สรุปเนื้อหา

ข้อมูลสำคัญจากบทความ

ข้อมูลสำคัญเกี่ยวกับ Java ConcurrentMap

Java Collections Framework เวอร์ชัน 1.5 และเวอร์ชันต่อมา ของ ภาษาการเขียนโปรแกรม Java กำหนดและใช้งานแผนที่แบบเธรดเดียวปกติแบบดั้งเดิม...

อินเทอร์เฟซแผนที่ Java

แผนภาพอินเทอร์เฟ ซเวอร์ชัน 1.8 java.util.Map มีรูปร่างดังด้านล่าง เซตสามารถพิจารณาได้ว่าเป็นกรณีย่อยของแผนที่ที่สอดคล้องกัน โดยที่ค่าต่างๆ จะเป็นค่าคงที่เฉพาะที่สามารถละเลยได้ แม้ว่า java.util.

ConcurrentHashMap

สำหรับการเข้าถึงแบบไม่เรียงลำดับตามที่กำหนดไว้ใน java.util.Map อินเทอร์เฟซ java.util.concurrent.ConcurrentHashMap จะใช้การใช้งาน java.util.concurrent.

ConcurrentSkipListMap

สำหรับการเข้าถึงตามลำดับที่กำหนดโดย java.util.NavigableMap อินเทอร์เฟซ java.util.concurrent.ConcurrentSkipListMap ได้ถูกเพิ่มใน Java 1.6 [ 1 ] และใช้งาน java.util.concurrent.ConcurrentMap และเช่นกัน java.util.concurrent.