WeakHashMap

前言:
WeakHashMap这个类我看了好久,一直不知道怎么写,有两点原因。第一:我怕把他写简单了,光从WeakHashMap的功能实现是描述,他和HashMap等非常的相似,无非也是用来hash表+单向链表的结构作为底层数据存储,再写一遍没太大意思。 第二:WeakHashMap的特点是以一种弱引用的关系存储数据,存储对象长期不用,可以被垃圾回收。讲这部分内容非常有意思,但是关于jvm部分了解不深,又怕讲不好。因此托了很长时间没写。
权衡以后,决定先讲解WeakHashMap弱引用的实现原理,垃圾回收部分先缓一缓。

一、与HashMap异同

WeakHashMap的实现和HashMap非常类似,他们有相同的数据结构,类似的存储机制。数据底层都是通过hash表+单向链表的结构存储数据。
1.get
获取元素的流程非常类似,首先对key进行hash处理,通过hash值寻找到对应的元素位置。数组元素保存的是单向链表,这是hash冲突导致的结果。然后元每个entry对比key值是否相同,最终找到对应的元素。

2.put
添加元素的流程也和HashMap类似,首先对key值hash处理,通过hash值求出对应的元素位置,然后通过对比单向链表中的key值,将元素添加到链表头部。最后会更加当前元素的数量与threshold对比,进行动态扩容。扩容部分的原理也和HashMap类似。

3.getTable
getTable方法是WeakHashMap特有的,这个方法是干什么用的呢?因为我们知道WeakHashMap的元素是通过弱引用的关系存储的。在容器中,有部分元素长时间未用,会被垃圾回收,getTable的作用就是清除被垃圾回收的元素。源码如下:

  1. private Entry<K,V>[] getTable() {
  2. expungeStaleEntries();
  3. return table;
  4. }
  5. // 清除所有被垃圾回收的元素
  6. private void expungeStaleEntries() {
  7. // queue队列中保存的是已经被垃圾回收的元素key值。遍历队列
  8. for (Object x; (x = queue.poll()) != null; ) {
  9. synchronized (queue) {
  10. @SuppressWarnings("unchecked")
  11. Entry<K,V> e = (Entry<K,V>) x;
  12. int i = indexFor(e.hash, table.length);
  13. Entry<K,V> prev = table[i];
  14. Entry<K,V> p = prev;
  15. while (p != null) {
  16. Entry<K,V> next = p.next;
  17. if (p == e) {
  18. if (prev == e)
  19. table[i] = next;
  20. else
  21. prev.next = next;
  22. e.value = null; // 有助于垃圾回收
  23. size--;
  24. break;
  25. }
  26. prev = p;
  27. p = next;
  28. }
  29. }
  30. }
  31. }

从源码中可以看出,首先会遍历queue队列,队列中保存的元素是已经被垃圾回收的元素的key值。然后将该key从table中删除。这步方法在getTable,resize,size方法中使用到,并且在垃圾回收过程中也会使用,因此他是在多线程环境下调用的,所以该方法使用了同步方法,确保线程安全。
那么是谁将垃圾回收的后的元素放入queue中的呢?这就要从Entry的结构说起。

4.Entry结构介绍
WeakHashMap的Entry是Reference的子类。Entry实例化时会引用当前的queue,如果当前Entry被垃圾回收后,会将key注册的queue中。在后文中我会详细介绍Reference类。

Entry结构

  1. private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
  2. V value;
  3. final int hash;
  4. Entry<K,V> next;
  5. Entry(Object key, V value,
  6. ReferenceQueue<Object> queue,
  7. int hash, Entry<K,V> next) {
  8. super(key, queue);
  9. this.value = value;
  10. this.hash = hash;
  11. this.next = next;
  12. }
  13. ......
  14. }

二、弱键引用

上文简单介绍了WeakHashMap和HashMap的异同点,大同小异,不想过多重复。我认为WeakHashMap的重点是对元素进行垃圾回收的部分。下面将结合源码进行讲解。

  1. public abstract class Reference<T> {
  2. private T referent;
  3. // 将回收的元素添加到队列中。
  4. volatile ReferenceQueue<? super T> queue;
  5. @SuppressWarnings("rawtypes")
  6. Reference next;
  7. transient private Reference<T> discovered;
  8. static private class Lock { };
  9. private static Lock lock = new Lock();
  10. // 垃圾收集器将回收的引用加入队列
  11. private static Reference<Object> pending = null;
  12. private static class ReferenceHandler extends Thread {
  13. ReferenceHandler(ThreadGroup g, String name) {
  14. super(g, name);
  15. }
  16. public void run() {
  17. for (;;) {
  18. Reference<Object> r;
  19. synchronized (lock) {
  20. if (pending != null) {
  21. r = pending;
  22. pending = r.discovered;
  23. r.discovered = null;
  24. } else {
  25. try {
  26. try {
  27. lock.wait();
  28. } catch (OutOfMemoryError x) { }
  29. } catch (InterruptedException x) { }
  30. continue;
  31. }
  32. }
  33. // Fast path for cleaners
  34. if (r instanceof Cleaner) {
  35. ((Cleaner)r).clean();
  36. continue;
  37. }
  38. ReferenceQueue<Object> q = r.queue;
  39. if (q != ReferenceQueue.NULL) q.enqueue(r);
  40. }
  41. }
  42. }
  43. static {
  44. ThreadGroup tg = Thread.currentThread().getThreadGroup();
  45. for (ThreadGroup tgn = tg;
  46. tgn != null;
  47. tg = tgn, tgn = tg.getParent());
  48. Thread handler = new ReferenceHandler(tg, "Reference Handler");
  49. handler.setPriority(Thread.MAX_PRIORITY);
  50. handler.setDaemon(true);
  51. handler.start();
  52. }
  53. 。。。省略。。。
  54. }

Reference的实现原理:

第一步通过静态代码块启动ReferenceHandler线程,所有Reference实例共享该线程。pending会被虚拟机调用,当有元素被垃圾回收以后,会添加到该队列中。Reference线程通过无线循环检查pending是否存在元素。当发现有元素被垃圾回收以后,会将该元素添加到queue中。如果当前没有发现被回收的元素,也就是pending为null时,会通过lock.wait() 阻塞线程。直到有元素被回收以后,会调用nodify唤醒线程。
该过程涉及到多线程的知识,jvm垃圾回收的原理等,这部分比较复杂,暂时无法很好的讲解。将回收的元素加入到queue队列的部分实现用了消费者模式。为了读者更好的理解该原理,我对他进行模仿,实现了一个简单的消费者模式的demo。这个demo主要功能如下:
1.多个生产者不断的生产某件产品,当产品数量大于10以后就停止生产,只要当产品数量小于10,就会生产。
2.多个消费者不断的消费产品,知道消费结束。
3.当生产者生产到10件以后,就阻塞生产线,通知消费者消费。
4.当消费者消费完结束以后,阻塞消费,通知生产者生产。

  1. public class ProducerCustomerDemo {
  2. private static int index = 0;
  3. private static int size = 0;
  4. static private class Lock { };
  5. private static Lock lock = new Lock();
  6. private static Entry head = null;
  7. public synchronized int getSize() {
  8. return size;
  9. }
  10. public synchronized void addSize() {
  11. ProducerCustomerDemo.size++;
  12. }
  13. public synchronized void minusSize() {
  14. ProducerCustomerDemo.size--;
  15. }
  16. public synchronized int getIndex() {
  17. return index;
  18. }
  19. public synchronized void addIndex() {
  20. index++;
  21. }
  22. public synchronized void minusIndex() {
  23. index--;
  24. }
  25. public static void main(String[] args) {
  26. new Thread(new ProducerCustomerDemo().new Consumer("aaa")).start();
  27. new Thread(new ProducerCustomerDemo().new Consumer("bbb")).start();
  28. new Thread(new ProducerCustomerDemo().new Consumer("ccc")).start();
  29. new Thread(new ProducerCustomerDemo().new Consumer("ddd")).start();
  30. new Thread(new ProducerCustomerDemo().new Producer("张三")).start();
  31. new Thread(new ProducerCustomerDemo().new Producer("李四")).start();
  32. }
  33. class Entry {
  34. Entry next;
  35. int value;
  36. public Entry(int value) {
  37. this.value = value;
  38. }
  39. }
  40. class Consumer implements Runnable{
  41. private String name;
  42. public Consumer(String name) {
  43. this.name = name;
  44. }
  45. @Override
  46. public void run() {
  47. for (;;) {
  48. Entry temp = null;
  49. synchronized (lock) {
  50. if (head != null) {
  51. try {
  52. Thread.sleep(1000);
  53. } catch (InterruptedException e) {
  54. e.printStackTrace();
  55. }
  56. temp = head;
  57. head = temp.next;
  58. temp.next = null;
  59. minusSize();
  60. System.out.println("消费,当前产品数"+getSize()+"--"+name+":"+ temp.value);
  61. lock.notify();
  62. } else {
  63. try {
  64. lock.wait();
  65. } catch (InterruptedException e) {
  66. e.printStackTrace();
  67. }
  68. }
  69. }
  70. }
  71. }
  72. }
  73. class Producer implements Runnable {
  74. private String name;
  75. public Producer(String name) {
  76. this.name = name;
  77. }
  78. @Override
  79. public void run() {
  80. for (;;) {
  81. synchronized (lock) {
  82. if (getSize() < 10) {
  83. try {
  84. Thread.sleep(1000);
  85. } catch (InterruptedException e) {
  86. e.printStackTrace();
  87. }
  88. addIndex();
  89. Entry temp = new Entry(getIndex());
  90. temp.next = head;
  91. head = temp;
  92. addSize();
  93. System.out.println("生产,当前产品数"+getSize()+"--" + name + ":" + temp.value);
  94. lock.notify();
  95. } else {
  96. try {
  97. lock.wait();
  98. } catch (InterruptedException e) {
  99. e.printStackTrace();
  100. }
  101. }
  102. }
  103. }
  104. }
  105. }
  106. }