ConcurrentHashMap değerleri iş parçacığını yineleme güvenli mi?


156

ConcurrentHashMap için javadoc'ta şu:

Alım işlemleri (get dahil) genellikle engellenmez, bu nedenle güncelleme işlemleriyle (put ve remove dahil) çakışabilir. Alımlar, en son tamamlanan güncelleme işlemlerinin başlangıcında tutulan sonuçlarını yansıtır. PutAll ve clear gibi toplu işlemler için, eşzamanlı alımlar yalnızca bazı girişlerin eklenmesini veya kaldırılmasını yansıtabilir. Benzer şekilde, Yineleyiciler ve Numaralandırmalar yineleyici / numaralandırmanın oluşturulmasının bir noktasında veya bu tarihten bu yana karma tablonun durumunu yansıtan öğeler döndürür. ConcurrentModificationException'ı atmazlar. Ancak, yineleyiciler aynı anda yalnızca bir iş parçacığı tarafından kullanılmak üzere tasarlanmıştır.

Bu ne demek? Haritayı aynı anda iki iş parçacığıyla yinelemeye çalışırsam ne olur? Tekrarlarken haritadan bir değer koyarsam veya kaldırırsam ne olur?

Yanıtlar:


193

Bu ne demek?

Bu, bir elde ettiğiniz her yineleyicinin ConcurrentHashMaptek bir iş parçacığı tarafından kullanılmak üzere tasarlandığı ve iletilmemesi gerektiği anlamına gelir. Bu, her bir döngü için sağladığı sözdizimsel şekeri içerir.

Haritayı aynı anda iki iş parçacığıyla yinelemeye çalışırsam ne olur?

İş parçacıklarının her biri kendi yineleyicisini kullanıyorsa beklendiği gibi çalışacaktır.

Tekrarlarken haritadan bir değer koyarsam veya kaldırırsam ne olur?

Bunu yaparsanız işlerin kırılmayacağı garanti edilir (bu, "eşzamanlı" ConcurrentHashMapanlamlarının bir parçasıdır ). Bununla birlikte, bir iş parçacığının diğer iş parçacığının gerçekleştirdiği haritadaki değişiklikleri (haritadan yeni bir yineleyici almadan) göreceğini garanti etmez. Yineleyicinin, oluşturulduğu sırada haritanın durumunu yansıtması garanti edilir. Diğer değişiklikler yineleyiciye yansıtılabilir, ancak bunların olması gerekmez.

Sonuç olarak,

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

neredeyse her gördüğünüzde iyi (veya en azından güvenli) olacaktır.


Yineleme sırasında başka bir iş parçacığı o10 nesnesini haritadan kaldırırsa ne olur? Çıkarılmış olsa bile yinelemede o10'u görebilir miyim? @Waldheinz
Alex

Yukarıda belirtildiği gibi, mevcut bir yineleyicinin harita üzerinde daha sonra yapılacak değişiklikleri yansıtıp yansıtmadığı gerçekten belirtilmemiştir. Bu yüzden bilmiyorum ve şartname ile hiç kimse (koda bakmadan, ve bu çalışma zamanı her güncelleme ile değişebilir). Yani ona güvenemezsin.
Waldheinz

8
Ama hala ConcurrentModificationExceptionyineleyen bir sürem var ConcurrentHashMap, neden?
Kimi Chiu

@KimiChiu muhtemelen bu istisnayı tetikleyen kodu sağlayan yeni bir soru göndermek gerekir, ama son derece doğrudan yineleme aynı anda bir kaptan kaynaklanıyor şüphe. Java uygulaması buggy olmadığı sürece.
Waldheinz

18

Bu sınıfı, iki erişen iş parçacığını ve bir tanesinin paylaşılan örneğini değiştirerek sınamak için kullanabilirsiniz 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);
  }
}

Hiçbir istisna atılmayacak.

Aynı yineleyiciyi erişimci iş parçacıkları arasında paylaşmak kilitlenmeye neden olabilir:

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

Aynı şeyi Iterator<Map.Entry<String, String>>erişimci ve mutasyona uğrayan iş parçacıkları arasında paylaşmaya java.lang.IllegalStateExceptionbaşlar başlamaz ortaya çıkmaya başlayacaktır.

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

'Aynı yineleyiciyi erişimci iş parçacıkları arasında paylaşmak kilitlenmeye yol açabilir' emin misiniz? Belgede okumanın engellenmediği söyleniyor ve programınızı denedim ve henüz kilitlenme olmadı. Tekrarlanan sonuç yanlış olsa da.
Tony

12

Bu, bir yineleyici nesnesini birden çok iş parçacığı arasında paylaşmamanız gerektiği anlamına gelir. Birden çok yineleyici oluşturmak ve bunları ayrı ayrı iş parçacıklarında aynı anda kullanmak iyidir.


Iterator'daki I harfini büyük kullanmamanızın bir nedeni var mı? Sınıfın adı olduğu için daha az kafa karıştırıcı olabilir.
Bill Michell

1
@ Fatura Michell, şimdi görgü kuralları gönderme anlamındayız. Iterator'ı bir Iterator için javadoc'a bir bağlantı yapması veya en azından satır içi kod ek açıklamalarına (`) yerleştirmesi gerektiğini düşünüyorum.
Tim Bender

10

Bu size iyi bir fikir verebilir

ConcurrentHashMap, arayanlara verdiği sözleri hafifleterek daha yüksek eşzamanlılık sağlar. Bir geri alma işlemi, en son tamamlanan ekleme işlemi tarafından eklenen değeri döndürür ve aynı zamanda devam eden bir ekleme işlemi tarafından eklenen bir değer döndürebilir (ancak hiçbir durumda anlamsız bir sonuç döndürmez). ConcurrentHashMap.iterator () tarafından döndürülen yineleyiciler, her öğeyi en fazla bir kez döndürür ve ConcurrentModificationException'ı atmaz, ancak yineleyici oluşturulduğundan bu yana gerçekleşen ekleme veya kaldırma işlemlerini yansıtabilir veya yansıtmayabilir. Koleksiyonu tekrarlarken iplik güvenliği sağlamak için tablo çapında kilitleme gerekmez (hatta mümkün olabilir). ConcurrentHashMap, güncellemeleri önlemek için tüm tabloyu kilitleme yeteneğine güvenmeyen herhangi bir uygulamada senkronize edilmiş Harita veya Hashtable'ın yerine kullanılabilir.

Bununla ilgili olarak:

Ancak, yineleyiciler aynı anda yalnızca bir iş parçacığı tarafından kullanılmak üzere tasarlanmıştır.

Bu, ConcurrentHashMap tarafından üretilen yineleyicileri iki iş parçacığında kullanırken güvenli uygulamada beklenmeyen bir sonuca neden olabilir.


4

Bu ne demek?

Bu, aynı yineleyiciyi iki iş parçacığında kullanmaya çalışmamanız gerektiği anlamına gelir. Anahtarlar, değerler veya girişler üzerinde yinelenmesi gereken iki iş parçacığınız varsa, her biri kendi yineleyicilerini oluşturmalı ve kullanmalıdır.

Haritayı aynı anda iki iş parçacığıyla yinelemeye çalışırsam ne olur?

Bu kuralı ihlal ederseniz ne olacağı tam olarak açık değildir. İki iş parçacığı standart girişten senkronize etmeden okumaya çalışırsa, aynı şekilde kafa karıştırıcı davranışlar elde edebilirsiniz. Ayrıca, iş parçacığı için güvenli olmayan bir davranış da alabilirsiniz.

Ancak iki iş parçacığı farklı yineleyiciler kullandıysa, iyi olmalısınız.

Tekrarlarken haritadan bir değer koyarsam veya kaldırırsam ne olur?

Bu ayrı bir konudur, ancak alıntıladığınız javadoc bölümü buna yeterince cevap verir. Temel olarak, yineleyiciler iş parçacığı için güvenlidir, ancak yineleyici tarafından döndürülen nesneler sırasına yansıyan eşzamanlı ekleme, güncelleme veya silme işlemlerinin etkilerini görüp görmeyeceğiniz tanımlanmamıştır . Uygulamada, muhtemelen güncellemelerin haritada nerede olduğuna bağlıdır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.