文章目录

  • 一、List
  • 1.1 ArrayList
  • 1.2 LinkedList
  • 1.3 Vector
  • 1.4 CopyOnWriteArrayList
  • 二、Map
  • 2.1 HashMap
  • 2.3 HashTable
  • 2.4 ConcurrentHashMap
  • JDK 1.7的实现
  • JDK 1.8的实现
  • 2.5 TreeMap
  • 三、Set
  • 3.1 HashSet
  • 3.2 TreeSet



如下所示的为Java集合的框架图(图片来自网络,侵删):

java 获取list前两百个数据 java获取list索引位置_数组


下面我们主要介绍其中的ListMap以及Set以及各类型常用的类。

一、List

特性:允许重复元素的存在,数据的插入顺序是有序的。
如下demo所示:

public class ListDemo {
    public static void main(String[] args) {
        List<Integer> demo = Lists.newArrayList();
        demo.add(1);
        demo.add(2);
        demo.add(3);
        demo.add(1);
        demo.add(4);
        demo.add(5);
        demo.add(3);
        demo.forEach(x -> System.out.print(x + " "));
    }
}

运行结果:

java 获取list前两百个数据 java获取list索引位置_Java基础_02

1.1 ArrayList

底层是通过数组(Object[])来实现的,适用于静态数据的查找。

java 获取list前两百个数据 java获取list索引位置_数据_03


如上图所示,在执行new ArrayList()时,会初始化一个空的object数组,但是在首次执行add(E e)方法之后,会初始化Object数组的默认大小为10;如下图所示:

java 获取list前两百个数据 java获取list索引位置_java 获取list前两百个数据_04

  • 优点:
  1. 顺序查找、随机查找都非常的快(通过索引即数组下标可以在O(1)时间内找到指定元素),如下所示的elementData数组存储的为ArrayList的数据。
  2. 顺序存储,存储的利用率非常的高(使用数组进行连续存储)。
  • 缺点:
  1. 在进行插入数据达到阀值的时候,需要构建新的数组并将旧数组中的元素移动到新的数组中(使用Arrays.copy(oldArr,newArrLength));

浅谈ArrayList动态扩容
Java 中 ArrayList 的实现解析

  1. 在进行插入和删除操作的时候,都需要移动其余元素。

1.2 LinkedList

底层是通过双向链表来实现的,适用于经常进行变化的动态数据

  • 优点:插入/删除不需要移动数据,只需要增减相应的节点即可

插入数据方法如下:

在执行add(E e)方法时,会调用如下的linkLast(E e),采用尾插法加入新节点。

java 获取list前两百个数据 java获取list索引位置_链表_05


删除数据方法如下:

调用remove()方法之后,首先会调用removeFirst()方法,意思为删除头结点;

java 获取list前两百个数据 java获取list索引位置_数据_06


接着该方法会调用unlinkFirst(f)方法来删除头结点:

java 获取list前两百个数据 java获取list索引位置_数据_07

  • 缺点:查找的时候,只能通过节点的遍历来实现,最糟糕的情况查找指定节点的时间复杂度为java 获取list前两百个数据 java获取list索引位置_Java基础_08(即需要遍历一半的链表)。

1.3 Vector

Vector与ArrayList的实现原理类似,都是基于Object[] 数组实现的,但是Vector默认扩容为原来的2倍(或是根据指定值进行扩容),而ArrayList则为1.5倍。

java 获取list前两百个数据 java获取list索引位置_java 获取list前两百个数据_09


而Vector和ArrayList最大的区别是同步(synchronized)的使用,Vector对外暴露的大部分方法都是同步方法,所以Vector是线程安全的,如下的get和set方法所示:

java 获取list前两百个数据 java获取list索引位置_数组_10


synchronized方法为重量级同步,当某一个线程在调用vector对象的同步方法的同时,其它线程调用该vector对象的同步方法会导致线程阻塞,因此vector效率并不高。

1.4 CopyOnWriteArrayList

由于Vector在多线程场景下效率并不高,所以对于读多写少的场景,可以使用CopyOnWriteArrayList
Copy-on-Write,也就是“写时复制”,当有 写类型的操作作用到 CopyOnWriteArrayList 对象的时候,它们都会先获取锁,然后复制一份当前数据作为副本,然后在当前的数据副本上做修改,最后把修改提交,然后释放锁。如下所示的方法:

未加锁的读方法:

java 获取list前两百个数据 java获取list索引位置_数据_11


加了锁的修改方法:

java 获取list前两百个数据 java获取list索引位置_java 获取list前两百个数据_12


java 获取list前两百个数据 java获取list索引位置_java 获取list前两百个数据_13

CopyOnWriteArrayListVector高效,主要有以下2个原因:

  1. Vector中,读写操作都被加锁了,而CopyOnWriteArrayList中,只有写操作才被加锁,而读操作没有进行加锁。这样,当读线程数量远大于写线程数量的时候(多读写少),CopyOnWriteArrayList尤为高效。
  2. Vector中,使用的是内置锁,而CopyOnWriteArrayList中,使用的是jdk1.5引入的ReentrantLock,相比于内置锁,ReentrantLock的性能还是有所提升的。

如何线程安全地遍历List:Vector、CopyOnWriteArrayList http://xxgblog.com/2016/04/02/traverse-list-thread-safe/
Java 中 Vector 、Stack 、CopyOnWriteArrayList 的实现解析

二、Map

Map结构主要有如下特性:

  • 不允许重复的元素存在,重复的时候相同的key对应的data数据会进行覆盖;
  • 插入的数据在存储的时候是按照hashcode顺序存储的,所以get的属性和put的顺序可能不一致;
  • 作为key的对象,必须要同时复写equals()hashcode()方法。

2.1 HashMap

  • 实现原理
  • JDK 1.7的实现
  1. 存储结构:数组+链表(图片来自网络,侵删)

HashMap实现原理及源码分析

2.主要的方法:
put()方法:
如果发现有相同key值的Entry存在,则使用本次值覆盖旧值并返回旧值,否则继续新增Entry

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);//key为null时的处理
    int hash = hash(key);//计算hash值
    int i = indexFor(hash, table.length);//计算在hash桶中的索引位置
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //如果当前索引位置有值,切key相等则使用当前value覆盖旧值,并返回
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    //添加新的Entry
    addEntry(hash, key, value, i);
    return null;
}

示意图如下所示:

java 获取list前两百个数据 java获取list索引位置_链表_14


put方法可能导致,hash表的扩容,扩容时需要将原hash表中的数据重新hash到新的表中,如下所示:

java 获取list前两百个数据 java获取list索引位置_Java基础_15


如下代码是使用的头插法,转移就链表的数据:

e.next = newTable[i];
newTable[i] = e;

示意图如下所示:

java 获取list前两百个数据 java获取list索引位置_Java基础_16


get()方法:

get方法主要的思路就是首先对key值计算出在hash桶中的索引位置(table[indexFor(hash, table.length)),接着在该索引位置找出key值相等的Entry的value值。get方法主要是调用了如下的getEntry方法:

java 获取list前两百个数据 java获取list索引位置_数据_17


3. HashMap存在的问题:

1)由于hashmap不是线程安全的所以在多线程情况下,读取和写入数据时会出现非预期结果;

2)在扩容的时候需要进行transfer以及rehash的操作,在这个过程中线程1可能会读到线程2transfer后的结果,这样在transfer节点的时候可能会造成环装链表,那么我们在get方法的时候,对于这个存在hash冲突的情况下,去遍历链表就会导致死循环。

3)在hash大量冲突的时候数组+链表结构会倒退为链表结构,这样get数据的时候时间复杂度达到了java 获取list前两百个数据 java获取list索引位置_链表_18

谈谈HashMap线程不安全的体现
疫苗:JAVA HASHMAP的死循环 https://coolshell.cn/articles/9606.html

  • JDK 1.8的实现
  1. 存储结构:数组+链表/红黑树
    1)当达到节点阀值(默认为8)的时候,会将链表转化成红黑树;
    2)当存在大量的hash冲突的时候,对map的查找会退化成一个红黑树,这样相对于链表来说时间查找的时间复杂度更优,可以达到java 获取list前两百个数据 java获取list索引位置_Java基础_19
    (图片来自网络,侵删)
    put方法主要步骤:
    1)新判断hash数组Node<k,v>[] table是否为空,为空则初始化;
    2)通过hash值计算在table中的位置,如果当前位置没有其他节点存在,则不存在hash冲突,则直接新增新的节点;
    3)如果存在hash冲突,则会有如下三种操作:
    -----a.第一次存在hash冲突,则判断key值,如果相等,则记录该节点e;

    -----b.如果冲突节点为树节点,则调用putTreeVal方法进行put操作;

    -----c.如果不是以上两种则,遍历链表,并判断是否存在key相等的情况,相等则记录该节点e,否则使用尾插法(如果结点数等于8则将链表转换成红黑树)。

    4)最终对于key相等的节点e,使用新的value 覆盖oldValue并返回。

    源码解读如下所示:
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
 
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // table是否为空或者length等于0, 如果是则调用resize方法进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;    
    // 通过hash值计算索引位置, 如果table表该索引位置节点为空则新增一个
    if ((p = tab[i = (n - 1) & hash]) == null)// 将索引位置的头节点赋值给p
        tab[i] = newNode(hash, key, value, null);
    else {  // table表该索引位置不为空
        Node<K,V> e; K k;
        if (p.hash == hash && // 判断p节点的hash值和key值是否跟传入的hash值和key值相等
            ((k = p.key) == key || (key != null && key.equals(k)))) 
            e = p;  // 如果相等, 则p节点即为要查找的目标节点,赋值给e
        // 判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法查找目标节点
        else if (p instanceof TreeNode) 
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {	// 走到这代表p节点为普通链表节点
            for (int binCount = 0; ; ++binCount) {  // 遍历此链表, binCount用于统计节点数
                if ((e = p.next) == null) { // p.next为空代表不存在目标节点则新增一个节点插入链表尾部
                    p.next = newNode(hash, key, value, null);
                    // 计算节点是否超过8个, 减一是因为循环是从p节点的下一个节点开始的
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);// 如果超过8个,调用treeifyBin方法将该链表转换为红黑树
                    break;
                }
                if (e.hash == hash && // e节点的hash值和key值都与传入的相等, 则e即为目标节点,跳出循环
                    ((k = e.key) == key || (key != null && key.equals(k)))) 
                    break;
                p = e;  // 将p指向下一个节点
            }
        }
        // e不为空则代表根据传入的hash值和key值查找到了节点,将该节点的value覆盖,返回oldValue
        if (e != null) { 
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); // 用于LinkedHashMap
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold) // 插入节点后超过阈值则进行扩容
        resize();
    afterNodeInsertion(evict);  // 用于LinkedHashMap
    return null;
}
  1. 是否存在如JDK1.7的死循环问题:
    上面已经提到了,导致死循环的问题主要是,在进行扩容进行节点的transfer的时候,节点的顺序会反掉(如:1->2->3 会变成3->2->1)。而Jdk1.8的普通链表的扩容如下所示:
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {   // 老table不为空
        if (oldCap >= MAXIMUM_CAPACITY) {      // 老table的容量超过最大容量值
            threshold = Integer.MAX_VALUE;  // 设置阈值为Integer.MAX_VALUE
            return oldTab;
        }
        // 如果容量*2<最大容量并且>=16, 则将阈值设置为原来的两倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)   
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // 老表的容量为0, 老表的阈值大于0, 是因为初始容量被放入阈值
        newCap = oldThr;	// 则将新表的容量设置为老表的阈值 
    else {	// 老表的容量为0, 老表的阈值为0, 则为空表,设置默认容量和阈值
        newCap = DEFAULT_INITIAL_CAPACITY; 
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {  // 如果新表的阈值为空, 则通过新的容量*负载因子获得阈值
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr; // 将当前阈值赋值为刚计算出来的新的阈值
    @SuppressWarnings({"rawtypes","unchecked"})
    // 定义新表,容量为刚计算出来的新容量
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab; // 将当前的表赋值为新定义的表
    if (oldTab != null) {   // 如果老表不为空, 则需遍历将节点赋值给新表
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {  // 将索引值为j的老表头节点赋值给e
                oldTab[j] = null; // 将老表的节点设置为空, 以便垃圾收集器回收空间
                // 如果e.next为空, 则代表老表的该位置只有1个节点, 
                // 通过hash值计算新表的索引位置, 直接将该节点放在该位置
                if (e.next == null) 
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                	 // 调用treeNode的hash分布(跟下面最后一个else的内容几乎相同)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null; // 存储跟原索引位置相同的节点
                    Node<K,V> hiHead = null, hiTail = null; // 存储索引位置为:原索引+oldCap的节点
                    Node<K,V> next;
                    do {
                        next = e.next;
                        //如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
                        if ((e.hash & oldCap) == 0) {   
                            if (loTail == null) // 如果loTail为空, 代表该节点为第一个节点
                                loHead = e; // 则将loHead赋值为第一个节点
                            else    
                                loTail.next = e;    // 否则将节点添加在loTail后面
                            loTail = e; // 并将loTail赋值为新增的节点
                        }
                        //如果e的hash值与老表的容量进行与运算为1,则扩容后的索引位置为:老表的索引位置+oldCap
                        else {  
                            if (hiTail == null) // 如果hiTail为空, 代表该节点为第一个节点
                                hiHead = e; // 则将hiHead赋值为第一个节点
                            else
                                hiTail.next = e;    // 否则将节点添加在hiTail后面
                            hiTail = e; // 并将hiTail赋值为新增的节点
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null; // 最后一个节点的next设为空
                        newTab[j] = loHead; // 将原索引位置的节点设置为对应的头结点
                    }
                    if (hiTail != null) {
                        hiTail.next = null; // 最后一个节点的next设为空
                        newTab[j + oldCap] = hiHead; // 将索引位置为原索引+oldCap的节点设置为对应的头结点
                    }
                }
            }
        }
    }
    return newTab;
}

以上代码主要可以分为两部分,一部分为计算的hash表的容量,另一部分为将老表的数据转移到新表中。而对于存在hash冲突的链表则是在:

do {
    next = e.next;
    //如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
    if ((e.hash & oldCap) == 0) {   
        if (loTail == null) // 如果loTail为空, 代表该节点为第一个节点
            loHead = e; // 则将loHead赋值为第一个节点
        else    
            loTail.next = e;    // 否则将节点添加在loTail后面
        loTail = e; // 并将loTail赋值为新增的节点
    }
    //如果e的hash值与老表的容量进行与运算为1,则扩容后的索引位置为:老表的索引位置+oldCap
    else {  
        if (hiTail == null) // 如果hiTail为空, 代表该节点为第一个节点
            hiHead = e; // 则将hiHead赋值为第一个节点
        else
            hiTail.next = e;    // 否则将节点添加在hiTail后面
        hiTail = e; // 并将hiTail赋值为新增的节点
    }

中实现的原来的transfer方法中转移hash冲突链表上的数据,转移数据到新的hash表是采用尾插法(即原来是1->2->3,那么新的hash表中还是1->2->3)。

Java集合:HashMap详解(JDK 1.8)

  • 如何转换成线程安全
    Collections.synchronizedMapHashMap包装起来,返回支持同步map结构(线程安全的SynchronizedMap)。为了保证按顺序访问,必须通过返回的SynchronizedMap完成对底层HashMap的所有访问,如下所示:
Map map = Collections.synchronizedMap(hashMap);
map.size();
map.containsKey();
......

java 获取list前两百个数据 java获取list索引位置_数据_20

2.3 HashTable

HashMapHashTable总体的实现逻辑相仿,但是存在如下的不同:

  • HashMap允许key和value为null,Hashtable不允许。
  • HashMap的默认初始容量为16,Hashtable为11。
  • HashMap的扩容为原来的2倍,Hashtable的扩容为原来的2倍加1。
  • HashMap非线程安全的,Hashtable线程安全的。
  • HashMap的hash值重新计算过,Hashtable直接使用hashCode。
  • HashMap去掉了Hashtable中的contains方法。
  • HashMap继承自AbstractMap类,Hashtable继承自Dictionary类。

2.4 ConcurrentHashMap

JDK 1.7的实现

1.存储结构

一个segment数组+多个HashEntry组成+链表形式;

使用锁分离技术,将锁的粒度降低,利用多个锁来控制多个小的table。如下图所示,Segment数组的意义就是将一个大的table分割成多个小的table(HashEntry数组)来进行加锁。 (图片来自网络,侵删)

java 获取list前两百个数据 java获取list索引位置_数据_21


2.实现原理

相比于对整个Map加锁的设计,分段锁大大的提高了高并发环境下的处理能力。但同时,由于不是对整个Map加锁,导致一些需要扫描整个Map的方法(如size(), containsValue())需要使用特殊的实现,另外一些方法(如clear())甚至放弃了对一致性的要求(因此ConcurrentHashMap不能完全替代HashTable,因为HashTable是属于强一致性的)。Segment:

Segment继承了ReentrantLock,所以其也具有了相应的锁特性:

java 获取list前两百个数据 java获取list索引位置_数组_22


HashEntry:

HashEntry的value和next使用volatile修饰,保证了数据的可见性:

java 获取list前两百个数据 java获取list索引位置_java 获取list前两百个数据_23


并发度:

并发度可以理解为程序运行时能够同时更新ConccurentHashMap且不产生锁竞争的最大线程数,实际上就是ConcurrentHashMap中的分段锁个数,即Segment[]的数组长度,默认为16,也可以用户在构造器中自定义:

java 获取list前两百个数据 java获取list索引位置_java 获取list前两百个数据_24


并发度需要设置合理的值,过小会带来严重的锁竞争问题;如果并发度设置的过大,原本位于同一个Segment内的访问会扩散到不同的Segment中,CPU cache命中率会下降,从而引起程序性能下降。3.常用方法解析:

put方法:

java 获取list前两百个数据 java获取list索引位置_Java基础_25


在put节点之前,先用tryLock()尝试获取锁,如果成功获取,则插入新值或覆盖旧值:

java 获取list前两百个数据 java获取list索引位置_java 获取list前两百个数据_26


如果获取锁失败,则调用scanAndLockForPut方法,该方法会循环遍历链表(主要目的是希望遍历的链表被CPU cache所缓存,为后续实际put过程中的链表遍历操作提升性能),如果不存在相同的key则可以提前建立新的节点,循环遍历达到指定次数之后,会显示获取锁

java 获取list前两百个数据 java获取list索引位置_链表_27


remove方法:

先定位在segment数组中的位置,然后再尝试获取锁,如果未获取锁成功,则遍历链表,提高后去的缓存命中率。

java 获取list前两百个数据 java获取list索引位置_数组_28


size方法:

如果连续两次所有Segmentmodcount和相等,则过程中没有发生其他线程修改ConcurrentHashMap的情况,返回获得的值。否则再锁定所有segment,重新计算:

java 获取list前两百个数据 java获取list索引位置_数据_29


get方法:

get方法没有使用锁,而是通过Unsafe对象的getObjectVolatile()方法提供的原子读语义,来获得Segment以及对应的链表,然后对链表遍历判断是否存在key相同的节点以及获得该节点的value。

java 获取list前两百个数据 java获取list索引位置_链表_30

ConcurrentHashMap总结

JDK 1.8的实现

1.存储结构:

Node数组+链表+红黑树(图片来自网络,侵删)

java 获取list前两百个数据 java获取list索引位置_数组_31


2.实现原理:

使用finalvolatilesynchronized、native的CAS方法以及引入一些辅助类(如TreeBinTraverser等对象内部类)来控制并发,将锁的粒度降低到了节点层面。

3.主要的方法
CAS方法:
ConcurrentHashMap定义了三个原子操作,用于对指定位置的节点进行操作。正是这些原子操作保证了ConcurrentHashMap的线程安全。

//获得在i位置上的Node节点
    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);
    }
		//利用CAS算法设置i位置上的Node节点。之所以能实现并发是因为他指定了原来这个节点的值是多少
		//在CAS算法中,会比较内存中的值与你指定的这个值是否相等,如果相等才接受你的修改,否则拒绝你的修改
		//因此当前线程中的值并不是最新的值,这种修改可能会覆盖掉其他线程的修改结果  有点类似于SVN
    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }
		//利用volatile方法设置节点位置的值
    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }

put方法
JDK8中的实现也是锁分离的思想,只是锁住的是一个Node,而不是JDK7中的Segment,而锁住Node之前的操作是无锁的并且也是线程安全的,建立在之前提到的3个原子操作上

public V put(K key, V value) {
        return putVal(key, value, false);
    }

/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
		//不允许 key或value为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;
        //如果table为空的话,初始化table
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //根据hash值计算出在table里面的位置 
        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
        }
        //当遇到表连接点时,需要进行整合表的操作
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            //结点上锁  这里的结点可以理解为hash值相同组成的链表的头结点
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    //fh〉0 说明这个节点是一个链表的节点 不是树的节点
                    if (fh >= 0) {
                        binCount = 1;
                        //在这里遍历链表所有的结点
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //如果hash值和key值相同  则修改对应结点的value值
                            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;
            }
        }
    }
    //将当前ConcurrentHashMap的元素数量+1
    addCount(1L, binCount);
    return null;
}

我们可以发现JDK8中的实现也是锁分离的思想,只是锁住的是一个Node,而不是JDK7中的Segment,而锁住Node之前的操作是无锁的并且也是线程安全的,建立在之前提到的3个原子操作上。

get方法:
get方法比较简单,给定一个key来确定value的时候,必须满足两个条件 key相同 hash值相同,对于节点可能在链表或树上的情况,需要分别去查找。

public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        //计算hash值
        int h = spread(key.hashCode());
        //根据hash值确定节点位置
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            //如果搜索到的节点key与传入的key相同且不为null,直接返回这个节点	
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            //如果eh<0 说明这个节点在树上 直接寻找
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
             //否则遍历链表 找到对应的值并返回
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

ConcurrentHashMap总结

2.5 TreeMap

  • 特性:存储的数据会根据key值进行排序
  • 实现原理:红黑树(调整:左旋、右旋、着色)

Java提高篇(二七)—–TreeMap http://cmsblogs.com/?p=1013

三、Set

3.1 HashSet

  • 特性:插入元素不可重复,无须的,允许null的存在
  • 实现:
    1.所有方法都是通过hashmap来实现的,只将hashmap的key暴露给用户使用

    2.而底层的hashmap的value为:private static final Object PRESENT = new Object();

HashMap和HashSet的区别 http://www.importnew.com/6931.html

3.2 TreeSet

  • 特性:插入元素不重复,有序
  • 实现:底层通过treemap来实现.

Java 集合系列17之 TreeSet详细介绍(源码解析)和使用示例