Java容器知识总结

容器类关系图

在这里插入图片描述

HashMap实现分析

HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。数组的特点是:存储区间连续,占用内存严重,寻址容易,插入删除困难;而链表的特点是:存储区间离散,占用内存比较宽松,寻址困难,插入删除容易;HashMap综合应用了这两种数据结构,实现了寻址容易,插入删除也比较容易的特点。
在这里插入图片描述

JDK1.8之前的并发问题

1.使用HashMap进行put操作时,会调用如下的方法:

void addEntry(int hash, K key, int bucketIndex){
	//将bucketIndex下标对应的值保存到“e”中
	Entry<K,V> e = table[bucketIndex];
	//采用“头插法”,将新元素插入table[bucketIndex]上,e作为新元素的下一个节点
	table[bucketIndex] = new Entry<K,V>(hash,key,value,e);
	//当HashMap中的数组中的实际元素数量大于等于“阈值”时,需要对HashMap进行扩容
	if(size++ >= threshold)
		resize(2 * table.length);
}

上面的这个方法没有经过任何同步以及加锁的操作,是线程不安全的。当A线程和B线程同时对同一个数组下标位置调用addEntry方法时,两个线程会同时得到同一个头节点,然后A线程写入新的头节点后,B线程也写入一个新的头节点,那么B线程的写入操作就会覆盖A线程的写入操作,从而导致数据丢失。

2.在删除键值对时,会调用以下代码:

final Entry<K,V> removeEntryForKey(Object key){
	//获取哈希值,当key为null时,哈希值为0;
	//当key不为null时,调用hash()计算对应的哈希值
	int hash = (key == null) ? 0 : hash(key.hashCode());
	int i = indexFor(hash, table.length);
	Entry<K,V> prev = table[i];
	Entry<K,V> e = prev;

	//删除HashMap中键为key的元素
	//也可以理解为删除单向链表中的元素
	while(e != null){
		Entry<K,V> next = e.next;
		Object k;
		if(e.hash == hash && 
			((k = e.key) == key || (key != null && key.equals(k)))){
			modCount++;
			size--;
			if(prev == e)
				table[i] = next;
			else
				//e为当前节点
				//将e的前驱节点的后继节点设置为e的后继节点
				prev.next = next;
			e.recordRemoval(this);
			return e;
		}
		prev = e;
		e = next;
	}
}

上面这个方法也没有经过任何同步以及加锁的操作,也是线程不安全的。如果有多个线程同时操作同一个数组的同一个下标位置时,这些线程也都会取得现在状态下该位置存储的头节点,然后各自去进行计算操作,之后再把结果写到该数组位置上。但是在写回的时候,有可能其他线程已经将该位置上的元素修改过了,再进行写操作时,就会覆盖掉其他线程的操作。

3.当我们需要给HashMap扩容时,会调用以下代码:

void resize(int newCapacity){
	Entry[] oldTable = table;
	int oldCapacity = oldTable.length;
	//如果容量已达最大值,则不能扩容
	if(oldCapacity == MAXIMUM_CAPACITY){
		threshold = Integer.MAX_VALUE;
		return;
	}
	//新建一个HashMap,将原来的HashMap中的元素全部添加到新的HashMap中
	//再将新的HashMap赋值给旧的HashMap,就实现了扩容的目的
	Entry[] newTable = new Entry[newCapacity];
	transfer(newTable);
	table = newTable;
	threshold = (int)(newCapacity * loadFactor);
}

这个操作会在HashMap内部产生一个新的数组,然后对原数组的所有键值对进行重新计算位置并写入新的数组中,再让table指向新的数组。当有多个线程同时需要进行扩容操作调用resize方法时,每个线程会各自生成新的数组并重新哈希后赋值给该map内部的数组table,最后的结果是只有一个线程生成的新数组会被赋值给table变量,其他线程的新数组均会丢失。并且当某些线程已经完成数赋值而其他线程刚开始的时候,就会使用已经被赋值过的table作为原始数组,这样也会产生问题

JDK1.8并发问题

HashMap中迭代器源码:

abstract class HashIterator{
	Node<K,V> next;
	Node<K,V> current;
	int exceptedModCount;
	int index;

	HashIterator(){
		exceptedModCount = modCount;
		Node<K,V>[] t = table;
		current = next = null;
		index = 0;
		if(t != null && size > 0){
			do{}while(index < t.length && (next = t[index++]) == null);
		}
	}
	
	public final boolean hasNext(){
		return next != null;
	}

	final Node<K,V> nextNode(){
		Node<K,V>[] t;
		Node<K,V> e= next;
		if(modCount != exceptedModeCount)
			throw new ConcurrentModificationException();
		if(e == null)
			throw new NoSuchElementException();
		if((next = (current = e).next) == null && (t = table) != null){
			do{}while(index < t.length && (next = t[index++]) == null);
		}		
		return e;
	}
}

注意:
modCount是HashMap中的成员变量,可以将它理解为HashMap的版本号,来记录HashMap是否被操作过;
在调用put(), remove(), clear(), ensureCapacity()这些会修改数据结构的方法中,都会使modCount++;
在获取迭代器的时候会把modCount赋值给exceptedModCount,此时二者相等;
在迭代元素的过程中如果HashMap调用自身的方法使集合发生变化,那么就会修改modCount的值,此时modCount与exceptedModCount的值不相等;
在迭代的过程中,如果发现modCount与exceptedModeCount不相等,则说明HashMap结构发生了变化,所以也就没有必要继续迭代了,此时会抛出ConcurrentModificationException,终止迭代操作;

HashMap中并发问题的解决

我们可以通过Synchronized关键字、Lock锁、同步类容器、并发类容器来解决并发问题

同步容器介绍

在Java中,同步容器主要分为两类:
①:Vector、Stack、HashTable等(可以独立创建)
②:Collections类中提供的静态工厂方法创建的类(借助工具类创建)

**Vector:**实现了List接口,Vector的底层实际上就是一个数组,和ArrayList类似,但不同的是,Vector中的方法都是加了synchronized关键字的,也就是进行了同步措施(目前很少使用Vector)

**Stack:**也是一个同步容器,继承于Vector类

**HashTable:**实现了Map接口,它与HashMap类似,但是HashTable进行了同步处理

**Collections:**是一个工具提供类,需要注意的是,它与Collection不同,Collection是一个顶层的接口。在Collections类中提供了大量的方法,比如对集合或者容器进行排序、查找等操作。另外,在Collections类中提供了几个静态工厂方法来创建同步容器类,如下图:
在这里插入图片描述
下面以HashTable为例分析同步容器的实现原理和特点:

HashTable

HashTable在jdk1.1的时候就有了,它是如何实现线程安全的?它的put、remove、get方法是进行了同步控制的

public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length; //获取数组待插入的下标
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        return null;
    }
public synchronized V remove(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length; 
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>)tab[index];
        for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                modCount++;
                if (prev != null) {
                    prev.next = e.next;
                } else {
                    tab[index] = e.next;
                }
                count--;
                V oldValue = e.value;
                e.value = null;
                return oldValue;
            }
        }
        return null;
    }

由于HashTable的remove、get等方法都是用了synchronized关键字修饰的,所以HashTable是线程安全的,但是synchronized又是一个重量级锁,加上之后代码的效率就会下降,所以HashTable的效率较低

并发容器介绍

同步容器的基本原理就是在每个方法上加上synchronized关键字,这样虽然可以保证我们的方法是线程安全的,但是这严重地降低了代码执行的效率。为了让我们的容器既实现线程安全,又不降低代码的执行效率,Java在jdk1.5中引入了并发性能较好的并发容器,引入了java.util.concurrent包。

ConcurrentHashMap:
对应的非并发容器:HashMap
目标:代替HashMap、synchronizedMap,支持复合操作
原理:JDK1.6中采用了一种更加细粒度的加锁机制Segment“分段锁”,JDK1.8中采用了CAS无锁算法

CopyOnWriteArrayList:
对应的非并发容器:ArrayList
目标:代替Vector、synchronizedList
原理:利用高并发往往是读多写少的特性,对读操作不加锁而写操作才加锁;对于写操作,先复制一份新的集合,然后再新的集合上进行修改,再将新的集合赋值给旧的引用,并且利用volatile关键字来保证其可见性。

CopyOnWriteArraySet:
对应的非并发容器:HashSet
目标:代替synchronizedSet
原理:基于CopyOnWriteArrayList实现,其不同之处在于它的add方法调用的是CopyOnWriteArrayList中的addIfAbsent方法,也就是遍历当前Object数组,如果Object数组中以及存在了当前元素,那么就直接返回,如果没有则直接放入Object数组的尾部,并返回

ConcurrentSkipListMap:
对应的非并发容器:TreeMap
目标:代替synchronizedSortedMap(TreemMap)
原理:SkipList是一种可以媲美平衡树的数据结构,默认是按照key值升序的。SkipList将已排序的数据分布在多层链表中,以0-1随机数来决定一个数据向上攀升与否,这是一种利用空间来换时间的算法。ConcurrentSkipListMap提供了一种线程安全的并发访问的排序映射表,其内部是通过SkipList结构实现的,在理论上来说,其查找、插入、删除操作的时间复杂度为O(log n)

ConcurrentSkipListSet:
对应的非并发容器:TreeSet
目标:代替synchronizedSortedSet
原理:内部基于ConcurrentSkipListMap实现

下面以ConcurrentHashMap为例,深入分析其数据结构在jdk1.7之前和jdk1.8中的区别:

ConcurrentHashMap数据结构

jdk1.7中基于分段的数据结构:
在这里插入图片描述
put方法:

public V put(K key, V value){
	Segment<K,V> s;
	if(value == null){
		throw new NullPointerException();
	}
	//计算key的hash值
	int hash = hash(key);
	//根据hash值找到Segment数组中的位置
	int j = (hash >>> segmentShift) & segmentMask;
	if((s = (Segment<K,V>)UNSAFE.getObject
	(segments,(j << SSHIFT) + SBASE)) == null)
		s = ensureSegment(j);
	//插入新值到槽s中
	return s.put(key, hash, value, false);
}

Segment继承了ReentrantLock,其内部的put方法(上述代码里的s.put(key,hash,value,false))如下:

final V put(K key, int hash, V value, boolean onlyIfAbsent){
	//在向该segment段中写入数据前,需要获取到该segment的独占锁
	HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
	V oldValue;
	try{
		//这是segment内部的数组
		HashEntry<K,V>[] tab = table;
		//再利用hash值,求应该放置的数组下标
		int index = (tab.length - 1) & hash;
		//first是数组该位置处的链表的表头
		HashEntry<K,V> first = entryAt(tab, index);

		for(HashEntry<K,V> e = first ;;){
			if(e != null){
				K k;
				if((k = e.key) == key || (e.hash == hash && key.equals(k))){
					oldValue = e.value;
					if(!onlyIfAbsent){
						//覆盖旧值
						oldValue = e.value;
						++modCount;
					}
					break;
				}
				//继续遍历链表
				e = e.next;
			}else{
				// node 是否为null,这个得看获取锁的过程
				//如果node不为null,那就直接将他设为链表的表头,如果为null,初始化并设置为链表表头
				if(node != null)
					node.setNext(first);
				else
					node = new HashEntry<K,V>(hash, key, value, first);
				int c = count + 1;

				//当超过segment的阈值的时候,我们需要为segment进行扩容
				if(c > threshold && tab.length < MAXIMUM_CAPACITY){
					rehash(node);
				}else
					//没有达到阈值,则将node放到数组tab的index位置
					//也就是将新节点设置为该index位置的链表的表头
					setEntryAt(tab, index, node);
				++modCount;
				count = c;
				oldValue = null;
				break;
			}
		}
	}finally{
		//解锁
		unlock();
	}
	return oldValue;
}

scanAndLockForPut()方法获取锁,对应于上边的scanAndLockForPut(key, hash, value)
如果tryLock成功了,循环终止;
当重试的次数超过了MAX_SCAN_RETRIES,则进入到lock方法,lock方法会阻塞等待,直到成功拿到独占锁

以下为代码演示:

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value){
	//根据hash值找到segment中的HashEntry节点
	HashEntry<K,V> first = entryForHash(this, hash); //首先获取头节点
	HashEntry<K,V> e = first;
	HashEntry<K,V> node = null;
	int retries = -1;

	//循环获取锁
	while(!tryLock){ //持续遍历该哈希链
		HashEntry<K,V> f;//首先再检查下面
		if(retries < 0){
			if(e == null){
				if(node == null)
					//代码走到这一步说明数组该位置的链表为空,没有任何元素
					//还有一个原因是,tryLock失败,所以该位置存在并发问题,所以不一定是该位置
					node = new HashEntry<K,V>(hash, key, value, null);
				retries = 0;
			}else if(key.equals(e.key))
				retries = 0;
			}else
				//顺着链表继续走
				e = e.next;
	}else if(++retries > MAX_SCAN_RETRIES){
	//重试的次数如果超过了MAX_SCAN_CAPACITY,那么就不再尝试获取锁了,而是直接进入阻塞队列等待锁‘
	//lock是阻塞方法,直到获取锁后返回
		lock();
		break;
	}else if((retries & 1) == 0
			(f = entryForHash(this, hash)) != first){
		e = first = f;
		retries = -1;
	}
}
return node;

jdk1.8中基于CAS的数据结构:
在这里插入图片描述
put方法:

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

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();
            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;
                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) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值