2009-11-20 16 views
16

Có phải ConcurrentHashMap.get()được bảo đảm để xem trước ConcurrentHashMap.put() theo chuỗi khác nhau? Mong đợi của tôi là có nghĩa là, và đọc JavaDocs dường như chỉ ra như vậy, nhưng tôi 99% tin rằng thực tế là khác nhau. Trên máy chủ sản xuất của tôi, bên dưới có vẻ như đang xảy ra. (Tôi đã bắt gặp nó với khai thác gỗ.) MãConcurrentHashMap.get() có được đảm bảo để xem một ConcurrentHashMap.put() trước đó bằng các luồng khác nhau không?

Pseudo dụ:

static final ConcurrentHashMap map = new ConcurrentHashMap(); 
//sharedLock is key specific. One map, many keys. There is a 1:1 
//  relationship between key and Foo instance. 
void doSomething(Semaphore sharedLock) { 
    boolean haveLock = sharedLock.tryAcquire(3000, MILLISECONDS); 

    if (haveLock) { 
     log("Have lock: " + threadId); 
     Foo foo = map.get("key"); 
     log("foo=" + foo); 

     if (foo == null) { 
      log("New foo time! " + threadId); 
      foo = new Foo(); //foo is expensive to instance 
      map.put("key", foo); 

     } else 
      log("Found foo:" + threadId); 

     log("foo=" + foo); 
     sharedLock.release(); 

    } else 
     log("No lock acquired"); 
} 

gì dường như xảy ra là thế này:

Thread 1       Thread 2 
- request lock     - request lock 
- have lock      - blocked waiting for lock 
- get from map, nothing there 
- create new foo 
- place new foo in map 
- logs foo.toString() 
- release lock 
- exit method      - have lock 
            - get from map, NOTHING THERE!!! (Why not?) 
            - create new foo 
            - place new foo in map 
            - logs foo.toString() 
            - release lock 
            - exit method 

Vì vậy, đầu ra của tôi trông như thế này:

Have lock: 1  
foo=null 
New foo time! 1 
[email protected] 
Have lock: 2  
foo=null 
New foo time! 2 
[email protected]  

Chuỗi thứ hai không ngay lập tức nhìn thấy giá trị đặt! Tại sao? Trên hệ thống sản xuất của tôi, có nhiều chủ đề hơn và tôi chỉ thấy một luồng, luồng đầu tiên ngay sau chuỗi 1, có vấn đề.

Tôi thậm chí đã cố gắng thu hẹp mức đồng thời trên ConcurrentHashMap xuống 1, không phải là vấn đề. Ví dụ:

static ConcurrentHashMap map = new ConcurrentHashMap(32, 1); 

Tôi sẽ làm gì sai? Mong đợi của tôi? Hoặc là có một số lỗi trong mã của tôi (phần mềm thực sự, không phải ở trên) đang gây ra điều này? Tôi đã đi qua nó nhiều lần và chắc chắn 99% tôi đang xử lý khóa một cách chính xác. Tôi thậm chí không thể hiểu được một lỗi trong ConcurrentHashMap hoặc JVM. Vui lòng lưu tôi từ chính tôi.

chi tiết cụ thể Gorey mà có thể có liên quan:

  • quad-core 64-bit Xeon (DL380 G5)
  • RHEL4 (Linux mysvr 2.6.9-78.0.5.ELsmp #1 SMP ... x86_64 GNU/Linux)
  • Java 6 (build 1.6.0_07-b06, 64-Bit Server VM (build 10.0-b23, mixed mode))
+0

Hàm get của bạn có tham số được gọi là "sharedLock" nhưng bạn sử dụng thành viên có tên "lock" ("sharedLock" không được sử dụng). Điều này có chủ ý không? – Arne

+0

Đây là điểm tôi bị hoang tưởng. Ngoài việc ghi nhật ký foo cuối cùng trước khi nhả khóa, hãy đăng nhập map.get ("key") và mã băm nhận dạng cho bản đồ. Khi đọc nhanh, chắc chắn mọi thứ đều ok và trên thực tế, khóa phải đủ nếu bản đồ không bị sửa đổi ở nơi khác. Có lẽ có điều gì đó quan trọng đã không làm cho nó thông qua việc tước bỏ câu hỏi? PSpeed

+0

@Arne: không, chỉ là lỗi đánh máy. Sẽ sửa chữa. –

Trả lời

6

Một số câu trả lời hay ở đây, nhưng theo như tôi có thể nói không ai thực sự đã cung cấp câu trả lời chính xác cho câu hỏi được hỏi: "ConcurrentHashMap.get() có được xem ConcurrentHashMap.put() trước đó không? ". Những người đã nói có không cung cấp một nguồn.

Vì vậy: có, nó được đảm bảo. Source (xem phần 'Memory quán Properties'):

Hoạt động trong một thread trước khi đặt một đối tượng vào bất kỳ bộ sưu tập đồng thời xảy ra-trước khi hành động tiếp theo để truy cập hoặc loại bỏ các yếu tố đó từ bộ sưu tập trong chủ đề khác .

+1

Tôi không nghĩ rằng câu trả lời cho câu hỏi.Tôi giải thích các báo giá nói rằng nếu bạn làm một đặt, mọi thứ khác các chủ đề sẽ có được thực hiện trước khi đưa, cũng sẽ được hiển thị cho các chủ đề làm cho có được bởi thời gian có được thành công. (Nhận được vẫn có thể tìm thấy một giá trị null hoặc cũ, miễn là không có gì khác đặt thread đã làm được hiển thị để có được thread). IOW, không có đảm bảo thời gian nghiêm ngặt nhưng có một bảo đảm đặt hàng tương đối. NHƯNG ví dụ OPs không nên bị ảnh hưởng bởi việc đặt hàng .. – jasonk

0

Chúng ta có thấy một biểu hiện thú vị của Mô hình bộ nhớ Java không? Dưới những điều kiện nào là các thanh ghi được xả lên bộ nhớ chính? Tôi nghĩ rằng nó được đảm bảo rằng nếu hai luồng đồng bộ trên cùng một đối tượng thì chúng sẽ thấy một khung nhìn bộ nhớ nhất quán.

Tôi không biết Semphore làm gì trong nội bộ, nó gần như rõ ràng là phải làm một số đồng bộ hóa, nhưng chúng ta có biết điều đó không?

gì xảy ra nếu bạn làm

synchronize(dedicatedLockObject) 

thay vì aquiring semaphore?

+0

'Foo' quá tốn kém để tạo (* giây! *) Mà tôi đảm bảo 99,9999% là OK - tôi thấy rằng chuỗi 2 không nhập logic của nó cho đến sau khi chuỗi 1 đã phát hành khóa. Và 'Semaphore' là, sau khi tất cả, trong 'java.util.concurrent' ... nó thực sự nên được tốt. –

+0

Tôi sẽ thử đề xuất của bạn mặc dù tối nay. Một lợi ích chính của Semaphore là nó lần ra và tôi có thể thoát ra. Điều này đang xảy ra trong yêu cầu webapp và việc khóa một chuỗi vĩnh viễn có thể làm hỏng máy chủ. Xấu. * Baaaaaad. * –

+0

Câu hỏi đặt ra là liệu những sổ đăng ký "CPU" cho luồng 1 có bị xóa hay không, chỉ những đảm bảo mà tôi có thể tìm thấy là khi đồng bộ được sử dụng. Tôi đồng ý rằng sẽ thật đáng ngạc nhiên nếu đây là vấn đề, nhưng chúng ta còn gì nữa? Bạn có thể sử dụng chờ/thông báo để thực hiện thời gian chờ. – djna

2

Tôi không nghĩ rằng sự cố nằm trong "ConcurrentHashMap" nhưng đúng hơn là ở đâu đó trong mã của bạn hoặc về lý do về mã của bạn. Tôi không thể phát hiện lỗi trong đoạn mã trên (có thể chúng ta không thấy phần xấu?).

Nhưng để trả lời câu hỏi của bạn "ConcurrentHashMap.get() có được đảm bảo xem ConcurrentHashMap.put() trước đó bằng chuỗi khác không?" Tôi đã cùng nhau tấn công một chương trình thử nghiệm nhỏ.

Tóm tắt: Không, ConcurrentHashMap là OK!

Nếu bản đồ được viết kém, chương trình sau sẽ in "Truy cập xấu!" ít nhất là theo thời gian. Nó ném 100 Chủ đề với 100000 cuộc gọi đến phương pháp bạn đã nêu ở trên. Nhưng nó in "Tất cả ok!"

import java.util.ArrayList; 
import java.util.List; 
import java.util.concurrent.Callable; 
import java.util.concurrent.ConcurrentHashMap; 
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
import java.util.concurrent.Semaphore; 
import java.util.concurrent.TimeUnit; 

public class Test { 
    private final static ConcurrentHashMap<String, Test> map = new ConcurrentHashMap<String, Test>(); 
    private final static Semaphore lock = new Semaphore(1); 
    private static int counter = 0; 

    public static void main(String[] args) throws InterruptedException { 
     ExecutorService pool = Executors.newFixedThreadPool(100); 
     List<Callable<Boolean>> testCalls = new ArrayList<Callable<Boolean>>(); 
     for (int n = 0; n < 100000; n++) 
      testCalls.add(new Callable<Boolean>() { 
       @Override 
       public Boolean call() throws Exception { 
        doSomething(lock); 
        return true; 
       } 
      }); 
     pool.invokeAll(testCalls); 
     pool.shutdown(); 
     pool.awaitTermination(5, TimeUnit.SECONDS); 
     System.out.println("All ok!"); 
    } 

    static void doSomething(Semaphore lock) throws InterruptedException { 
     boolean haveLock = lock.tryAcquire(3000, TimeUnit.MILLISECONDS); 

     if (haveLock) { 
      Test foo = map.get("key"); 
      if (foo == null) { 
       foo = new Test(); 
       map.put("key", new Test()); 
       if (counter > 0) 
        System.err.println("Bad access!"); 
       counter++; 
      } 
      lock.release(); 
     } else { 
      System.err.println("Fail to lock!"); 
     } 
    } 
} 
+0

Máy thử nghiệm của bạn có bao nhiêu lõi? Cảm ơn vì đã viết cái này lên. Tôi sẽ chạy nó trên máy sản xuất của tôi tối nay. –

+0

Máy của tôi có hai lõi. – Arne

+1

Kiểm tra mã đa luồng như thế này không hoạt động. Tất cả các lần lặp lại có khả năng chạy với chính xác cùng một luồng xen kẽ. –

1

Cập nhật:putIfAbsent() là logic đúng ở đây, nhưng không tránh được những vấn đề chỉ tạo ra một Foo trong trường hợp quan trọng là không có mặt. Nó luôn tạo ra Foo, ngay cả khi nó không kết thúc đưa nó vào bản đồ. Câu trả lời của David Roussel là tốt, giả sử bạn có thể chấp nhận sự phụ thuộc vào Bộ sưu tập của Google trong ứng dụng của bạn.


Có thể tôi đang thiếu điều gì đó hiển nhiên, nhưng tại sao bạn bảo vệ bản đồ bằng Semaphore? ConcurrentHashMap (CHM) là chủ đề an toàn (giả sử nó được xuất bản an toàn, ở đây). Nếu bạn đang cố gắng để có được nguyên tử "đặt nếu không có trong đó", sử dụng chm. putIfAbsent(). Nếu bạn cần thêm các bất biến được biên dịch, trong đó nội dung bản đồ không thể thay đổi, bạn có thể cần sử dụng HashMap thông thường và đồng bộ hóa nó như bình thường.

Để trả lời câu hỏi của bạn trực tiếp hơn: Sau khi bạn gửi trả lại, giá trị bạn đặt trong bản đồ được đảm bảo để xem chuỗi tiếp theo tìm kiếm câu hỏi đó.

Lưu ý phụ, chỉ +1 cho một số nhận xét khác về việc đưa bản phát hành semaphore vào cuối cùng.

if (sem.tryAcquire(3000, TimeUnit.MILLISECONDS)) { 
    try { 
     // do stuff while holding permit  
    } finally { 
     sem.release(); 
    } 
} 
+0

Tôi không bảo vệ bản đồ, nhưng là chìa khóa cho bản đồ. Semaphore là chìa khóa cụ thể. '.putIfAbsent()' không làm việc cho tôi - việc tạo đối tượng là tốn kém. –

+0

@Stu: Không chắc tôi theo dõi. Trong ví dụ trên, các phím trong bản đồ chỉ là các chuỗi (không thay đổi). Ngay cả khi họ không ở trong mã thực sự, bạn chỉ cần sử dụng chìa khóa để tìm một cái gì đó (phải không?), Vì vậy tôi không ngay lập tức thấy lý do tại sao chính nó cần một người bảo vệ. – overthink

+0

Có mối quan hệ 1: 1 giữa khóa và ví dụ Foo đắt tiền. Tôi không muốn tạo thêm một cá thể Foo cùng một lúc. I * do * muốn các cá thể Foo mới được tạo cho các khóa khác, đồng thời nếu cần. Tôi không muốn serialize tất cả các cá thể tạo Foo cho bất kỳ khóa nào. –

10

Vấn đề tạo đối tượng khó tạo trong bộ nhớ cache dựa trên sự cố không tìm thấy trong bộ nhớ cache đã được biết. Và may mắn thay điều này đã được thực hiện.

Bạn có thể sử dụng MapMaker từ Google Collecitons. Bạn chỉ cần cung cấp cho nó một cuộc gọi lại tạo đối tượng của bạn và nếu mã khách hàng nhìn vào bản đồ và bản đồ trống, cuộc gọi lại được gọi và kết quả được đưa vào bản đồ.

Xem MapMaker javadocs ...

ConcurrentMap<Key, Graph> graphs = new MapMaker() 
     .concurrencyLevel(32) 
     .softKeys() 
     .weakValues() 
     .expiration(30, TimeUnit.MINUTES) 
     .makeComputingMap(
      new Function<Key, Graph>() { 
      public Graph apply(Key key) { 
       return createExpensiveGraph(key); 
      } 
      }); 

BTW, trong ví dụ ban đầu của bạn không có lợi thế để sử dụng một ConcurrentHashMap, như bạn đang khóa từng truy cập, tại sao không chỉ cần sử dụng một HashMap bình thường bên trong phần khóa của bạn?

+0

Semaphore là khóa bản đồ cụ thể, không phải bản đồ cụ thể. Đây là lần thứ ba ai đó đưa ra vấn đề ... sẽ sửa đổi câu trả lời để làm rõ điều này. –

+1

Ok, tôi hiểu rồi. MapMaker vẫn nên làm việc cho bạn. –

+0

Bạn có thể giải thích thêm về "lỗi tìm thấy trong bộ nhớ đệm là một vấn đề đã biết", trong ví dụ của anh ta nói rõ khi anh ta truy cập bộ nhớ cache nó là null. Tôi muốn biết thêm về kịch bản này, hoặc chỉ cho tôi một hướng để tìm hiểu thêm. – reccles

3

Một điều cần xem xét là liệu các khóa của bạn có bằng nhau không và có mã băm giống nhau ở cả hai lần gọi "nhận". Nếu chúng chỉ là String thì có, sẽ không có vấn đề gì ở đây. Nhưng như bạn đã không đưa ra các loại chung của các phím, và bạn đã elided "không quan trọng" chi tiết trong mã giả, tôi tự hỏi nếu bạn đang sử dụng một lớp học như là một chìa khóa.

Trong mọi trường hợp, bạn có thể muốn ghi lại mã băm của các khóa được sử dụng để lấy/đặt trong chủ đề 1 và 2. Nếu chúng khác nhau, bạn có vấn đề. Cũng lưu ý rằng key1.equals(key2) phải đúng; Đây không phải là thứ bạn có thể đăng nhập một cách dứt khoát, nhưng nếu các khóa không phải là các lớp cuối cùng, nó sẽ đáng để ghi tên lớp đầy đủ của chúng, sau đó xem phương thức equals() cho lớp/lớp đó để xem liệu có thể chìa khóa thứ hai có thể được coi là không bình đẳng với lần đầu tiên.

Và để trả lời tiêu đề của bạn - có, ConcurrentHashMap.get() được đảm bảo để xem bất kỳ lần đặt trước nào(), trong đó "trước" có nghĩa là có mối quan hệ xảy ra trước giữa hai được chỉ định bởi Mô hình bộ nhớ Java. (Đối với ConcurrentHashMap nói riêng, đây thực chất là những gì bạn mong đợi, với thông báo trước rằng bạn có thể không biết được điều gì xảy ra trước tiên nếu cả hai luồng thực thi tại cùng một thời điểm trên các lõi khác nhau. , bạn chắc chắn sẽ thấy kết quả của put() trong chuỗi 2).

+0

Tôi chắc rằng các phím này là tốt vì tôi sử dụng cùng một khóa chính xác cho bộ đệm ẩn Semaphore, và tôi chắc chắn 100% * rằng * đang hoạt động chính xác. Cảm ơn, mặc dù. –

0

Tại sao bạn khóa bản đồ băm đồng thời? Bởi def. chủ đề của nó an toàn. Nếu có vấn đề, nó nằm trong mã khóa của bạn. Đó là lý do tại sao chúng tôi có các gói an toàn chỉ trong Java Cách tốt nhất để gỡ lỗi này là đồng bộ hóa hàng rào.

3

Nếu chuỗi đặt giá trị trong bản đồ băm đồng thời thì một số chuỗi khác truy lục giá trị của bản đồ được đảm bảo để xem giá trị được chèn bởi chuỗi trước đó.

Vấn đề này đã được làm rõ trong "Java Concurrency in Practice" của Joshua Bloch.

Trích dẫn từ văn bản: -

Các bộ sưu tập thư viện thread-safe cung cấp đảm bảo công bố an toàn sau đó, ngay cả khi javadoc là ít hơn rõ ràng về đề tài này:

  • Đặt một chìa khóa hoặc giá trị trong một Hashtable, synchronizedMap hoặc Concurrent-Map xuất bản an toàn cho bất kỳ chủ đề nào khác để truy lục nó từ Bản đồ (cho dù trực tiếp hoặc thông qua một trình lặp);
Các vấn đề liên quan