IT박스

ConcurrentHashMap 값을 반복해도 스레드가 안전합니까?

itboxs 2020. 6. 22. 08:08
반응형

ConcurrentHashMap 값을 반복해도 스레드가 안전합니까?


ConcurrentHashMap 에 대한 javadoc 에서 다음은 다음과 같습니다.

검색 작업 (get 포함)은 일반적으로 차단되지 않으므로 업데이트 작업 (put 및 remove 포함)과 겹칠 수 있습니다. 검색은 가장 최근에 완료된 업데이트 작업의 결과를 반영합니다. putAll 및 clear와 같은 집계 작업의 경우 동시 검색에 일부 항목의 삽입 또는 제거가 반영 될 수 있습니다. 마찬가지로 반복자 및 열거는 반복자 / 열거를 생성 한 이후 또는 이후에 해시 테이블의 상태를 반영하는 요소를 반환합니다. ConcurrentModificationException을 발생시키지 않습니다. 그러나 반복기는 한 번에 하나의 스레드 만 사용하도록 설계되었습니다.

무슨 뜻인가요? 두 개의 스레드로 동시에 맵을 반복하려고하면 어떻게됩니까? 반복하는 동안지도에서 값을 넣거나 제거하면 어떻게 되나요?


무슨 뜻인가요?

즉, a에서 얻은 각 반복자 ConcurrentHashMap는 단일 스레드에서 사용하도록 설계되었으며 전달해서는 안됩니다. 여기에는 for-each 루프가 제공하는 구문 설탕이 포함됩니다.

두 개의 스레드로 동시에 맵을 반복하려고하면 어떻게됩니까?

각 스레드가 자체 반복자를 사용하면 예상대로 작동합니다.

반복하는 동안지도에서 값을 넣거나 제거하면 어떻게 되나요?

이렇게하면 문제가 해결되지 않습니다 ( "동시"의 ConcurrentHashMap의미 중 일부입니다 ). 그러나 한 스레드가 다른 스레드가 수행하는 맵의 변경 사항을 볼 수 있다는 보장은 없습니다 (맵에서 새 반복자를 얻지 않고). 이터레이터는 맵 생성시 맵의 상태를 반영합니다. 추가 변경 사항은 반복자에 반영 될 수 있지만 반드시 그럴 필요는 없습니다.

결론적으로

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

거의 볼 때마다 괜찮을 것입니다.


이 클래스를 사용하여 두 개의 액세스 스레드와 하나의 공유 인스턴스를 변경하는 테스트 할 수 있습니다 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);
  }
}

As soon as you start sharing the same Iterator<Map.Entry<String, String>> among accessor and mutator threads java.lang.IllegalStateExceptions will start popping up.

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

It means that you should not share an iterator object among multiple threads. Creating multiple iterators and using them concurrently in separate threads is fine.


This might give you a good insight

ConcurrentHashMap achieves higher concurrency by slightly relaxing the promises it makes to callers. A retrieval operation will return the value inserted by the most recent completed insert operation, and may also return a value added by an insertion operation that is concurrently in progress (but in no case will it return a nonsense result). Iterators returned by ConcurrentHashMap.iterator() will return each element once at most and will not ever throw ConcurrentModificationException, but may or may not reflect insertions or removals that occurred since the iterator was constructed. No table-wide locking is needed (or even possible) to provide thread-safety when iterating the collection. ConcurrentHashMap may be used as a replacement for synchronizedMap or Hashtable in any application that does not rely on the ability to lock the entire table to prevent updates.

Regarding this:

However, iterators are designed to be used by only one thread at a time.

It means, while using iterators produced by ConcurrentHashMap in two threads are safe, it may cause an unexpected result in the application.


What does it mean?

It means that you should not try to use the same iterator in two threads. If you have two threads that need to iterate over the keys, values or entries, then they each should create and use their own iterators.

What happens if I try to iterate the map with two threads at the same time?

It is not entirely clear what would happen if you broke this rule. You could just get confusing behavior, in the same way that you do if (for example) two threads try to read from standard input without synchronizing. You could also get non-thread-safe behavior.

But if the two threads used different iterators, you should be fine.

What happens if I put or remove a value from the map while iterating it?

That's a separate issue, but the javadoc section that you quoted adequately answers it. Basically, the iterators are thread-safe, but it is not defined whether you will see the effects of any concurrent insertions, updates or deletions reflected in the sequence of objects returned by the iterator. In practice, it probably depends on where in the map the updates occur.

참고URL : https://stackoverflow.com/questions/3768554/is-iterating-concurrenthashmap-values-thread-safe

반응형