Khối đồng bộ hóa Java so với Collections.synchronizedMap


85

Mã sau có được thiết lập để đồng bộ hóa các cuộc gọi một cách chính xác synchronizedMapkhông?

public class MyClass {
  private static Map<String, List<String>> synchronizedMap = Collections.synchronizedMap(new HashMap<String, List<String>>());

  public void doWork(String key) {
    List<String> values = null;
    while ((values = synchronizedMap.remove(key)) != null) {
      //do something with values
    }
  }

  public static void addToMap(String key, String value) {
    synchronized (synchronizedMap) {
      if (synchronizedMap.containsKey(key)) {
        synchronizedMap.get(key).add(value);
      }
      else {
        List<String> valuesList = new ArrayList<String>();
        valuesList.add(value);
        synchronizedMap.put(key, valuesList);
      }
    }
  }
}

Theo hiểu biết của tôi, tôi cần khối được đồng bộ hóa trong addToMap()để ngăn luồng khác gọi remove()hoặc containsKey()trước khi tôi thực hiện lệnh gọi đến put()nhưng tôi không cần khối được đồng bộ hóa trong doWork()vì một chuỗi khác không thể nhập khối được đồng bộ hóa trong addToMap()trước khi remove()trả về vì tôi đã tạo Bản đồ ban đầu với Collections.synchronizedMap(). Đúng không? Có cách nào tốt hơn để làm điều này?

Câu trả lời:


90

Collections.synchronizedMap() đảm bảo rằng mỗi hoạt động nguyên tử bạn muốn chạy trên bản đồ sẽ được đồng bộ hóa.

Tuy nhiên, chạy hai (hoặc nhiều) hoạt động trên bản đồ, phải được đồng bộ hóa trong một khối. Vì vậy, có - bạn đang đồng bộ hóa chính xác.


26
Tôi nghĩ sẽ rất tốt nếu đề cập rằng điều này hoạt động vì javadocs tuyên bố rõ ràng rằng Bản đồ đồng bộ hóa tự đồng bộ hóa trên bản đồ chứ không phải một số khóa nội bộ. Nếu đó là trường hợp được đồng bộ hóa (Bản đồ đồng bộ hóa) sẽ không đúng.
extraneon vào

2
@Yuval bạn có thể giải thích câu trả lời của mình sâu hơn một chút không? Bạn nói SychronizedMap thực hiện các hoạt động theo nguyên tử, nhưng sau đó tại sao bạn lại cần khối được đồng bộ hóa của riêng mình nếu syncMap biến tất cả các hoạt động của bạn thành nguyên tử? Đoạn đầu tiên của bạn dường như loại trừ lo lắng về đoạn thứ hai.
almel

@almel xem câu trả lời
Sergey

2
tại sao cần phải có khối đồng bộ như bản đồ đã sử dụng Collections.synchronizedMap()? Tôi không nhận được điểm thứ hai.
Bimal Sharma


13

thể có một lỗi nhỏ trong mã của bạn.

[ CẬP NHẬT: Vì anh ấy đang sử dụng map.remove () nên mô tả này không hoàn toàn hợp lệ. Tôi đã bỏ lỡ sự thật đó lần đầu tiên. :( Cảm ơn tác giả của câu hỏi đã chỉ ra điều đó. Tôi vẫn để nguyên phần còn lại, nhưng đã thay đổi câu dẫn đầu để nói rằng có thể có một lỗi.]

Trong doWork (), bạn nhận giá trị Danh sách từ Bản đồ theo cách an toàn theo chuỗi. Tuy nhiên, sau đó, bạn đang truy cập danh sách đó trong một vấn đề không an toàn. Ví dụ: một luồng có thể đang sử dụng danh sách trong doWork () trong khi một luồng khác gọi syncMap.get (key) .add (value) trong addToMap () . Hai quyền truy cập đó không được đồng bộ hóa. Quy tắc chung là đảm bảo an toàn chuỗi của một bộ sưu tập không mở rộng đến các khóa hoặc giá trị mà chúng lưu trữ.

Bạn có thể khắc phục điều này bằng cách chèn một danh sách được đồng bộ hóa vào bản đồ như

List<String> valuesList = new ArrayList<String>();
valuesList.add(value);
synchronizedMap.put(key, Collections.synchronizedList(valuesList)); // sync'd list

Ngoài ra, bạn có thể đồng bộ hóa trên bản đồ khi truy cập danh sách trong doWork () :

  public void doWork(String key) {
    List<String> values = null;
    while ((values = synchronizedMap.remove(key)) != null) {
      synchronized (synchronizedMap) {
          //do something with values
      }
    }
  }

Tùy chọn cuối cùng sẽ giới hạn đồng thời một chút, nhưng IMO có phần rõ ràng hơn.

Ngoài ra, một ghi chú nhanh về ConcurrentHashMap. Đây là một lớp thực sự hữu ích, nhưng không phải lúc nào cũng là sự thay thế thích hợp cho các HashMap đồng bộ. Trích dẫn từ Javadocs của nó,

Lớp này hoàn toàn có thể tương tác với Hashtable trong các chương trình dựa vào sự an toàn của luồng nhưng không dựa trên chi tiết đồng bộ hóa của nó .

Nói cách khác, putIfAbsent () rất phù hợp để chèn nguyên tử nhưng không đảm bảo các phần khác của bản đồ sẽ không thay đổi trong lần gọi đó; nó chỉ đảm bảo tính nguyên tử. Trong chương trình mẫu của mình, bạn đang dựa vào các chi tiết đồng bộ hóa của (một HashMap được đồng bộ hóa) cho những thứ khác ngoài put ().

Thứ cuối cùng. :) Trích dẫn tuyệt vời này từ Java Concurrency in Practice luôn giúp tôi thiết kế một chương trình đa luồng gỡ lỗi.

Đối với mỗi biến trạng thái có thể thay đổi có thể được truy cập bởi nhiều hơn một luồng, tất cả các truy cập vào biến đó phải được thực hiện với cùng một khóa được giữ.


Tôi hiểu quan điểm của bạn về lỗi nếu tôi đang truy cập danh sách bằng syncMap.get (). Vì tôi đang sử dụng remove (), không nên thêm lần tiếp theo với khóa đó tạo ArrayList mới và không can thiệp vào danh sách mà tôi đang sử dụng trong doWork?
Ryan Ahearn

Chính xác! Tôi hoàn toàn vượt qua sự loại bỏ của bạn.
JLR

1
Đối với mỗi biến trạng thái có thể thay đổi có thể được truy cập bởi nhiều hơn một luồng, tất cả các truy cập vào biến đó phải được thực hiện với cùng một khóa được giữ. ---- Tôi thường thêm thuộc tính riêng chỉ là một Đối tượng mới () và sử dụng thuộc tính đó cho các khối đồng bộ hóa của mình. Bằng cách đó tôi biết tất cả thông qua bản thô cho bối cảnh đó. đồng bộ hóa (objectInVar) {}
AnthonyJClink

11

Có, bạn đang đồng bộ hóa chính xác. Tôi sẽ giải thích điều này chi tiết hơn. Bạn phải đồng bộ hóa hai hoặc nhiều lệnh gọi phương thức trên đối tượng Bản đồ đồng bộ hóa chỉ trong trường hợp bạn phải dựa vào kết quả của (các) lệnh gọi phương thức trước đó trong lần gọi phương thức tiếp theo trong chuỗi lệnh gọi phương thức trên đối tượng Bản đồ đồng bộ hóa. Hãy xem mã này:

synchronized (synchronizedMap) {
    if (synchronizedMap.containsKey(key)) {
        synchronizedMap.get(key).add(value);
    }
    else {
        List<String> valuesList = new ArrayList<String>();
        valuesList.add(value);
        synchronizedMap.put(key, valuesList);
    }
}

Trong mã này

synchronizedMap.get(key).add(value);

synchronizedMap.put(key, valuesList);

các cuộc gọi phương thức được dựa trên kết quả của

synchronizedMap.containsKey(key)

cuộc gọi phương thức.

Nếu chuỗi lệnh gọi phương thức không được đồng bộ, kết quả có thể sai. Ví dụ: thread 1đang thực thi phương thức addToMap()thread 2đang thực thi phương thức doWork() Trình tự các lệnh gọi phương thức trên synchronizedMapđối tượng có thể như sau: Thread 1đã thực thi phương thức

synchronizedMap.containsKey(key)

và kết quả là " true". Sau khi hệ điều hành đó đã chuyển điều khiển thực thi sang thread 2và nó đã thực thi

synchronizedMap.remove(key)

Sau khi điều khiển thực thi đó đã được chuyển trở lại thread 1và nó đã thực thi chẳng hạn

synchronizedMap.get(key).add(value);

tin rằng synchronizedMapđối tượng chứa keyNullPointerExceptionsẽ được ném vì synchronizedMap.get(key) sẽ quay trở lại null. Nếu chuỗi các lệnh gọi phương thức trên synchronizedMapđối tượng không phụ thuộc vào kết quả của nhau thì bạn không cần phải đồng bộ hóa chuỗi. Ví dụ: bạn không cần phải đồng bộ hóa chuỗi này:

synchronizedMap.put(key1, valuesList1);
synchronizedMap.put(key2, valuesList2);

Đây

synchronizedMap.put(key2, valuesList2);

cuộc gọi phương thức không dựa trên kết quả của

synchronizedMap.put(key1, valuesList1);

cuộc gọi phương thức (nó không quan tâm nếu một số luồng đã can thiệp vào giữa hai cuộc gọi phương thức và ví dụ: đã loại bỏ key1).


4

Điều đó có vẻ đúng với tôi. Nếu tôi phải thay đổi bất cứ điều gì, tôi sẽ ngừng sử dụng Collections.synchronizedMap () và đồng bộ hóa mọi thứ theo cùng một cách, chỉ để làm cho nó rõ ràng hơn.

Ngoài ra, tôi sẽ thay thế

  if (synchronizedMap.containsKey(key)) {
    synchronizedMap.get(key).add(value);
  }
  else {
    List<String> valuesList = new ArrayList<String>();
    valuesList.add(value);
    synchronizedMap.put(key, valuesList);
  }

với

List<String> valuesList = synchronziedMap.get(key);
if (valuesList == null)
{
  valuesList = new ArrayList<String>();
  synchronziedMap.put(key, valuesList);
}
valuesList.add(value);

3
Việc cần làm. Tôi không nhận được nó tại sao chúng ta nên sử dụng các Collections.synchronizedXXX()API khi chúng ta vẫn phải đồng bộ hóa trên một số đối tượng (trong đó sẽ chỉ là bộ sưu tập riêng của mình trong hầu hết các trường hợp) trong logic mỗi ngày ứng dụng của chúng tôi
kellogs

3

Cách bạn đã đồng bộ hóa là đúng. Nhưng có một nhược điểm

  1. Trình bao bọc được đồng bộ hóa được cung cấp bởi khung công tác Bộ sưu tập đảm bảo rằng các cuộc gọi phương thức tức là thêm / nhận / chứa sẽ chạy loại trừ lẫn nhau.

Tuy nhiên, trong thế giới thực, bạn thường truy vấn bản đồ trước khi đưa giá trị vào. Do đó, bạn sẽ cần thực hiện hai thao tác và do đó cần có một khối được đồng bộ hóa. Vì vậy, cách bạn đã sử dụng nó là đúng. Tuy nhiên.

  1. Bạn có thể đã sử dụng triển khai đồng thời Bản đồ có sẵn trong khuôn khổ Bộ sưu tập. Lợi ích của 'ConcurrentHashMap' là

a. Nó có một API 'putIfAbsent' sẽ làm những công việc tương tự nhưng theo cách hiệu quả hơn.

b. Hiệu quả của nó: d Bản đồ CocurrentMap chỉ khóa các phím do đó nó không chặn toàn bộ thế giới của bản đồ. Nơi bạn có khóa cũng như giá trị bị chặn.

c. Bạn có thể đã chuyển tham chiếu của đối tượng bản đồ của mình ở một nơi khác trong cơ sở mã của bạn, nơi bạn / nhà phát triển khác trong nguyên tắc của bạn có thể sử dụng nó không chính xác. Tức là anh ta có thể chỉ thêm () hoặc lấy () mà không cần khóa đối tượng của bản đồ. Do đó, cuộc gọi của anh ấy sẽ không chạy loại trừ lẫn nhau đối với khối đồng bộ hóa của bạn. Nhưng việc sử dụng một triển khai đồng thời giúp bạn yên tâm rằng nó không bao giờ có thể được sử dụng / triển khai không chính xác.


2

Kiểm tra Bộ sưu tập của Google ' Multimap, ví dụ: trang 28 của bản trình bày này .

Nếu bạn không thể sử dụng thư viện đó vì lý do nào đó, hãy cân nhắc sử dụng ConcurrentHashMapthay vì SynchronizedHashMap; nó có một putIfAbsent(K,V)phương thức tiện lợi mà bạn có thể thêm nguyên tử danh sách phần tử nếu nó chưa có ở đó. Ngoài ra, hãy xem xét việc sử dụng CopyOnWriteArrayListcho các giá trị bản đồ nếu cách sử dụng của bạn đảm bảo làm như vậy.

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.