先简单总结常见list、set和map,list和set都集成于Collection 集合,list有序集,能存储相同元素,set无序集,不能存储相同元素,map键值对方式存储
list下面有ArrayList、LinkedList,前者底层是以数组方式存储,后者链表方式;浅谈几个问题
增删改查效率问题
对于增删操作,前者操作比较麻烦,删除或者增加后数组会进行移位调制,后者方便,删除后直接前后修改链表指向即可;改查操作则相反,因为数组存储底层的位置是连续的查询相对较快,后者慢些;但是Linkedlist功能相对完全,提供了许多api,可以把它当作队列、堆栈进行使用;
扩容问题
ArrayList是一个动态数组,每次增加数据先判断是否需要扩容,需要就扩大其1.5倍,过程先创建新的数组,在把旧数组的元素全部copy到新数组;而linkedlist则无需扩容,直接申请内存即可
set常用的是hashset,与hashMap类似,相当与hashmap的key值存储,无序集合,扩容时有一个加载因子,当存储数据为总数组的加载因子倍,就扩容一倍;比如,加载因子0.75,当存储量大于初始容量的0.75倍就进行扩容
HashMap: 默认是一个16位的数组加横向链表,每次put元素时,会先hash算法计算key的hash值,在对数组长度求余,确定数组中的位置,如果数组的链表为空,就直接new一个Entry(hash,key,value,nextEntry)的对象装入,如果链表不为空,就出现hash碰撞,解决的办法就是先遍历单链表的整个链表,并比较hash和key值是否有相等的元素,有就直接更新,没有就new一个新的Entry添加在链表尾;除此之外,当存储的size超过总容量*加载因子倍,就会对map扩容2倍,在重新hash将旧map的数组迁移到新map中去;看源码更方便理解:
//1 put
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//2 hash算法加移位操作 减少hash冲突
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//3 加入value
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//检查hash数组是否需要扩容resize
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//与操作,确定当前值在数组中的index
//如果index为空直接插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//index不为空
else {
Node<K,V> e; K k;
//如果和index处的entry相等,替换
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果链表长度大于8了,已经成为treeNode红黑树结构,红黑树结构去插入
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//小于8,单链表结构,依次遍历链表,有相等的替换,没有就加入尾巴
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
HashMap在JDK1.8及以后的版本中引入了红黑树结构,若桶中链表元素个数大于等于8时,链表转换成树结构;若桶中链表元素个数小于等于6时,树结构还原成链表。因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
__ArrayMap:__使用两个一维数组进行存储,一个存放key的hash值,从小到大排列,一个存放key和value值,每次添加和删除有可能导致数组的元素移动,但是内存使用量较小,适合移动端,每次查找时在hash的数组里面采用二分法查找
为什么ArrayMap内存使用量相对于HashMap小?
HashMap的碰撞问题?
put时大量的hash值求余后确定到同一个数组index就出现了hash碰撞,HashMap解决的办法:
5. 对key的hash值进行移位求余操作,将高16位右移16位于地位相亦或
6. hashmap上述介绍的key值比较
HashMap从jdk1.7到1.8为什么把链表从头插改为尾插?
最直接的原因就是为了解决链表循环问题;怎么出现以及解决的呢?
两个线程同时添加元素的,并且刚好遇到map需要扩容,需要重新hash确定数组中的位置;如果扩容前某个链表为10-2-null,扩容后,在移动链表后,正确的结果应该是2-10-null,但是两个线程同时进入都处理为10-null,也都取到2节点,这时其中一个线程先处理完结果为2-10-null,然后第二个线程也把2处理了,变为2-10,然后遍历2的next,发现是10(线程1执行的结果),然后把10的next指向2,再次遍历10的next为null,链表移动结束,现在的结果是2-10,但是2和10是双向链表,查找时变为死循环
可以参考:https://juejin.im/post/5ba457a25188255c7b168023
HashMap是线程同步不安全的;当然也可以用
Collections.synchronizedMap(Map<K,V> m);
使其同步安全;除此之外还有一个HashTable结构是线程安全的,用法和HashMap一样,只是同步安全的结果造成访问效率较低,同一时间只允许一个线程访问,并且HashTable在put和get都做了同步操作;为了使效率更高,我们可以使用更高级的ConcurrentHashMap,如下
和HashMap结构一样,仍然是数组加链表segement、HashEntry,数组变为Segment;存取原理也和HashMap一样,通过HashCode值定位数组位置,在比较hash、key是否相等等条件,也有大于8转为红黑树结构;
首先ConcurrentHashMap的HashEntry中:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val; //volalite保证读取的是变量真实内存存放值,而不是缓存
volatile Node<K,V> next;
}
val的属性是volatile,保证读取的是变量真实内存存放值,而不是缓存;所以在读取get的时候不必要进行同步操作;
而Segment继承于ReentrantLock重入锁;从而在数组上每个Segment独享自己的锁lock;不与其他Segment去争抢锁,保证了并发的效率,每个链表独享自己数组index的锁lock
static class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
final float loadFactor;
Segment(float lf) { this.loadFactor = lf; }
}
以上是jdk1.7的操作;在jdk1.8又做了相应的改进;改进主要有以下几点:
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//初始化容量
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//没有hash冲突情况,跳出循环
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//如果当前位置为-1,需要进行扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
//....链表遍历插入
}
}
addCount(1L, binCount);
return null;
}
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
//CAS写入数据
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
CAS(compare and swap)比较交换,
do{
备份旧数据get_old;
基于旧数据构造新数据new_value;
}while(!CAS( 内存地址,备份的旧数据,新数据 ))
CAS即去内存地址取出的值与旧值相同,就用新数据更新;否则就不做任何操作;此操作为原子性操作,无需锁,提高性能
首先他是一个二叉树,二叉树的特性:
红黑树 在上面的基础上,为每个节点增加一种属性,非红即黑,并且要满足以下特性
对红黑树进行查找时,由于自身的特性,效率是比较高的,在进行插入、修改元素时还需要对树的结构重新调整,上色
TreeMap 就是以红黑树为基础封装的一个类,以下是其基本使用方法
private void testTreeMap(){
TreeMap<String, String> map = new TreeMap<>(new MyCompartor());
map.put("as", "dslkf");
map.put("re", "dslertekf");
map.put("awers", "dslkf");
map.put("ahjgklis", "dslkdfgdfdgf");
map.put("ass3s", "dslkf");
map.put("a1232432s", "dslk3435f");
map.put("a78s", "dsl3452kf");
map.put("87as", "dsl34kf");
map.put("aasdas", "dsl242342kf");
map.put("as67", "dslk2423f");
map.put("a1221312s", "dsl242kf");
map.put("as546", "dsl5678kf");
map.put("a12s", "dslkfghfghsf");
map.put("a111111s", "dfgdgslkf");
for(String key : map.keySet()){
Log.i("j_tag", "key " + key + " value " + map.get(key));
}
}
private class MyCompartor implements Comparator<String>{
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
}
阻塞队列:先进先出、线程安全,多用于消费者-生产者模式
一个支持线程优先级排序的无界队列,默认自然序进行排序,也可以自定义实现compareTo()方法来指定元素排序规则,不能保证同优先级元素的顺序。
用数组的方式存储数据,用法等同于ArrayList,但是线程安全
严格来说集合中使用了hash算法存储的要重写hashCode(hashset/hashmap/hashtable),非hashCode结构则不需要(arrayList/linkedList);并且在比较时要hashCode和equals必须同时相等时才能说明两个元素相等
从hashCode和equals两个方法单独来说,不同的对象都有可能分别相等,两者一起比较加强了比较的相等逻辑;按照正常集合比较逻辑来说,就是遍历整个集合,一个一个比较,但是这种太过于麻烦,而hashmap结构key值的存储位置和hashCode很密切,通过hashCode可以快速定位key的位置,然后在比较,提高比较效率;
而ArrayList这种结构,存储位置是数组结构,只能遍历所有一个个比较,可以不用重写hashCode,重写equals即可
indexof查找元素在集合List中的位置,因为List允许插入空元素,所以也可以查找null空元素;
所以返回值如果能在集合中查找到的话就返回index,也包含null,如果已经插入null,也返回null处的位置
另一种,查找不到元素,统一返回错误码-1