Việc lặp lại các giá trị của luồng concienHashMap có an toàn không?


156

Trong javadoc cho ConcảnHashMap là như sau:

Các hoạt động truy xuất (bao gồm get) thường không chặn, do đó có thể trùng lặp với các hoạt động cập nhật (bao gồm đặt và xóa). Các phản hồi phản ánh kết quả của các hoạt động cập nhật được hoàn thành gần đây nhất khi bắt đầu. Đối với các hoạt động tổng hợp như putAll và rõ ràng, các truy xuất đồng thời có thể phản ánh việc chèn hoặc xóa chỉ một số mục. Tương tự, các trình lặp và liệt kê trả về các phần tử phản ánh trạng thái của bảng băm tại một số điểm tại hoặc kể từ khi tạo ra trình lặp / liệt kê. Họ không ném ConcExModificationException. Tuy nhiên, các trình vòng lặp được thiết kế để chỉ được sử dụng bởi một luồng tại một thời điểm.

Nó có nghĩa là gì? Điều gì xảy ra nếu tôi cố gắng lặp lại bản đồ với hai luồng cùng một lúc? Điều gì xảy ra nếu tôi đặt hoặc xóa một giá trị khỏi bản đồ trong khi lặp lại nó?

Câu trả lời:


193

Nó có nghĩa là gì?

Điều đó có nghĩa là mỗi trình vòng lặp bạn nhận được từ một ConcurrentHashMapđược thiết kế để được sử dụng bởi một luồng duy nhất và không nên truyền qua. Điều này bao gồm đường cú pháp mà vòng lặp for cung cấp.

Điều gì xảy ra nếu tôi cố gắng lặp lại bản đồ với hai luồng cùng một lúc?

Nó sẽ hoạt động như mong đợi nếu mỗi luồng sử dụng trình vòng lặp riêng của nó.

Điều gì xảy ra nếu tôi đặt hoặc xóa một giá trị khỏi bản đồ trong khi lặp lại nó?

Nó được đảm bảo rằng mọi thứ sẽ không bị phá vỡ nếu bạn làm điều này (đó là một phần của những gì "đồng thời" ConcurrentHashMapcó nghĩa là). Tuy nhiên, không có gì đảm bảo rằng một luồng sẽ thấy các thay đổi đối với bản đồ mà luồng khác thực hiện (mà không nhận được trình lặp mới từ bản đồ). Trình lặp được đảm bảo để phản ánh trạng thái của bản đồ tại thời điểm tạo. Những thay đổi xa hơn có thể được phản ánh trong iterator, nhưng chúng không phải như vậy.

Tóm lại, một tuyên bố như

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

sẽ ổn (hoặc ít nhất là an toàn) gần như mỗi khi bạn nhìn thấy nó.


Vậy điều gì sẽ xảy ra nếu trong quá trình lặp lại, một luồng khác đã xóa một đối tượng o10 khỏi bản đồ? Tôi vẫn có thể thấy o10 trong lần lặp ngay cả khi nó đã bị xóa? @Waldheinz
Alex

Như đã nêu ở trên, nó thực sự không được chỉ định nếu một trình vòng lặp hiện có sẽ phản ánh những thay đổi sau này đối với bản đồ. Vì vậy, tôi không biết, và theo đặc tả, không ai làm điều đó (không nhìn vào mã và điều đó có thể thay đổi với mỗi bản cập nhật của thời gian chạy). Vì vậy, bạn không thể dựa vào nó.
Waldheinz

8
Nhưng tôi vẫn có một lần ConcurrentModificationExceptionlặp lại a ConcurrentHashMap, tại sao?
Kimi Chiu

@KimiChiu có lẽ bạn nên đăng một câu hỏi mới cung cấp mã kích hoạt ngoại lệ đó, nhưng tôi rất nghi ngờ nó bắt nguồn trực tiếp từ việc lặp lại một thùng chứa đồng thời. trừ khi triển khai Java là lỗi.
Waldheinz

18

Bạn có thể sử dụng lớp này để kiểm tra hai luồng truy cập và một luồng làm thay đổi thể hiện được chia sẻ của 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);
  }
}

Không có ngoại lệ sẽ được ném.

Chia sẻ cùng một trình vòng lặp giữa các luồng truy cập có thể dẫn đến bế tắc:

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

Ngay khi bạn bắt đầu chia sẻ tương tự Iterator<Map.Entry<String, String>>giữa các chủ đề java.lang.IllegalStateExceptioncủa trình truy cập và trình biến đổi, s sẽ bắt đầu bật lên.

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

Bạn có chắc chắn về 'Chia sẻ cùng một trình vòng lặp giữa các luồng truy cập có thể dẫn đến bế tắc' không? Tài liệu nói rằng đọc không bị chặn và tôi đã thử chương trình của bạn và chưa có bế tắc nào xảy ra. Mặc dù kết quả lặp sẽ sai.
Tony

12

Điều đó có nghĩa là bạn không nên chia sẻ một đối tượng lặp trong số nhiều luồng. Tạo nhiều vòng lặp và sử dụng chúng đồng thời trong các luồng riêng biệt là tốt.


Bất kỳ lý do nào bạn không viết hoa I trong Iterator? Vì nó là tên của lớp, nên nó có thể ít gây nhầm lẫn hơn.
Bill Michell

1
@Bill Michell, bây giờ chúng tôi đang trong ngữ nghĩa của việc đăng nghi thức. Tôi nghĩ rằng anh ta nên tạo Iterator một liên kết trở lại javadoc cho một Iterator hoặc ít nhất là đặt nó bên trong các chú thích mã nội tuyến (`).
Tim Bender

10

Điều này có thể cung cấp cho bạn một cái nhìn sâu sắc tốt

ConcảnHashMap đạt được sự đồng thời cao hơn bằng cách thư giãn nhẹ những lời hứa mà nó đưa ra cho người gọi. Một hoạt động truy xuất sẽ trả về giá trị được chèn bởi hoạt động chèn hoàn thành gần đây nhất và cũng có thể trả về một giá trị được thêm vào bởi một hoạt động chèn đồng thời đang diễn ra (nhưng trong mọi trường hợp sẽ không trả về kết quả vô nghĩa). Các trình vòng lặp được trả về bởi ConcảnHashMap.iterator () sẽ trả về từng phần tử một lần và sẽ không bao giờ ném ConcurrencyModificationException, nhưng có thể hoặc không thể phản ánh các phần chèn hoặc xóa đã xảy ra kể từ khi trình lặp được xây dựng. Không cần khóa rộng bảng (hoặc thậm chí có thể) để cung cấp an toàn luồng khi lặp bộ sưu tập. ConcảnHashMap có thể được sử dụng để thay thế cho SyncMap hoặc Hashtable trong bất kỳ ứng dụng nào không phụ thuộc vào khả năng khóa toàn bộ bảng để ngăn cập nhật.

Về vấn đề này:

Tuy nhiên, các trình vòng lặp được thiết kế để chỉ được sử dụng bởi một luồng tại một thời điểm.

Điều đó có nghĩa là, trong khi sử dụng các trình vòng lặp được tạo bởi ConcảnHashMap trong hai luồng là an toàn, nó có thể gây ra kết quả không mong muốn trong ứng dụng.


4

Nó có nghĩa là gì?

Điều đó có nghĩa là bạn không nên cố gắng sử dụng cùng một trình vòng lặp trong hai luồng. Nếu bạn có hai luồng cần lặp qua các khóa, giá trị hoặc mục, thì mỗi luồng sẽ tạo và sử dụng các trình lặp riêng của chúng.

Điều gì xảy ra nếu tôi cố gắng lặp lại bản đồ với hai luồng cùng một lúc?

Nó không hoàn toàn rõ ràng những gì sẽ xảy ra nếu bạn phá vỡ quy tắc này. Bạn chỉ có thể có hành vi khó hiểu, giống như cách bạn làm nếu (ví dụ) hai luồng cố gắng đọc từ đầu vào tiêu chuẩn mà không đồng bộ hóa. Bạn cũng có thể có hành vi không an toàn chủ đề.

Nhưng nếu hai luồng sử dụng các vòng lặp khác nhau, bạn sẽ ổn thôi.

Điều gì xảy ra nếu tôi đặt hoặc xóa một giá trị khỏi bản đồ trong khi lặp lại nó?

Đó là một vấn đề riêng biệt, nhưng phần javadoc mà bạn trích dẫn đã trả lời đầy đủ. Về cơ bản, các trình vòng lặp là an toàn luồng, nhưng không xác định liệu bạn sẽ thấy tác động của bất kỳ sự chèn, cập nhật hoặc xóa đồng thời nào được phản ánh trong chuỗi các đối tượng được trả về bởi trình lặp. Trong thực tế, nó có thể phụ thuộc vào nơi cập nhật trong bản đồ.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.