2009-11-26 6 views
55

Java WeakHashMap thường được trích dẫn là hữu ích cho bộ nhớ đệm. Nó có vẻ kỳ quặc mặc dù tài liệu tham khảo yếu của nó được định nghĩa trong điều khoản của các phím của bản đồ, không phải giá trị của nó. Ý tôi là, đó là các giá trị tôi muốn lưu vào bộ nhớ cache và tôi muốn lấy rác khi không có ai khác ngoài bộ nhớ đệm đang tham chiếu mạnh mẽ, phải không?WeakHashMap của Java và lưu vào bộ nhớ đệm: Tại sao nó tham chiếu các khóa, chứ không phải các giá trị?

Có cách nào để giữ tham chiếu yếu cho khóa? Nếu bạn làm một ExpensiveObject o = weakHashMap.get("some_key"), thì tôi muốn bộ nhớ cache giữ 'o' cho đến khi người gọi không giữ tham chiếu mạnh mẽ nữa, và tôi không quan tâm chút nào về đối tượng chuỗi "some_key".

Tôi có thiếu gì đó không?

+0

Java API là đầy đủ trong những thói quen kỳ lạ. Bạn luôn có thể viết lại WeakHashMap bằng WeakReference. – Pacerier

Trả lời

98

WeakHashMap không phải là hữu ích như bộ nhớ cache, ít nhất là cách hầu hết mọi người nghĩ về nó. Như bạn nói, nó sử dụng các phím yếu yếu, không yếu giá trị, vì vậy nó không được thiết kế cho hầu hết mọi người muốn sử dụng nó (và, trên thực tế, tôi đã thấy mọi người sử dụng nó không đúng).

WeakHashMap chủ yếu hữu ích để giữ siêu dữ liệu về các đối tượng có vòng đời bạn không kiểm soát. Ví dụ, nếu bạn có một loạt các đối tượng đi qua lớp của bạn, và bạn muốn theo dõi các dữ liệu bổ sung về chúng mà không cần phải được thông báo khi chúng nằm ngoài phạm vi và không có tham chiếu đến chúng.

Một ví dụ đơn giản (và một Tôi đã sử dụng trước đó) có thể là một cái gì đó như:

WeakHashMap<Thread, SomeMetaData> 

nơi bạn có thể theo dõi những gì chủ đề khác nhau trong hệ thống của bạn đang làm; khi chuỗi chết, mục nhập sẽ bị xóa khỏi bản đồ của bạn và bạn sẽ không giữ cho Chủ đề bị thu gom rác nếu bạn là tham chiếu cuối cùng của nó. Sau đó bạn có thể lặp qua các mục trong bản đồ đó để tìm hiểu siêu dữ liệu nào bạn có về các chủ đề đang hoạt động trong hệ thống của mình.

Xem WeakHashMap in not a cache! để biết thêm thông tin.

Đối với loại bộ nhớ cache bạn đang sử dụng, hãy sử dụng hệ thống bộ nhớ cache chuyên dụng (ví dụ: EHCache) hoặc xem google-collections'MapMaker class; cái gì đó như

new MapMaker().weakValues().makeMap(); 

sẽ làm những gì bạn đang sau, hoặc nếu bạn muốn nhận được ưa thích bạn có thể thêm hết hạn theo thời gian:

new MapMaker().weakValues().expiration(5, TimeUnit.MINUTES).makeMap(); 
+4

Chỉ cần cập nhật tính năng này cho tháng 8 năm 2013: Google Collections hiện được đặt tên là Guava và logic tạo bộ nhớ cache hiện là một phần của [CacheBuilder] (http://docs.guava-libraries.googlecode.com/git-history/release/ javadoc/index.html). –

+0

Liên kết chính xác hơn: http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/cache/CacheBuilder.html –

+1

Lưu ý, tôi nghĩ trong ví dụ của bạn cho MapMaker bạn là có nghĩa là để nói MapMaker mới(). softValues ​​(). makeMap(), như gọi weakValues ​​() cho bạn kết quả tương tự như một WeakHashMap. Có một ví dụ tuyệt vời ở đây về cách tạo bộ nhớ cache với MapMaker - http://stackoverflow.com/questions/3737140/use-of-google-collections-mapmaker – jklp

30

Việc sử dụng chính cho WeakHashMap là khi bạn có ánh xạ mà bạn muốn biến mất khi chìa khóa của họ biến mất. Bộ nhớ cache là ngược lại --- bạn có ánh xạ mà bạn muốn biến mất khi giá trị của chúng biến mất.

Để có bộ nhớ cache, nội dung bạn muốn là Map<K,SoftReference<V>>. A SoftReference sẽ được thu gom rác khi bộ nhớ bị siết chặt. (Tương phản với WeakReference, có thể bị xóa ngay sau khi không còn tham chiếu cứng nhắc đến tham chiếu của nó nữa.) Bạn muốn tham chiếu của mình trở nên mềm trong bộ nhớ cache (ít nhất là trong một ánh xạ khóa-giá trị không đi cũ), kể từ đó có khả năng là các giá trị của bạn sẽ vẫn ở trong bộ nhớ cache nếu bạn tìm kiếm chúng sau này. Nếu các tham chiếu yếu, thay vào đó, các giá trị của bạn sẽ được gc'd ngay lập tức, đánh bại mục đích của bộ nhớ đệm.

Để thuận tiện, bạn có thể muốn ẩn các giá trị SoftReference bên trong việc triển khai Map để bộ nhớ cache của bạn có vẻ là loại <K,V> thay vì <K,SoftReference<V>>. Nếu bạn muốn làm điều đó, this question có các đề xuất cho việc triển khai có sẵn trên mạng. cũng

Lưu ý rằng khi bạn sử dụng SoftReference giá trị trong một Map, bạn phải làm điều gì đó để loại bỏ cặp khóa-giá trị đó đã SoftReferences họ xóa --- khác Map của bạn sẽ bị rò rỉ bộ nhớ.

+0

sử dụng giải pháp này theo thời gian để lại cho bạn nhiều mục hashmap giá trị đã được gc-ed. là có một sự thay thế sử dụng một cách tiếp cận tương tự? –

+0

Phương thức 'Bản đồ >' để lại các trường hợp của 'SoftReference' trong bản đồ chứa các tham chiếu' null' sau khi GC đã chạy. Tôi nghĩ rằng việc thực hiện nội bộ của bản đồ này phải định kỳ xóa tất cả các ánh xạ với một giá trị là một tham chiếu mềm đang nắm giữ một tham chiếu 'null', để dọn dẹp tốt đẹp. – Timmos

+2

(tiếp tục) Nói đúng ra, nếu một lập trình viên ngây thơ sử dụng việc thực hiện 'HashMap >' thì điều này sẽ gây ra rò rỉ bộ nhớ. Bạn có thể xem xét để bao gồm điều này trong câu trả lời của bạn. Có một cái nhìn như thế nào 'WeakHashMap' hiện nó, Oracle JDK có một phương pháp riêng' expungeStaleEntries' trong nó mà sẽ chăm sóc này dọn dẹp. – Timmos

6

Một điều cần lưu ý là nếu bạn thực hiện phương pháp Map<K, WeakReference<V>>, giá trị có thể biến mất, nhưng ánh xạ sẽ không. Tùy thuộc vào cách sử dụng, bạn có thể kết quả với Bản đồ chứa nhiều mục có Tham chiếu yếu đã được GC.

+0

'Bản đồ ', không phải 'Bản đồ >'. Câu trả lời này dường như có ý nghĩa trên bề mặt, nhưng lưu ý các ánh xạ mất tích có thể được loại bỏ mỗi khi người dùng gọi 'Map.get', và thấy rằng [đây chính là cách WeakHashMap loại bỏ khóa] (http://archive.is/ Z3aK9 # selection-1069.117-1069.237), nó không thể được rằng đội Java đã không nhận ra điều này. – Pacerier

6

Bạn cần hai bản đồ: bản đồ giữa khóa bộ nhớ cache và các giá trị weak referenced và một trong ánh xạ hướng đối diện giữa các giá trị tham chiếu yếu và các phím. Và bạn cần một reference queue và một chuỗi dọn dẹp.

Tham chiếu yếu có khả năng di chuyển tham chiếu vào hàng đợi khi đối tượng được tham chiếu không thể truy cập được nữa. Hàng đợi này phải được rút ra bởi một luồng dọn dẹp. Và để dọn dẹp, cần phải lấy chìa khóa để tham khảo. Đây là lý do tại sao bản đồ thứ hai là bắt buộc.

Ví dụ sau đây cho thấy cách tạo bộ nhớ cache với bản đồ băm tham chiếu yếu. Khi bạn chạy chương trình bạn sẽ có được kết quả như sau:

 
$ javac -Xlint:unchecked Cache.java && java Cache 
{even: [2, 4, 6], odd: [1, 3, 5]} 
{even: [2, 4, 6]} 

Dòng đầu tiên cho thấy các nội dung của bộ nhớ cache trước khi tham chiếu đến danh sách lẻ đã bị xóa và dòng thứ hai sau khi tỷ lệ cược đã bị xóa.

Đây là mã:

import java.lang.ref.Reference; 
import java.lang.ref.ReferenceQueue; 
import java.lang.ref.WeakReference; 
import java.util.Arrays; 
import java.util.Collections; 
import java.util.HashMap; 
import java.util.List; 
import java.util.Map; 

class Cache<K,V> 
{ 
    ReferenceQueue<V> queue = null; 
    Map<K,WeakReference<V>> values = null; 
    Map<WeakReference<V>,K> keys = null; 
    Thread cleanup = null; 

    Cache() 
    { 
     queue = new ReferenceQueue<V>(); 
     keys = Collections.synchronizedMap (new HashMap<WeakReference<V>,K>()); 
     values = Collections.synchronizedMap (new HashMap<K,WeakReference<V>>()); 
     cleanup = new Thread() { 
       public void run() { 
        try { 
         for (;;) { 
          @SuppressWarnings("unchecked") 
          WeakReference<V> ref = (WeakReference<V>)queue.remove(); 
          K key = keys.get(ref); 
          keys.remove(ref); 
          values.remove(key); 
         } 
        } 
        catch (InterruptedException e) {} 
       } 
      }; 
     cleanup.setDaemon (true); 
     cleanup.start(); 
    } 

    void stop() { 
     cleanup.interrupt(); 
    } 

    V get (K key) { 
     return values.get(key).get(); 
    } 

    void put (K key, V value) { 
     WeakReference<V> ref = new WeakReference<V>(value, queue); 
     keys.put (ref, key); 
     values.put (key, ref); 
    } 

    public String toString() { 
     StringBuilder str = new StringBuilder(); 
     str.append ("{"); 
     boolean first = true; 
     for (Map.Entry<K,WeakReference<V>> entry : values.entrySet()) { 
      if (first) 
       first = false; 
      else 
       str.append (", "); 
      str.append (entry.getKey()); 
      str.append (": "); 
      str.append (entry.getValue().get()); 
     } 
     str.append ("}"); 
     return str.toString(); 
    } 

    static void gc (int loop, int delay) throws Exception 
    { 
     for (int n = loop; n > 0; n--) { 
      Thread.sleep(delay); 
      System.gc(); // <- obstinate donkey 
     } 
    } 

    public static void main (String[] args) throws Exception 
    { 
     // Create the cache 
     Cache<String,List> c = new Cache<String,List>(); 

     // Create some values 
     List odd = Arrays.asList(new Object[]{1,3,5}); 
     List even = Arrays.asList(new Object[]{2,4,6}); 

     // Save them in the cache 
     c.put ("odd", odd); 
     c.put ("even", even); 

     // Display the cache contents 
     System.out.println (c); 

     // Erase one value; 
     odd = null; 

     // Force garbage collection 
     gc (10, 10); 

     // Display the cache again 
     System.out.println (c); 

     // Stop cleanup thread 
     c.stop(); 
    } 
} 
+1

Câu trả lời hay. Đáng chú ý là một ReferenceQueue, không giống như nhiều loại bộ sưu tập khác, khối cho đến khi một giá trị có thể được trả về từ queue.remove(). Điều này có nghĩa là luồng dọn dẹp không phải là vòng lặp vô hạn không chờ đợi mà cái nhìn đầu tiên có thể gợi ý. –

+0

@Gordon Nếu bạn sử dụng các phím yếu và các giá trị yếu, mọi thứ yếu và sẽ thu thập rác ngay sau khi bạn thêm nó vào bộ đệm. – ceving

+0

** Đây là câu trả lời **. Vì vậy, chúng tôi có thể nói rằng vì vậy nó cho ** tối ưu hóa giai đoạn dọn dẹp ** mà API được thực hiện như vậy. – Pacerier