ชุดเธรดปลอดภัยประเภทต่างๆใน Java


138

ดูเหมือนว่าจะมีการใช้งานและวิธีการสร้างชุดเธรดที่ปลอดภัยใน Java ที่แตกต่างกันมากมาย ตัวอย่างบางส่วน ได้แก่

1) CopyOnWriteArraySet

2) Collections.synchronizedSet (ชุดเซ็ต)

3) ConcurrentSkipListSet

4) Collections.newSetFromMap (ใหม่ ConcurrentHashMap ())

5) ชุดอื่น ๆ ที่สร้างขึ้นในลักษณะคล้ายกับ (4)

ตัวอย่างเหล่านี้มาจากรูปแบบพร้อมกัน: การใช้งานชุดพร้อมกันใน Java 6

ใครช่วยอธิบายความแตกต่างข้อดีข้อเสียของตัวอย่างเหล่านี้และอื่น ๆ ได้ไหม ฉันมีปัญหาในการทำความเข้าใจและรักษาทุกอย่างให้ตรงจาก Java Std Docs

คำตอบ:


206

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

ใช้สิ่งนี้สำหรับชุดเล็ก ๆ เท่านั้นซึ่งจะอ่าน (ซ้ำ) บ่อยครั้งและเปลี่ยนแทบไม่ (ชุดฟัง Swings น่าจะเป็นตัวอย่าง แต่สิ่งเหล่านี้ไม่ใช่ชุดจริงๆและควรใช้จาก EDT เท่านั้น)

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

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

3) ConcurrentSkipListSetเป็นการSortedSetใช้งานพร้อมกันโดยมีการดำเนินการขั้นพื้นฐานที่สุดใน O (log n) ช่วยให้สามารถเพิ่ม / ลบและอ่าน / ทำซ้ำได้พร้อมกันโดยที่การทำซ้ำอาจบอกหรือไม่บอกเกี่ยวกับการเปลี่ยนแปลงตั้งแต่สร้างตัววนซ้ำ การดำเนินการจำนวนมากเป็นเพียงการโทรหลายครั้งและไม่ใช่แบบอะตอมเธรดอื่น ๆ อาจสังเกตเห็นเพียงบางส่วนเท่านั้น

เห็นได้ชัดว่าคุณสามารถใช้สิ่งนี้ได้ก็ต่อเมื่อคุณมีลำดับทั้งหมดในองค์ประกอบของคุณ สิ่งนี้ดูเหมือนเป็นตัวเลือกที่เหมาะสำหรับสถานการณ์ที่มีความพร้อมกันสูงสำหรับชุดที่ไม่ใหญ่เกินไป (เนื่องจาก O (log n))

4) สำหรับConcurrentHashMap(และชุดที่ได้มาจากมัน): ตัวเลือกพื้นฐานส่วนใหญ่คือ (โดยเฉลี่ยถ้าคุณมีดีและเร็วhashCode()) ใน O (1) (แต่อาจลดลงเป็น O (n)) เช่น HashMap / แฮชเซ็ต การเขียนพร้อมกันมี จำกัด (ตารางถูกแบ่งพาร์ติชันและการเข้าถึงการเขียนจะถูกซิงโครไนซ์บนพาร์ติชันที่ต้องการ) ในขณะที่การเข้าถึงการอ่านจะทำงานพร้อมกันอย่างสมบูรณ์กับตัวมันเองและเธรดการเขียน (แต่อาจยังไม่เห็นผลลัพธ์ของการเปลี่ยนแปลงที่กำลังดำเนินอยู่ เขียน). ตัววนซ้ำอาจหรือไม่เห็นการเปลี่ยนแปลงตั้งแต่ถูกสร้างขึ้นและการดำเนินการจำนวนมากไม่ใช่ปรมาณู การปรับขนาดทำได้ช้า (สำหรับ HashMap / HashSet) ดังนั้นพยายามหลีกเลี่ยงปัญหานี้โดยการประมาณขนาดที่ต้องการในการสร้าง (และใช้อีกประมาณ 1/3 เนื่องจากจะปรับขนาดเมื่อ 3/4 เต็ม)

ใช้สิ่งนี้เมื่อคุณมีชุดใหญ่ฟังก์ชั่นแฮชที่ดี (และเร็ว) และสามารถประมาณขนาดชุดและการทำงานพร้อมกันที่จำเป็นก่อนสร้างแผนที่

5) มีการใช้งานแผนที่พร้อมกันอื่น ๆ ที่สามารถใช้ได้หรือไม่?


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

บนConcurrentHashMapชุดตาม "จึงพยายามที่จะหลีกเลี่ยงปัญหานี้โดยการประมาณขนาดที่จำเป็นในการสร้าง." ขนาดที่คุณให้กับแผนที่ควรใหญ่กว่าค่าประมาณของคุณ (หรือค่าที่ทราบ) มากกว่า 33% เนื่องจากชุดจะปรับขนาดเมื่อโหลด 75% ฉันใช้expectedSize + 4 / 3 + 1
Daren

@Daren ฉันเดาว่าคนแรก+น่าจะเป็น*?
Paŭlo Ebermann

@ PaŭloEbermannแน่นอน ... มันควรจะเป็นexpectedSize * 4 / 3 + 1
Daren

1
สำหรับConcurrentMap(หรือHashMap) ใน Java 8 หากจำนวนรายการที่แมปกับที่เก็บข้อมูลเดียวกันถึงค่าขีด จำกัด (ฉันเชื่อว่าเป็น 16) รายการจะเปลี่ยนเป็นต้นไม้ค้นหาแบบไบนารี (ต้นไม้สีแดง - ดำจะถูกกำหนดไว้) และในกรณีนั้นให้ค้นหา เวลาจะเป็นและไม่ได้O(lg n) O(n)
akhil_mittal

21

เป็นไปได้ที่จะรวมcontains()ประสิทธิภาพของHashSetกับคุณสมบัติที่เกี่ยวข้องกับการทำงานพร้อมกันCopyOnWriteArraySetโดยใช้AtomicReference<Set>และแทนที่ทั้งชุดในการแก้ไขแต่ละครั้ง

ร่างการใช้งาน:

public abstract class CopyOnWriteSet<E> implements Set<E> {

    private final AtomicReference<Set<E>> ref;

    protected CopyOnWriteSet( Collection<? extends E> c ) {
        ref = new AtomicReference<Set<E>>( new HashSet<E>( c ) );
    }

    @Override
    public boolean contains( Object o ) {
        return ref.get().contains( o );
    }

    @Override
    public boolean add( E e ) {
        while ( true ) {
            Set<E> current = ref.get();
            if ( current.contains( e ) ) {
                return false;
            }
            Set<E> modified = new HashSet<E>( current );
            modified.add( e );
            if ( ref.compareAndSet( current, modified ) ) {
                return true;
            }
        }
    }

    @Override
    public boolean remove( Object o ) {
        while ( true ) {
            Set<E> current = ref.get();
            if ( !current.contains( o ) ) {
                return false;
            }
            Set<E> modified = new HashSet<E>( current );
            modified.remove( o );
            if ( ref.compareAndSet( current, modified ) ) {
                return true;
            }
        }
    }

}

จริงๆแล้วAtomicReferenceทำเครื่องหมายว่าค่ามีความผันผวน หมายความว่าจะทำให้แน่ใจว่าไม่มีเธรดอ่านข้อมูลเก่าและให้happens-beforeการรับประกันเนื่องจากคอมไพลเลอร์ไม่สามารถจัดลำดับโค้ดได้ แต่ถ้าใช้เฉพาะวิธี get / set AtomicReferenceเราก็กำลังทำเครื่องหมายตัวแปรของเราในรูปแบบแฟนซี
akhil_mittal

คำตอบนี้ไม่สามารถเพิ่มคะแนนได้เพียงพอเนื่องจาก (1) เว้นแต่ฉันจะพลาดบางสิ่งบางอย่างมันจะใช้ได้กับคอลเลกชันทุกประเภท (2) ไม่มีคลาสอื่น ๆ ที่ให้วิธีการอัปเดตคอลเลกชันทั้งหมดในครั้งเดียว ... สิ่งนี้มีประโยชน์มาก .
Gili

ฉันลองใช้คำต่อคำนี้ แต่พบว่ามีป้ายกำกับabstractดูเหมือนว่าจะหลีกเลี่ยงไม่ต้องเขียนหลายวิธี ฉันจะตั้งค่าเกี่ยวกับการเพิ่มพวกเขา iterator()แต่วิ่งเข้าไปในสิ่งกีดขวางบนถนนด้วย ฉันไม่รู้วิธีรักษาตัววนซ้ำในสิ่งนี้โดยไม่ทำลายโมเดล ดูเหมือนว่าฉันมักจะต้องผ่านrefและอาจได้รับชุดพื้นฐานที่แตกต่างกันในแต่ละครั้งซึ่งต้องได้รับตัวทำซ้ำใหม่ในชุดพื้นฐานซึ่งไม่มีประโยชน์สำหรับฉันเพราะมันจะเริ่มต้นด้วยรายการศูนย์ ข้อมูลเชิงลึกใด ๆ
nclark

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

เป็นwhile ( true )สิ่งที่จำเป็นที่นี่?
user3908406

11

หาก Javadocs ไม่สามารถช่วยได้คุณควรหาหนังสือหรือบทความเพื่ออ่านเกี่ยวกับโครงสร้างข้อมูล สรุป:

  • CopyOnWriteArraySet สร้างสำเนาใหม่ของอาร์เรย์พื้นฐานทุกครั้งที่คุณกลายพันธุ์คอลเลกชันดังนั้นการเขียนจึงช้าและ Iterators จะเร็วและสม่ำเสมอ
  • Collections.synchronizedSet () ใช้การเรียกวิธีการซิงโครไนซ์แบบ old-school เพื่อสร้างชุดเธรดที่ปลอดภัย นี่จะเป็นเวอร์ชันที่มีประสิทธิภาพต่ำ
  • ConcurrentSkipListSet เสนอการเขียนที่มีประสิทธิภาพด้วยการดำเนินการแบตช์ที่ไม่สอดคล้องกัน (addAll, removeAll ฯลฯ ) และ Iterators
  • Collections.newSetFromMap (ConcurrentHashMap ใหม่ ()) มีความหมายของ ConcurrentHashMap ซึ่งฉันเชื่อว่าไม่จำเป็นต้องได้รับการปรับให้เหมาะสมสำหรับการอ่านหรือเขียน แต่เช่นเดียวกับ ConcurrentSkipListSet มีการดำเนินการแบตช์ที่ไม่สอดคล้องกัน

1
developer.com/java/article.php/10922_3829891_2/… <ดียิ่งกว่าหนังสือ)
ycomp

1

ชุดการอ้างอิงที่อ่อนแอพร้อมกัน

บิดอีกเป็นชุดด้ายปลอดภัยของข้อมูลอ้างอิงที่อ่อนแอ

ชุดดังกล่าวมีประโยชน์สำหรับการติดตามผู้ติดตามในสถานการณ์pub-sub เมื่อสมาชิกออกนอกขอบเขตในที่อื่นดังนั้นจึงมุ่งหน้าไปสู่การเป็นผู้สมัครรับการเก็บขยะผู้สมัครสมาชิกไม่จำเป็นต้องกังวลกับการยกเลิกการสมัครสมาชิกอย่างสง่างาม การอ้างอิงที่ไม่เหมาะสมช่วยให้สมาชิกสามารถเปลี่ยนไปเป็นผู้สมัครรับการเก็บขยะได้ เมื่อเก็บขยะในที่สุดรายการในชุดจะถูกลบออก

แม้ว่าจะไม่มีชุดดังกล่าวมาให้โดยตรงกับคลาสที่รวมไว้ แต่คุณสามารถสร้างได้โดยใช้การโทรเพียงไม่กี่ครั้ง

อันดับแรกเราเริ่มต้นด้วยการสร้างการSetอ้างอิงที่อ่อนแอโดยใช้ประโยชน์จากWeakHashMapชั้นเรียน สิ่งนี้แสดงในเอกสารประกอบการเรียนสำหรับCollections.newSetFromMap.

Set< YourClassGoesHere > weakHashSet = 
    Collections
    .newSetFromMap(
        new WeakHashMap< YourClassGoesHere , Boolean >()
    )
;

ราคาของแผนที่Booleanเป็นที่ไม่เกี่ยวข้องที่นี่เป็นกุญแจสำคัญSetของแผนที่จะทำให้เรา

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

ก้าวไปอีกขั้นด้วยการห่อเป็นชุดที่ซิงโครไนซ์เพื่อให้ชุดนี้ปลอดภัย Collections.synchronizedSetป้อนเข้าสู่การเรียกไปยัง

this.subscribers =
        Collections.synchronizedSet(
                Collections.newSetFromMap(
                        new WeakHashMap <>()  // Parameterized types `< YourClassGoesHere , Boolean >` are inferred, no need to specify.
                )
        );

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

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