前言
声明,本文用的是jdk1.8
前面章节回顾:
之前介绍了HashMap,也说到它是线程不安全的,那么如何在多线程环境下使用呢?
接下来我们介绍并发安全的ConcurrentHashMap:
可以看出ConcurrentHashMap继承了AbstractMap,这是一个java.util下的抽象类,如果我们需要自己来实现一个Map,一般是继承AbstractMap
ConcurrentHashMap实现了ConcurrentMap这个接口,这个接口是在jdk1.5引入的,这个接口提供了一些对于Map的原子操作
public interface ConcurrentMap<K, V> extends Map<K, V> {}
一、ConcurrentHashMap解析
ConcurrentHashMap定义:
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable { }
****ConcurrentHashMap简介:
-
ConCurrentHashMap的底层是:散列表+红黑树,与HashMap是一样的。ConCurrentHashMap支持高并发的访问和更新,它是线程安全的。
-
检索操作不用加锁,get方法是非阻塞的,其中key和value都不允许为null。
-
ConcurrentHashMap实现了ConcurrentMap这个接口,ConcurrentMap是在JDK1.5时随着J.U.C包引入的,这个接口其实就是提供了一些针对Map的原子操作。
基本结构介绍:ConcurrentHashMap内部维护了一个Node类型的数组,也就是table。数组的每一个位置table[i]代表了一个桶,当插入键值对时,会根据键的hash值映射到不同的桶位置~
上图中,不同的桶用不同颜色表示,可以看到,有的桶链接着链表,有的桶链接着树,这也是JDK1.8中ConcurrentHashMap的特殊之处。简单介绍下五种节点和作用:
-
**TreeNode结点:**TreeNode就是红黑树的结点,TreeNode不会直接链接到table[i]——桶上面,而是由TreeBin链接,TreeBin会指向红黑树的根结点
-
**TreeBin结点:**TreeBin相当于TreeNode的代理结点。TreeBin会直接链接到table[i]——桶上面,该结点提供了一系列红黑树相关的操作,以及加锁、解锁操作
-
**ForwardingNode结点:**ForwardingNode结点仅仅在扩容时才会使用
-
**ReservationNode结点:**保留结点,ConcurrentHashMap中的一些特殊方法会专门用到该类结点
Jdk1.7底层实现:
上面说的的是JDK1.8底层是:散列表+红黑树,但是JDK1.7的底层是:segments+HashEntry数组
Segment继承了ReentrantLock,每个片段都有了一个锁,叫做“锁分段”。ConcurrentHashMap将哈希表分成许多片段(segments),每一个片段(table)都类似于HashMap,它有一个HashEntry数组,数组的每项又是HashEntry组成的链表。每个片段都是Segment类型的,Segment继承了ReentrantLock,所以Segment本质上是一个可重入的互斥锁。
这样每个片段都有了一个锁,这就是“锁分段”。线程如想访问某一key-value键值对,需要先获取键值对所在的segment的锁,获取锁后,其他线程就不能访问此segment了,但可以访问其他的segment。
感兴趣的可以自行研究,不多介绍,接下来重点分析1.8~
****JDK1.8下ConcurrentHashMap源码分析:
在介绍这个之前,先简单介绍下CAS和Volatile~
CAS,即Compare and swap, 是一种有名的乐观锁算法,CAS有3个操作数:
-
旧的预期值A
-
要修改的新值B
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值(A和内存值V相同时,将内存值V修改为B),而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试(否则什么都不做)。
其实简单一句话概括就是:先比较是否相等,如果相等则替换(CAS算法)。
CAS算法中有一个ABA问题:如果线程1开始记录预期值A,中间A经过了A->B->A这样一个过程,而线程A是不知道的,它认为变量A是未被修改的,而实际上是修改过的。
我们可以通过加时间戳或者版本号等方法来防止ABA问题
接下来我们看看volatile关键字,可能很多人已经了解volatile关键字的作用,这也是面试常问的点之一。
volatile经典总结:volatile仅仅用来保证该变量对所有线程的可见性,但不保证原子性。
我们将其拆开来解释一下:
-
保证该变量对所有线程的可见性:在多线程的环境下:当这个变量修改时,所有的线程都会知道该变量被修改了,也就是所谓的“可见性”
-
不保证原子性:修改变量(赋值)实质上是在JVM中分了好几步,而在这几步内(从装载变量到修改),它是不安全的
看下ConcurrentHashMap的域对象~
构造函数
可以发现,在构造方法中有几处都调用了tableSizeFor(),我们来看一下他是干什么的:
哎,怎么如此熟悉?哦,对了!这不就是HashMap中用来获取大于参数且最接近2的整次幂的数,赋值给sizeCtl属性也就说明了:这是下次扩容的大小。
put()方法:放入元素
结构也和HashMap的put方法类似,点进去**putVal()**看下~
get()方法:不用加锁,非阻塞的
2、ConcurrentHashMap重点关注问题
ConcurrentHashMap的特点概况?
-
Hashtable是将所有的方法进行同步,效率低下。而ConcurrentHashMap作为一个高并发的容器,它是通过部分锁定+CAS算法来进行实现线程安全的。CAS算法也可以认为是乐观锁的一种。
-
get方法是非阻塞,无锁的。重写Node类,通过volatile修饰next来实现每次获取都是最新设置的值,ConcurrentHashMap的key和Value都不能为null。
-
size方法是读取baseCount和CounterCell数据的总数量,因为是并发的也不一定正确。
-
ConcurrentHashMap的弱一致性主要表现在他的一些视图和迭代器上,通过迭代器遍历元素的时候如果之前的元素发生修改是不会抛出fail-fast异常的,后面的元素如果修改了会体现在迭代器遍历的结果上。
ConcurrentHashMap扩容基本思路?
Hash表的扩容,一般都包含两个步骤:
-
数据迁移,就是把旧table中的各个槽中的结点重新分配到新table中。比如,单线程情况下,可以遍历原来的table,然后put到新table中。这一过程通常涉及到槽中key的rehash,因为key映射到桶的位置与table的大小有关,新table的大小变了,key映射的位置一般也会变化。
扩容时机问题
其实ConcurrentHashMap的扩容时机和HashMap的扩容机制类似,并不是说链表长度大于8就一定扩容,而是对table数组的长度进行一次判断,如果table长度小于阈值MIN_TREEIFY_CAPACITY——默认64,则会调用tryPresize方法把数组长度扩大到原来的两倍。
3、IdentityHashMap
简单说常用的HashMap和IdentityHashMap的区别是:前者比较key时是“引用相等”而后者是“对象相等”,即对于k1和k2,当k1==k2时,IdentityHashMap认为两个key相等,而HashMap只有在k1.equals(k2) == true 时才会认为两个key相等。
IdentityHashMap 允许使用null作为key和value,不保证任何Key-value对的之间的顺序,更不能保证他们的顺序随时间的推移不会发生变化。
IdentityHashMap有其特殊用途,比如序列化,深度复制或者记录对象代理。举个例子~
举个例子,jvm中的所有对象都是独一无二的,哪怕两个对象是同一个class的对象,而且两个对象的数据完全相同,对于jvm来说,他们也是完全不同的,如果要用一个map来记录这样jvm中的对象,你就需要用IdentityHashMap,而不能使用其他Map实现。
四、弱键WeakHashMap
在jvm中,一个对象如果不再被使用就会被当做垃圾给回收掉,判断一个对象是否是垃圾,通常有两种方法:引用计数法和可达性分析法。不管是哪一种方法判断一个对象是否是垃圾的条件总是一个对象的引用是都没有了。
JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用、软引用、弱引用、虚引用4 种。而我们的WeakHashMap就是基于弱引用。
1、强引用:类似于 Object obj = new Object(); 创建的,只要强引用在垃圾回收器就不回收。
2、弱引用:用于描述一些还有用但非必需的对象。用SoftReference 类实现软引用,在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。若回收完还是没有足够的内存才会抛出内存溢出正常。
3、软引用:用来描述非必需对象的,强度比软引用更弱一些。用WeakReference 类实现弱引用,对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。
4、虚幻引用:幽灵引用或者幻影引用(很魔幻的名称)。它是一种最弱的引用关系,一个对象的虚引用不会对生存时间构成影响、也无法通过虚引用来取得对象实例。用PhantomReference 类实现虚引用,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
WeakHashMap定义:
public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> { }
****WeakHashMap简介:
-
WeakHashMap 特殊之处在于 WeakHashMap 里的entry可能会被垃圾回收器自动删除,也就是说即使你没有调用remove()或者clear()方法,它的entry也可能会慢慢变少。所以多次调用比如isEmpty,containsKey,size等方法时可能会返回不同的结果。
-
WeakHashMap中的key是间接保存在弱引用中的,所以当key没有被继续使用时,就可能会在GC的时候被回收掉。只有key对象是使用弱引用保存的,value对象实际上仍旧是通过普通的强引用来保持的,所以应该确保value不会直接或者间接的保持其对应key的强引用,因为这样会阻止key被回收。
-
数据结构是数组+链表的形式,这一点跟HashMap也是一致的,但不同的是,在JDK8中,当发生较多key冲突的时候,HashMap中会由链表转为红黑树,而WeakHashMap则一直使用链表进行存储。
WeakHashMap中的Entry为什么会自动被回收?
大家都知道HashMap实现里面有个Entry数组,WeakHashMap也一样也有一个Entry数组,但是此Entry与彼Entry有些不一样。WeakHashMap的Entry是继承WeakReference,这样一来,整个Entry就是一个WeakReference,再来看看Entry的构造方法,调用了super(key, queue),也就是调用了这个构造方法。
从这里我们可以看到其内部的Entry继承了WeakReference,也就是弱引用,所以就具有了弱引用的特点。不过还要注意一点,那就是ReferenceQueue,他的作用是GC会清理掉对象之后,引用对象会被放到ReferenceQueue中。
WeakHashMap中的Entry被GC后,WeakHashMap是如何将其移除的?
WeakHashMap内部有一个expungeStaleEntries函数,在这个函数内部实现移除其内部不用的entry从而达到的自动释放内存的目的。因此我们每次访问WeakHashMap的时候,都会调用这个expungeStaleEntries函数清理一遍。首先GC每次清理掉一个对象之后,引用对象会被放到ReferenceQueue中,后遍历这个queue进行删除即可。
WeakHashMap应用场景
1、由于WeakHashMap可以自动清除Entry,所以比较适合用于存储非必需对象,用作缓存非常合适。
2、WeakHashMap还有这样一个不错的应用场景,配合事务进行使用,存储事务过程中的各类信息。
结构:WeakHashMap<String,Map<K,V>> transactionCache,这里key为String类型,可以用来标志区分不同的事务,起到一个事务id的作用。value是一个map,可以是一个简单的HashMap或者LinkedHashMap,用来存放在事务中需要使用到的信息。
在事务开始时创建一个事务id,并用它来作为key,事务结束后,将这个强引用消除掉,这样既能保证在事务中可以获取到所需要的信息,又能自动释放掉map中的所有信息。
絮叨叨
你知道的越多,你不知道的也越多。
建议:多读书,多看视频,少吃零食,多撸代码