การทำซ้ำ ConcurrentHashMap ทำให้เธรดปลอดภัยหรือไม่


156

ใน javadoc สำหรับConcurrentHashMapมีดังต่อไปนี้:

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

มันหมายความว่าอะไร? จะเกิดอะไรขึ้นถ้าฉันพยายามทำซ้ำแผนที่ด้วยสองเธรดในเวลาเดียวกัน จะเกิดอะไรขึ้นถ้าฉันใส่หรือลบค่าออกจากแผนที่ในขณะที่วนซ้ำ

คำตอบ:


193

มันหมายความว่าอะไร?

นั่นหมายความว่าตัววนซ้ำแต่ละตัวที่คุณได้รับจาก a ConcurrentHashMapถูกออกแบบมาให้ใช้งานโดยเธรดเดี่ยวและไม่ควรผ่านไปมา ซึ่งรวมถึงน้ำตาลซินแทคติกที่แต่ละวงให้

จะเกิดอะไรขึ้นถ้าฉันพยายามทำซ้ำแผนที่ด้วยสองเธรดในเวลาเดียวกัน

มันจะทำงานได้ตามที่คาดไว้ถ้าแต่ละเธรดใช้ตัววนซ้ำของมันเอง

จะเกิดอะไรขึ้นถ้าฉันใส่หรือลบค่าออกจากแผนที่ในขณะที่วนซ้ำ

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

โดยสรุปแล้วคำสั่งเช่น

for (Object o : someConcurrentHashMap.entrySet()) {
    // ...
}

จะดี (หรืออย่างน้อยปลอดภัย) เกือบทุกครั้งที่คุณเห็น


ดังนั้นจะเกิดอะไรขึ้นหากในระหว่างการทำซ้ำเธรดอื่นจะลบวัตถุ o10 ออกจากแผนที่ ฉันยังสามารถเห็น o10 ในการทำซ้ำได้แม้ว่าจะถูกลบไปแล้ว? @Waldheinz
อเล็กซ์

ตามที่ระบุไว้ข้างต้นมันไม่ได้ระบุจริงๆหากตัววนซ้ำที่มีอยู่จะสะท้อนการเปลี่ยนแปลงในแผนที่ในภายหลัง ดังนั้นฉันไม่ทราบและตามข้อกำหนดไม่มีใครทำ (โดยไม่ต้องดูรหัสและที่อาจเปลี่ยนแปลงได้กับทุกการปรับปรุงของ runtime) ดังนั้นคุณไม่สามารถวางใจได้
Waldheinz

8
แต่ฉันยังมีอีกConcurrentModificationExceptionสักConcurrentHashMapพักทำไม?
Kimi Chiu

@KimiChiu คุณน่าจะโพสต์คำถามใหม่โดยระบุรหัสที่ทำให้เกิดข้อยกเว้น แต่ฉันสงสัยอย่างมากว่ามันเกิดขึ้นโดยตรงจากการวนซ้ำของคอนเทนเนอร์ที่เกิดขึ้นพร้อมกัน เว้นแต่ว่าการนำ Java ไปใช้จะเป็นบั๊กซี
Waldheinz

18

คุณสามารถใช้คลาสนี้เพื่อทดสอบเธรดการเข้าถึงสองเธรดและหนึ่งการกลายพันธุ์อินสแตนซ์ที่แชร์ของConcurrentHashMap:

import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ConcurrentMapIteration
{
  private final Map<String, String> map = new ConcurrentHashMap<String, String>();

  private final static int MAP_SIZE = 100000;

  public static void main(String[] args)
  {
    new ConcurrentMapIteration().run();
  }

  public ConcurrentMapIteration()
  {
    for (int i = 0; i < MAP_SIZE; i++)
    {
      map.put("key" + i, UUID.randomUUID().toString());
    }
  }

  private final ExecutorService executor = Executors.newCachedThreadPool();

  private final class Accessor implements Runnable
  {
    private final Map<String, String> map;

    public Accessor(Map<String, String> map)
    {
      this.map = map;
    }

    @Override
    public void run()
    {
      for (Map.Entry<String, String> entry : this.map.entrySet())
      {
        System.out.println(
            Thread.currentThread().getName() + " - [" + entry.getKey() + ", " + entry.getValue() + ']'
        );
      }
    }
  }

  private final class Mutator implements Runnable
  {

    private final Map<String, String> map;
    private final Random random = new Random();

    public Mutator(Map<String, String> map)
    {
      this.map = map;
    }

    @Override
    public void run()
    {
      for (int i = 0; i < 100; i++)
      {
        this.map.remove("key" + random.nextInt(MAP_SIZE));
        this.map.put("key" + random.nextInt(MAP_SIZE), UUID.randomUUID().toString());
        System.out.println(Thread.currentThread().getName() + ": " + i);
      }
    }
  }

  private void run()
  {
    Accessor a1 = new Accessor(this.map);
    Accessor a2 = new Accessor(this.map);
    Mutator m = new Mutator(this.map);

    executor.execute(a1);
    executor.execute(m);
    executor.execute(a2);
  }
}

จะไม่มีข้อยกเว้นจะถูกโยน

การแชร์ตัววนซ้ำเดียวกันระหว่างเธรดการเข้าถึงสามารถนำไปสู่การหยุดชะงัก:

import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ConcurrentMapIteration
{
  private final Map<String, String> map = new ConcurrentHashMap<String, String>();
  private final Iterator<Map.Entry<String, String>> iterator;

  private final static int MAP_SIZE = 100000;

  public static void main(String[] args)
  {
    new ConcurrentMapIteration().run();
  }

  public ConcurrentMapIteration()
  {
    for (int i = 0; i < MAP_SIZE; i++)
    {
      map.put("key" + i, UUID.randomUUID().toString());
    }
    this.iterator = this.map.entrySet().iterator();
  }

  private final ExecutorService executor = Executors.newCachedThreadPool();

  private final class Accessor implements Runnable
  {
    private final Iterator<Map.Entry<String, String>> iterator;

    public Accessor(Iterator<Map.Entry<String, String>> iterator)
    {
      this.iterator = iterator;
    }

    @Override
    public void run()
    {
      while(iterator.hasNext()) {
        Map.Entry<String, String> entry = iterator.next();
        try
        {
          String st = Thread.currentThread().getName() + " - [" + entry.getKey() + ", " + entry.getValue() + ']';
        } catch (Exception e)
        {
          e.printStackTrace();
        }

      }
    }
  }

  private final class Mutator implements Runnable
  {

    private final Map<String, String> map;
    private final Random random = new Random();

    public Mutator(Map<String, String> map)
    {
      this.map = map;
    }

    @Override
    public void run()
    {
      for (int i = 0; i < 100; i++)
      {
        this.map.remove("key" + random.nextInt(MAP_SIZE));
        this.map.put("key" + random.nextInt(MAP_SIZE), UUID.randomUUID().toString());
      }
    }
  }

  private void run()
  {
    Accessor a1 = new Accessor(this.iterator);
    Accessor a2 = new Accessor(this.iterator);
    Mutator m = new Mutator(this.map);

    executor.execute(a1);
    executor.execute(m);
    executor.execute(a2);
  }
}

ทันทีที่คุณเริ่มแบ่งปันสิ่งเดียวกันIterator<Map.Entry<String, String>>ระหว่าง accessor และ mutator threads java.lang.IllegalStateExceptions จะเริ่มต้นขึ้น

import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ConcurrentMapIteration
{
  private final Map<String, String> map = new ConcurrentHashMap<String, String>();
  private final Iterator<Map.Entry<String, String>> iterator;

  private final static int MAP_SIZE = 100000;

  public static void main(String[] args)
  {
    new ConcurrentMapIteration().run();
  }

  public ConcurrentMapIteration()
  {
    for (int i = 0; i < MAP_SIZE; i++)
    {
      map.put("key" + i, UUID.randomUUID().toString());
    }
    this.iterator = this.map.entrySet().iterator();
  }

  private final ExecutorService executor = Executors.newCachedThreadPool();

  private final class Accessor implements Runnable
  {
    private final Iterator<Map.Entry<String, String>> iterator;

    public Accessor(Iterator<Map.Entry<String, String>> iterator)
    {
      this.iterator = iterator;
    }

    @Override
    public void run()
    {
      while (iterator.hasNext())
      {
        Map.Entry<String, String> entry = iterator.next();
        try
        {
          String st =
              Thread.currentThread().getName() + " - [" + entry.getKey() + ", " + entry.getValue() + ']';
        } catch (Exception e)
        {
          e.printStackTrace();
        }

      }
    }
  }

  private final class Mutator implements Runnable
  {

    private final Random random = new Random();

    private final Iterator<Map.Entry<String, String>> iterator;

    private final Map<String, String> map;

    public Mutator(Map<String, String> map, Iterator<Map.Entry<String, String>> iterator)
    {
      this.map = map;
      this.iterator = iterator;
    }

    @Override
    public void run()
    {
      while (iterator.hasNext())
      {
        try
        {
          iterator.remove();
          this.map.put("key" + random.nextInt(MAP_SIZE), UUID.randomUUID().toString());
        } catch (Exception ex)
        {
          ex.printStackTrace();
        }
      }

    }
  }

  private void run()
  {
    Accessor a1 = new Accessor(this.iterator);
    Accessor a2 = new Accessor(this.iterator);
    Mutator m = new Mutator(map, this.iterator);

    executor.execute(a1);
    executor.execute(m);
    executor.execute(a2);
  }
}

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

12

หมายความว่าคุณไม่ควรแชร์วัตถุตัววนซ้ำในหลายเธรด การสร้างตัววนซ้ำหลายตัวและการใช้ตัววนซ้ำพร้อมกันในเธรดแยกต่างหากนั้นใช้ได้


เหตุผลใดที่คุณไม่ได้ใช้ประโยชน์จาก I ใน Iterator? เนื่องจากเป็นชื่อของคลาสจึงอาจทำให้สับสนน้อยลง
Bill Michell

1
@ บิลมิเชลตอนนี้เราอยู่ในความหมายของการโพสต์มารยาท ฉันคิดว่าเขาควรทำให้ Iterator เชื่อมโยงกลับไปที่ javadoc สำหรับ Iterator หรืออย่างน้อยที่สุดก็วางไว้ในคำอธิบายประกอบแบบอินไลน์โค้ด (`)
ทิมเบนเดอร์

10

นี่อาจทำให้คุณมีความเข้าใจที่ดี

ConcurrentHashMap ให้การทำงานพร้อมกันที่สูงขึ้นโดยการผ่อนคลายสัญญาเล็กน้อยที่ทำให้ผู้โทร การดำเนินการดึงข้อมูลจะส่งกลับค่าที่แทรกโดยการดำเนินการแทรกล่าสุดที่เสร็จสิ้นแล้วและอาจส่งคืนค่าที่เพิ่มโดยการดำเนินการแทรกที่กำลังดำเนินการพร้อมกัน (แต่ไม่ว่าในกรณีใดจะส่งคืนผลลัพธ์ไร้สาระ) Iterators ที่ส่งคืนโดย ConcurrentHashMap.iterator () จะคืนค่าองค์ประกอบแต่ละครั้งมากที่สุดและจะไม่โยน ConcurrentModificationException แต่อาจหรือไม่สะท้อนการแทรกหรือการลบที่เกิดขึ้นตั้งแต่ iterator ถูกสร้างขึ้น. ไม่จำเป็นต้องมีการล็อกทั้งโต๊ะ (หรือเป็นไปได้) เพื่อสร้างความปลอดภัยให้กับเธรดเมื่อวนซ้ำคอลเลกชัน ConcurrentHashMap อาจใช้แทนการซิงโครไนซ์แมปหรือ Hashtable ในแอปพลิเคชันใด ๆ ที่ไม่ต้องอาศัยความสามารถในการล็อคทั้งตารางเพื่อป้องกันการอัพเดท

เกี่ยวกับเรื่องนี้:

อย่างไรก็ตามตัววนซ้ำได้รับการออกแบบให้ใช้งานได้ครั้งละหนึ่งเธรดเท่านั้น

หมายความว่าในขณะที่ใช้ตัววนซ้ำที่ผลิตโดย ConcurrentHashMap ในสองเธรดนั้นปลอดภัย แต่อาจทำให้เกิดผลลัพธ์ที่ไม่คาดคิดในแอปพลิเคชัน


4

มันหมายความว่าอะไร?

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

จะเกิดอะไรขึ้นถ้าฉันพยายามทำซ้ำแผนที่ด้วยสองเธรดในเวลาเดียวกัน

ยังไม่ชัดเจนว่าจะเกิดอะไรขึ้นถ้าคุณทำผิดกฎนี้ คุณสามารถทำให้เกิดความสับสนในลักษณะเดียวกับที่คุณทำถ้า (เช่น) สองเธรดพยายามอ่านจากอินพุตมาตรฐานโดยไม่ซิงโครไนซ์ คุณอาจได้รับพฤติกรรมที่ไม่ปลอดภัยต่อเธรด

แต่ถ้าทั้งสองกระทู้ใช้ตัววนซ้ำที่แตกต่างกันคุณก็น่าจะดี

จะเกิดอะไรขึ้นถ้าฉันใส่หรือลบค่าออกจากแผนที่ในขณะที่วนซ้ำ

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

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