jdk-map
JUC
HashMap
java
扩容因子DEFAULT_LOAD_FACTOR=0.75的作用,默认初始空间DEFAULT_INITIAL_CAPACITY=16(2的幂,每次扩容2倍)的原因
- 16和2 => 便于&运算
- 0.75 => loadFactory过大碰撞概率增大,loadFactory过小浪费存储空间;16*0.75=9。存储大于9时自动扩容;即
size> threshold = loadFactory * capacity即会发生扩容2倍的动作(rehashing操作)
red-black-tree
- hashMap-JDK7之前是使用数组+琏表存储数据,
在JDK8之后引入红黑树;某个琏表的长度length>8 && 数组长度总length<64使用琏表存储, link.length > 8 & array.length > 64时使用数,经验值根据伯松分布。
多线程扩容时发生
条件竞争问题
- jdk1.7 使用
头插:可能出现琏表环,造成get()数据时死循环,put/rehash等所有操作都是头插 - jdk1.8 使用尾插,解决条件竞争问题.
- 注意,hanhMap不是线程安全,不能用在多线程环境
HashMap-jdk1.7
// 存储数组
// 1. 线程不安全 => 首先因为使用数组+琏表存储,数组是实例属性,没有加锁做同步
// 2. 线程不安全 => tranfer()函数的问题
transient Entry[] table;
static class Entry<K,V> implements Map.Entry<K,V>{
// 1. key尽量使用final => equals,hascode重写
final K key;
V v;
Entry next;
int hash;
}
static int indexFor(int hash, int length) {
return hash & (length-1);
}
// 多线程下容易形成琏表环
// 解决1.加锁2.多线程分组3.copy-on-write
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
// 1. 旧琏表头
Entry<K,V> e = src[j];
if (e != null) {
// 释放旧Entry数组的对象引用
src[j] = null;
do {
// 记录下一个节点
Entry<K,V> next = e.next;
// 计算新位置
int i = indexFor(e.hash, newCapacity);
// 插入新位置的头
e.next = newTable[i];
newTable[i] = e;
// 处理下一个节点
e = next;
} while (e != null);
}
}
}
主要的问题
- 初始大小,扩容因子的选择
- 线程不安全的原因=> 主要是对实例数组的操作没有加锁同步,put不具有原子性
- 取模算法
头插操作
- 依次遍历数组;遇到连表从上到下遍历连表
- 链条头插入新连表
- put/transfer函数都是头插
所谓的线程安全的实现方法=>1. 共享的元素需要加锁 2. 不同的线程有自己的备份
HashMap-jdk1.8
与jdk1.7的区别
- 红黑树(8|64)
- 尾插
- 扩容后迁移,算法
static class Node<K,V> implements Map.Entry<K,V> {
// 1. 使用final修饰关键field(key,hash) => 用于计算hashcode,equals,不能随便被修改
final int hash;
final K key;
V value;
Node<K,V> next;
}
HashMap{
transient int size; // 逻辑长度
transient int modCount; // 修改次数
transient Node<K,V>[] table; // hash数组
int threshold; // 扩容临界值
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 模式初始大小
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大值是1<<31 - 1, 50%的空间利用率
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 扩展因子
// 扩展因子
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
}
// hash改动
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// @param onlyIfAbsent if true, don't change existing value,不存在时插入
// @param evict if false, the table is in creation mode.
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1.null先扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
// 2. 数组不存在,则插入
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 3. 是琏表第一个节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
// 4. tree
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 5. 琏表遍历
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 6. 琏表结尾插入=>尾插
p.next = newNode(hash, key, value, null);
// 7. 转树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 8.琏表上存在,覆盖
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 8.琏表上存在,覆盖
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 9.最后扩容
if (++size > threshold) resize();
afterNodeInsertion(evict);
return null;
}
// get
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null)
{
// 1. 琏表头,返回
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 2. tree
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 4. 遍历琏表
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
- HashMap使用
Node存储单元 - 先使用
k.hashcode()(hashcode是内存地址)计算数组索引index = (length-1) & hashcode - 发生
hash碰撞时,解决方法:开地址法,拉链法(java默认); - 通过e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))判断是否插入
- 插入使用
尾插法;put/rehash都是。
<k1,v1>,<k2,v2>发生hash碰撞的情况时get/put的流程
index(k1.hashcode()) = index(k2.hashcode())计算索引时发生hash碰撞。
发生碰撞,会存放在相同的bucket,再使用琏表依次存储Node<k,v>对象.如果k.equals(k1) && k1 == k2相等则覆盖,不相等则尾插入;- 读取时先计算k.hashcode()计算bucket,在使用k.equal()做值比较找到指定的对象.
重写equals方法的时候,一定要重写hashCode方法;equal()默认使用hashcode()
对于<k1,v1>,<k2,v2>; 如果k1.equals(k2)则,说明k1,k2是相同对象,应该存在map.get(k1) = map.get(k2); 如果不重写hashcode()计算索引时会得到不同index,则put(k1,v1)和put(k2,v1)会放到不通的bucket;这里是矛盾的。
作为key的值的对象final定义,重写equals(),hashCode()函数,避免出现碰撞;如Integer,String做Key.使用fianl修饰计算hashcode()的field.
jdk1.7-bug
条件竞争
- jdk7头插,插入时新琏表中的相对顺序与原来的琏表相反,多线程容易生成琏表环
尾插,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题
HashMap-HashTable-ConcurrentHashMap
HashMap: 非synchronized,可以key=null,普通的hash数据结构, HashTable: 使用synchronized修饰方法,不可以key=null; Collections.synchronizedMap,使用synchronzed代码块,修饰final Object(封装了has-a) ConcurrentHashMap:使用分段锁
- HashMap的迭代器(Iterator)是fail-fast
- Hashtable的enumerator不是fail-fast的
HashMap-HashSet-TreeMap
HashMap,TreeMap实现Map HashSet实现Set
ConcurrentHashMap
线程安全的选项
- Collections.synchronizedMap(Map)
- Hashtable
- ConcurrentHashMap
Collections.synchronizedMap(Map)
// 使用对象锁实现,使用的是synchronized的代码块
SynchronizedMap(Map<K,V> m) {
this.m = Objects.requireNonNull(m);
mutex = this;
}
SynchronizedMap(Map<K,V> m, Object mutex) {
this.m = m;
this.mutex = mutex;
}
public int size() {synchronized (mutex) {return m.size();}}
Hashtable
// 使用的是synchronized的方法
public synchronized int size() {return count;}
HashTable对key,value是否为null做了判断,如果是则抛出异常.底层原因是fail-safe.
failFast-failSafe
在Collection集合和JUC的各个集合类中,有线程安全和线程不安全这2大类的版本。
线程不安全的类,并发情况下可能会出现fail-fast情况线程安全的类,可能出现fail-safe的情况.... 安全的就safe.....
集合的迭代器Iterator的实现原理是对原始的list和modCount(修改次数)拷贝了一份快照。
- 如果对线程不安全的类进行并发的修改,modCount不是期望的值会抛出异常
- 如果对线程安全的类进行遍历,copy的过程中无法判断null代表的是不存在还是空,copy的数据就会出现问题。
对比
实现方式不同:Hashtable 继承了 Dictionary类,而 HashMap 继承的是 AbstractMap 类。Dictionary 是 JDK 1.0 添加的,貌似没人用过这个,我也没用过。初始化容量不同:HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75。扩容机制不同:当现有容量大于总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1。迭代器不同:HashMap 中的 Iterator 迭代器是 fail-fast 的,而 Hashtable 的 Enumerator 不是 fail-fast 的- null值存取不同,hashtable是fail-safe的.
ConcurrentHashMap-jdk1.7
// 分段锁的实现
static final class Segment<K,V> extends ReentrantLock implements Serializable{
// 和 HashMap 中的 HashEntry 作用一样,真正存放数据的
transient volatile HashEntry<K,V>[] table;
transient int count;
// fail—safe
transient int modCount;
// 扩容界限
transient int threshold;
// 负载因子
final float loadFactor;
}
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
// 当用默认构造函数时,最大并发数是 16,即最大允许 16 个线程同步写操作
private static final int DEFAULT_CAPACITY = 16;
}
// hash数组
static final class HashEntry<K,V> {
// final,volatile
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
}
public V put(K key, V value) {
Segment<K,V> s;
if (value == null) throw new NullPointerException();
// 1. 计算hash
int hash = hash(key.hashCode());
// 2. 计算segment索引
// hash值是int值,32bits。segmentShift=28,无符号右移28位,剩下高4位,其余补0。
// segmentMask=15,长度16,二进制低4位全部是1,所以j相当于hash右移后的低4位
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject (segments, (j << SSHIFT) + SBASE)) == null)
// 3. 使用segment[0]初始化segment[j]
s = ensureSegment(j);
// 4. 插入
return s.put(key, hash, value, false);
}
1.7 初始化
- Segment数组长度为 16,不可以扩容
- Segment[i] 的默认大小为 2,负载因子是 0.75,得出初始阈值为 1.5,也就是以后插入第一个元素不会触发扩容,
插入第二个会进行第一次扩容 - 这里初始化了 segment[0],其他位置还是 null,至于为什么要初始化 segment[0],方便使用segment[0]新建segment[i]
- 当前 segmentShift 的值为 32 - 4 = 28,segmentMask=16 - 1 = 15,segment大小是16使用hashcode高4为计算
1.7 put
- k.hashcode高4位计算segment的index,segments只初始化了segments[0],其他的put时初始化
- segment[index]==null时,则cas创建,
Unsafe使用segmens[]+index偏移地址做cas => 初始化segment使用cas - hash计算segment中的index,加锁使用
ReentrantLock插入类似Map => 插入使用Lock
ConcurrentHashMap-jdk1.8
/**
* 通过值控制Map的状态
* 0: 默认值,-1:正在初始化table,-n:n-1个线程在扩容
* table = null,未初始化:是需要初始化的大小,default=0
* table != null,初始化完成: 是length*0.75
**/
private transient volatile int sizeCtl;
// buckets
transient volatile Node<K,V>[] table;
// 扩容时用的buckets => 扩容时使用,有点类似redis
private transient volatile Node<K,V>[] nextTable;
// put底层实现
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
// 计算hash
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 不允许null
if (key == null || value == null) throw new NullPointerException();
// 计算hash
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 1. 先初始化buckets(tab)
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// tabAt = table_at
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 2. bucket的头节点==null,直接cas设置(unsafe工具),casTabAt=cas_table_at
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
// 3. 判断正在扩容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 4. bucket头节点存在,直接synchronized保护整个琏表, 此时是hash冲突
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 琏表存在则更新,尾插
if (e.hash == hash &&
((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) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 红黑树
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
// 琏表长度大于8
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
// 琏表长度>8则调用
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
// MIN_TREEIFY_CAPACITY 为 64
// 1. 所以,如果数组长度小于 64 的时候,其实也就是 32 或者 16 或者更小的时候,会进行数组扩容
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);
// 2. 转树
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
// 加锁
synchronized (b) {
if (tabAt(tab, index) == b) {
// 下面就是遍历链表,建立一颗红黑树
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
// 将红黑树设置到数组相应位置中
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
put流程
- 如果没有初始化就先调用 initTable()方法来进行初始化过程(unsafe)
- 如果没有 hash 冲突就直接 CAS 插入头节点(unsafe)
- 如果还在进行扩容操作就先进行扩容
- 如果存在 hash 冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入
- 最后一个如果该链表的数量大于阈值 8,就要先转换成黑红树的结构,break 再一次进入循环,如果添加成功就调用 addCount()方法统计 size,并且检查是否需要扩容
transfer
阅读源码之前,先要理解并发操作的机制。原数组长度为 n,所以我们有 n 个迁移任务,让每个线程每次负责一个小任务是最简单的,每做完一个任务再检测是否有其他没做完的任务,帮助迁移就可以了,而 Doug Lea 使用了一个 stride,简单理解就是步长,每个线程每次负责迁移其中的一部分,如每次迁移 16 个小任务。所以,我们就需要一个全局的调度者来安排哪个线程执行哪几个任务,这个就是属性 transferIndex 的作用。
第一个发起数据迁移的线程会将 transferIndex 指向原数组最后的位置,然后从后往前的 stride 个任务属于第一个线程,然后将 transferIndex 指向新的位置,再往前的 stride 个任务属于第二个线程,依此类推。当然,这里说的第二个线程不是真的一定指代了第二个线程,也可以是同一个线程

jdk1.8中cas实现的原理
- 了解Unsafe实现cas的原理(cpu-cas)
- JDK1.8中使用synchronized + CAS(Unsafe类)实现来更高效的对map中每个Node的细粒度加锁,cas对第一个元素操作;
- JDK1.7segment+cas,用来初始化segemnt
jdk1.8针对synchronized的优化
- 无锁,轻量锁,cas,synchronized,
- jdk1.7的Segment继承ReetantLock
- jdk1.8替换为sychronized代码块,性能更好
扩容条件
- table中节点数大于length*0.75
- 琏表长度>8且table.length>64,构建tree
jdk1.8的的死锁
Map<String, Integer> map = new ConcurrentHashMap<>(16);
// must not attempt to update any other mappings of this map.
// 不要同时更新2个
map.computeIfAbsent("AaAa",key ->map.computeIfAbsent("BBBB",key2 -> 42));
不是很重要(还需要研究) blog.csdn.net/zhanglong_4…
CopyOnWriteArrayList
- CopyOnWriteArrayList,通过 has-a ReentrantLock实现同步,好想不是使用cow实现的同步.....暂时不确定
- Vector使用synchronized修饰方法
LinkedHashMap
- is-a HashMap
- Entry继承为支持连表
- 模版函数修改顺序
HashSet
- has-a HashMap
- put函数(HashSet的value存入到HashMap的Key)
- 使用时注意equals()
ArrayList,Vector,LinkedList
- Vector,java遗留容器(Vector,hastTable,Dictionary,Stack,Properties,BitSet...不推荐使用); synchronized实现线程安全
- 线程安全的工具; synchronizedList
collection
- Collection
- List: ArrayList,LinkedList
- Set: HashSet,LinkedHashSet,TreeSet
- Queue:
- Map
- HashMap-LinkedHashMap
- TreeMap
- ConcurrentHashMap
- Queue: 本身有线程安全的要求
- TreeSet: 是平衡树
- HashMap
- 线程不安全 => Collections.synchronizedMap() 或 ConcurrentHashMap
- 最多只允许一个key=null
- jdk7: 数组 + 单链表(解决hash冲突)
- jdk8: 数组 + 单链表 + 红黑树(是一种平衡树,链表长度>8编程则红黑树)
- ConcurrentHashMap
- 通过Segment实现分段加锁,默认16个
- Segment继承了ReentranLock
- HashTable
- 继承Dictionary
- 通过sychronized实现线程安全
ps
- HashMap-jdk1.7
- HashMap-jdk1.8
- ConcurrentHashMap-1.7
- ConcurrentHashMap-1.8
- HashMap1.7-1.8
- Unsafe
- LinkedHashMap
- HashSet
- hashmap的put过程,什么时候扩容,前后扩容的好处,有几次扩容处理,为什么