Apakah pengulangan thread ConcurrentHashMap values ​​aman?

156

Di javadoc untuk ConcurrentHashMap adalah sebagai berikut:

Pengambilan operasi (termasuk get) umumnya tidak memblokir, jadi mungkin tumpang tindih dengan operasi pembaruan (termasuk menempatkan dan menghapus). Pengambilan mencerminkan hasil dari operasi pembaruan yang paling baru selesai diadakan saat onsetnya. Untuk operasi agregat seperti putAll dan clear, pengambilan secara bersamaan dapat mencerminkan penyisipan atau penghapusan hanya beberapa entri. Demikian pula, Iterator dan Enumerasi mengembalikan elemen yang mencerminkan keadaan tabel hash di beberapa titik di atau sejak penciptaan iterator / enumerasi. Mereka tidak membuang ConcurrentModificationException. Namun, iterator dirancang untuk digunakan hanya oleh satu utas pada satu waktu.

Apa artinya? Apa yang terjadi jika saya mencoba untuk mengulangi peta dengan dua utas secara bersamaan? Apa yang terjadi jika saya meletakkan atau menghapus nilai dari peta saat iterasi?

Palo
sumber

Jawaban:

193

Apa artinya?

Itu berarti bahwa setiap iterator yang Anda peroleh dari a ConcurrentHashMapdirancang untuk digunakan oleh satu utas dan tidak boleh diedarkan. Ini termasuk gula sintaksis yang disediakan untuk setiap loop.

Apa yang terjadi jika saya mencoba untuk mengulangi peta dengan dua utas secara bersamaan?

Ini akan berfungsi seperti yang diharapkan jika masing-masing utas menggunakan iterator sendiri.

Apa yang terjadi jika saya meletakkan atau menghapus nilai dari peta saat iterasi?

Dijamin bahwa segala sesuatunya tidak akan rusak jika Anda melakukan ini (itu artinya "bersamaan" ConcurrentHashMap). Namun, tidak ada jaminan bahwa satu utas akan melihat perubahan pada peta yang dilakukan utas lainnya (tanpa memperoleh iterator baru dari peta). Iterator dijamin untuk mencerminkan keadaan peta pada saat pembuatannya. Perubahan lebih lanjut dapat tercermin dalam iterator, tetapi tidak harus demikian.

Kesimpulannya, pernyataan seperti

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

akan baik-baik saja (atau setidaknya aman) hampir setiap kali Anda melihatnya.

Waldheinz
sumber
Jadi apa yang akan terjadi jika selama iterasi, utas lain menghapus objek o10 dari peta? Apakah saya masih dapat melihat o10 dalam iterasi meskipun telah dihapus? @Waldheinz
Alex
Seperti yang dinyatakan di atas, itu benar-benar tidak ditentukan jika iterator yang ada akan mencerminkan perubahan peta nanti. Jadi saya tidak tahu, dan berdasarkan spesifikasi tidak ada yang melakukannya (tanpa melihat kode, dan itu dapat berubah dengan setiap pembaruan runtime). Jadi Anda tidak bisa mengandalkannya.
Waldheinz
8
Tapi saya masih punya ConcurrentModificationExceptionwaktu iterasi ConcurrentHashMap, kenapa?
Kimi Chiu
@ KimiChiu Anda mungkin harus memposting pertanyaan baru dengan memberikan kode yang memicu pengecualian itu, tetapi saya sangat ragu itu berasal langsung dari iterasi wadah bersamaan. kecuali implementasi Java bermasalah.
Waldheinz
18

Anda dapat menggunakan kelas ini untuk menguji dua utas pengakses dan satu memutasi instance bersama dari 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);
  }
}

Tidak terkecuali akan terlempar.

Berbagi iterator yang sama antara utas pengakses dapat menyebabkan kebuntuan:

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);
  }
}

Segera setelah Anda mulai berbagi yang sama di Iterator<Map.Entry<String, String>>antara accessor dan mutator threads java.lang.IllegalStateExceptionakan mulai bermunculan.

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);
  }
}
Boris Pavlovic
sumber
Apakah Anda yakin tentang 'Berbagi iterator yang sama antara utas accessor dapat menyebabkan kebuntuan'? Dokumen mengatakan baca belum diblokir dan saya mencoba program Anda dan belum ada kebuntuan yang terjadi. Meskipun hasil iterate akan salah.
Tony
12

Ini berarti bahwa Anda tidak boleh berbagi objek iterator di antara banyak utas. Membuat banyak iterator dan menggunakannya secara bersamaan di utas terpisah adalah hal yang baik.

Tuure Laurinolli
sumber
Apa alasan Anda tidak menggunakan huruf I di Iterator? Karena itu adalah nama kelas, itu mungkin kurang membingungkan.
Bill Michell
1
@ Bill Michell, sekarang kita berada di semantik memposting etiket. Saya pikir dia seharusnya membuat Iterator tautan kembali ke javadoc untuk Iterator, atau paling tidak menempatkannya di dalam anotasi kode inline (`).
Tim Bender
10

Ini mungkin memberi Anda wawasan yang baik

ConcurrentHashMap mencapai konkurensi yang lebih tinggi dengan sedikit mengendurkan janji-janji yang dibuat untuk penelepon. Operasi pengambilan akan mengembalikan nilai yang disisipkan oleh operasi penyisipan yang baru saja selesai, dan juga dapat mengembalikan nilai yang ditambahkan oleh operasi penyisipan yang secara bersamaan sedang berlangsung (tetapi tidak akan mengembalikan hasil omong kosong). Iterator yang dikembalikan oleh ConcurrentHashMap.iterator () akan mengembalikan setiap elemen paling banyak sekali dan tidak akan pernah membuang ConcurrentModificationException, tetapi mungkin atau mungkin tidak mencerminkan penyisipan atau pemindahan yang terjadi sejak iterator dibangun. Tidak diperlukan penguncian selebar tabel (atau bahkan mungkin) untuk memberikan keamanan benang saat melakukan iterasi koleksi. ConcurrentHashMap dapat digunakan sebagai pengganti untuk sinkronisasi peta atau Hashtable dalam aplikasi apa pun yang tidak bergantung pada kemampuan untuk mengunci seluruh tabel untuk mencegah pembaruan.

Mengenai hal ini:

Namun, iterator dirancang untuk digunakan hanya oleh satu utas pada satu waktu.

Itu berarti, sementara menggunakan iterator yang dihasilkan oleh ConcurrentHashMap di dua utas aman, itu dapat menyebabkan hasil yang tidak terduga dalam aplikasi.

nanda
sumber
4

Apa artinya?

Ini berarti Anda tidak boleh mencoba menggunakan iterator yang sama dalam dua utas. Jika Anda memiliki dua utas yang perlu untuk mengulangi kunci, nilai atau entri, maka masing-masing harus membuat dan menggunakan iterator mereka sendiri.

Apa yang terjadi jika saya mencoba untuk mengulangi peta dengan dua utas secara bersamaan?

Tidak sepenuhnya jelas apa yang akan terjadi jika Anda melanggar aturan ini. Anda bisa saja mendapatkan perilaku yang membingungkan, dengan cara yang sama seperti yang Anda lakukan jika (misalnya) dua utas mencoba membaca dari input standar tanpa menyinkronkan. Anda juga bisa mendapatkan perilaku yang tidak aman.

Tetapi jika kedua utas menggunakan iterator yang berbeda, Anda harus baik-baik saja.

Apa yang terjadi jika saya meletakkan atau menghapus nilai dari peta saat iterasi?

Itu masalah terpisah, tetapi bagian javadoc yang Anda kutip menjawabnya dengan memadai. Pada dasarnya, iterator aman digunakan, tetapi tidak ditentukan apakah Anda akan melihat efek dari penyisipan, pembaruan, atau penghapusan bersamaan yang tercermin dalam urutan objek yang dikembalikan oleh iterator. Dalam praktiknya, itu mungkin tergantung pada di mana peta pembaruan terjadi.

Stephen C
sumber