1:几个重要的常量定义 private static final int MAXIMUM_CAPACITY = 1 << 30; //map 容器的最大容量 private static final int DEFAULT_CAPACITY = 16; // map容器的默认大小 private static final float LOAD_FACTOR = 0.75f; //加载因子 static final int TREEIFY_THRESHOLD = 8; //由链表转为树状结构的链表长度 static final int UNTREEIFY_THRESHOLD = 6; //由树状结构转为链表 static final int MIN_TREEIFY_CAPACITY = 64; //数组长度最小为64才会转为红黑树 // 成员变量定义 transient volatile Node<K,V>[] table; //Node数组 用于存储元素 private transient volatile Node<K,V>[] nextTable; //当扩容的时候用于临时存储数组链表 private transient volatile long baseCount; //保存着哈希表所有节点的个数总和,相当于hash Map size private transient volatile int sizeCtl;
接下来分析几个关键的点:
1:第一次扩容的场景第一次初始化 map 中的table数组
2:在table 成员变量的 i 索引处添加元素 即table[i] 为空的时候添加元素
3:当table[i] 不为空的时候添加元素,即拉链法
4:扩容机制,是如何实现扩容的,如何保证线程安全,在扩容的时候如果这个时候其他线程执行 put和 get的时候会怎样,如何保证线程安全
下面进入几个关键点的具体分析:
在ConcurrentHashMap 进行初始化的时候只是执行一个空的构造方法,对成员变量中的值没有进行初始化操作 即table=null;
1:第一次扩容:当map第一次进行put操作的时候,成员变量table=null 这个时候会进行扩容操作,代码如下:
if (tab == null || (n = tab.length) == 0) tab = initTable();
下面主要看下,当线程 t1 进入initTable(); 的时候,这时线程 t2 也符合tab == null添加下则进入 initTable();方法,这个时候如何保证 t1,t2 扩容时候的线程安全;
保证方式:其实首次对map进行扩容的时候,即初始化table变量的时候,只需要保证第一个线程进入时进行初始化,其他线程无法执行即可。
这时通过CAS保证update只有一个线程成功即可。
下面看看 initTable() 这个方法的实现方案:
if ((sc = sizeCtl) < 0) // 初始化时为0 当为负数的时候线程 进入yield()方法 Thread.yield(); // yield()方法会通知线程调度器放弃对处理器的占用,当前线程放弃执行权
当t1 是第一次获(sc = sizeCtl) < 0进入这个判断的时候 sizeCtl=0 是不会进入线程放弃执行权的,这时会进入以下的逻辑
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //通过CAS方法将sizeCtl的值更新为-1,这时一个原子操作,只有当原始值是0的时候才能够更新成功 try { //因为CAS只有一个线程可以成功 所以一下逻辑保证只有一个线程可以进入执行 可以保证线程安全 if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; // 这里创建一个 n=16 的Node[]数组 并且赋值给table变量 table = tab = nt; sc = n - (n >>> 2); } } finally { sizeCtl = sc; //当 t1首次扩容完成之后 sizeCtl=0 表明扩容完成 } break; }
2:在table 成员变量的 i 索引处添加元素 即table[i] 为空的时候添加元素:这个时候的 table[i]==null,只需要保证第一个线程添加成功,
并且对其他线程可见即可 使用 CAS+volatile 即可保证,无需加锁
具体的代码如下: 假设线程 t1 和 线程 t2 同时操作
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //1:t1 先进入 t2 后进入 当t1 通过 casTabAt 更改过table[i] 为非null 之后,t2线程则进入不了下面的逻辑 2:有可能t1 t2 获取到的 table[i]都是null 这是都会进入下面的逻辑进行操作 if (casTabAt(tab, i, null, // 这里使用casTabAt()方法来更新 table[i] 的值,只有一个线程可以更改成功,这样就保证了只有一个线程操作成功,保证了线程安全。 new Node<K,V>(hash, key, value, null))) break; // 当 table[i] 的元素为空的时候,不需要通过加锁的方式来进行put操作,减少了开销,而map中进行put操作时 大部分的场景下 table[i]==null 这样避免了频繁加锁和释放锁的开销 }
tabAt 的具体方法如下: 获取table[i] 的元素 保证可见性
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE); }
3:当table[i] 不为空的时候添加元素,即拉链法 这个时候因为成员变量 table是共享的,所以对table[i] 进行操作的时候需要加锁的,这里使用的是 synchronized 来加锁
加锁的代码如下:
synchronized (f) // f=table[i] 其实就是table数组的第i段,所以这里的加锁粒度也是对 table 数组的某一个需要操作的分段尽心加锁 if (e.hash == hash && //这段逻辑就是对key重复的元素进行覆盖 ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; }
真正执行操作的是下面的逻辑
Node<K,V> pred = e; if ((e = e.next) == null) { //当遍历table[i] 中的元素,e为最后一个Node的时候,将新的Node添加到 e.next 元素中 这就完成了拉链法的操作 就是在table[i] 的对象锁下进行操作的。 pred.next = new Node<K,V>(hash, key,value, null); break; }
4:扩容机制,是如何实现扩容的,如何保证线程安全,在扩容的时候如果这个时候其他线程执行 put和 get的时候会怎样,如何保证线程安全?
我们知道扩容的时候需要 新建一个 nextTable 的Node[]数组,然后就是一个将就的table元素复制到新的nextTable数组中的过程,
这里新建一个nextTable Node[] 时需要保证只有一个线程在操作,这样可以保证线程安全
代码如下: 这里通过compareAndSwapInt 的CAS方法来保证只有一个线程执行下面的transfer 方法
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) transfer(tab, nt);
接下来就是进入了transfer(tab, nt); 的方法中步骤如下:
1: Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; //新建一个Node[]数组 长度为原来长度的2倍
nextTab = nt; //将新的Node 数组指向nextTab
2: 初始化ForwardingNode节点,其中保存了新数组nextTable的引用,在处理完每个槽位的节点之后当做占位节点,表示该槽位已经处理过了;
int nextn = nextTab.length; ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); boolean advance = true;
3、通过for自循环处理每个槽位中的链表元素,默认advace为真,通过CAS设置transferIndex属性值,并初始化i和bound值,i指当前处理的槽位序号
for (int i = 0, bound = 0;;) { Node<K,V> f; int fh; while (advance) { // ... 逻辑代码 }
4、在当前假设条件下,槽位15中没有节点,则通过CAS插入在第二步中初始化的ForwardingNode节点,用于告诉其它线程该槽位已经处理过了;
else if ((f = tabAt(tab, i)) == null) advance = casTabAt(tab, i, null, fwd);
5、如果槽位15已经被线程A处理了,那么线程B处理到这个节点时,取到该节点的hash值应该为MOVED,值为-1,则直接跳过,继续处理下一个槽位14的节点;
else if ((fh = f.hash) == MOVED) advance = true;
6:如果f 是一个链表结构,首先需要对该该链表进行加锁后,遍历链表中的Node 元素,将链表中的元素复制到新的 table 数组中,这里面用到了快速将元素从旧的链表中复制到新的链表中,然后将操作完成的链表索引指向一个ForwardingNode节点,表示操作完成。
7:遍历完成旧的table[]数组中的所有节点之后,完成操作了,将 table引用指向新的table[]数组 完成了扩容的机制扩容的过程也是通过synchronized 加 CAS的方式来保证线程的安全