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


156

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

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

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

답변:


193

무슨 뜻인가요?

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

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

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

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

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

결론적으로

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

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


반복 중에 다른 스레드가 맵에서 객체 o10을 제거하면 어떻게됩니까? o10이 제거 되어도 반복에서 여전히 o10을 볼 수 있습니까? @ Waldheinz
Alex

위에서 언급했듯이 기존 반복자가 나중에 맵에 대한 변경 사항을 반영 할 경우에는 실제로 지정되지 않습니다. 그래서 나는 알지 못하고 사양에 따라 아무도 코드를 보지 않고 런타임마다 업데이트 될 때마다 변경 될 수 있습니다. 따라서 당신은 그것에 의존 할 수 없습니다.
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>>접근 자와 뮤 테이터 스레드간에 동일한 공유를 시작하자마자 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);
  }
}

'액세서 스레드간에 동일한 반복자를 공유하면 교착 상태가 발생할 수 있습니다'에 대해 확실합니까? 문서에 읽기가 차단되지 않았으며 프로그램을 시도했지만 교착 상태가 발생하지 않았습니다. 반복 결과가 잘못되었지만.
Tony

12

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


Iterator에서 I를 대문자로 표시하지 않은 이유는 무엇입니까? 클래스의 이름이므로 혼동이 줄어 듭니다.
Bill Michell

1
@Bill Michell, 이제 우리는 에티켓을 게시하는 의미에 있습니다. 그는 Iterator를 Iterator의 javadoc에 대한 링크로 만들었거나 최소한 인라인 코드 주석 (`) 안에 배치해야한다고 생각합니다.
Tim Bender

10

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

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

이것을 고려하면:

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

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


4

무슨 뜻인가요?

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

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

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

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

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

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

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.