[java] 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);
  }
}

Iterator<Map.Entry<String, String>>접근 자와 뮤 테이터 스레드간에 동일한 공유를 시작하자마자 java.lang.IllegalStateException팝업이 시작됩니다.

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


답변

이는 여러 스레드간에 반복자 객체를 공유하지 않아야 함을 의미합니다. 여러 반복자를 만들고 별도의 스레드에서 동시에 사용하는 것이 좋습니다.


답변

이것은 당신에게 좋은 통찰력을 줄 수 있습니다

ConcurrentHashMap은 호출자에 대한 약속을 약간 완화하여 동시성을 향상시킵니다. 검색 작업은 가장 최근에 완료된 삽입 작업에 의해 삽입 된 값을 반환하고 동시에 진행중인 삽입 작업에 의해 추가 된 값을 반환 할 수도 있습니다 (그러나 어떠한 경우에도 넌센스 결과를 반환하지는 않습니다). ConcurrentHashMap.iterator ()에 의해 리턴 된 반복자는 각 요소를 최대 한 번만 리턴하며 ConcurrentModificationException을 발생시키지 않지만 반복자가 구성된 이후에 발생한 삽입 또는 제거를 반영하거나 반영하지 않을 수 있습니다.. 콜렉션을 반복 할 때 스레드 안전성을 제공하기 위해 테이블 ​​전체 잠금이 필요하지 않습니다. ConcurrentHashMap은 업데이트를 방지하기 위해 전체 테이블을 잠그는 기능에 의존하지 않는 모든 응용 프로그램에서 synchronizedMap 또는 Hashtable의 대체물로 사용될 수 있습니다.

이것을 고려하면:

그러나 반복기는 한 번에 하나의 스레드 만 사용하도록 설계되었습니다.

즉, ConcurrentHashMap에 의해 생성 된 반복자를 두 스레드에서 사용하는 것이 안전하지만 응용 프로그램에 예기치 않은 결과가 발생할 수 있습니다.


답변

무슨 뜻인가요?

이는 두 개의 스레드에서 동일한 반복기를 사용해서는 안됨을 의미합니다. 키, 값 또는 항목을 반복해야하는 두 개의 스레드가있는 경우 각각 고유 한 반복자를 작성하고 사용해야합니다.

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

이 규칙을 어겼을 때 어떤 일이 일어날 지 명확하지 않습니다. 예를 들어 두 개의 스레드가 동기화하지 않고 표준 입력에서 읽으려고하는 것과 같은 방식으로 혼란스러운 동작을 얻을 수 있습니다. 스레드로부터 안전하지 않은 동작을 얻을 수도 있습니다.

그러나 두 스레드가 다른 반복자를 사용했다면 괜찮을 것입니다.

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

그것은 별도의 문제이지만 인용 한 javadoc 섹션은 적절하게 대답합니다. 기본적으로 이터레이터는 스레드로부터 안전하지만 이터레이터가 반환 한 객체 시퀀스에 동시 삽입, 업데이트 또는 삭제의 영향을 반영할지 여부 는 정의되어 있지 않습니다 . 실제로는 아마도지도에서 업데이트가 발생하는 위치에 따라 다릅니다.


답변