HashMap、HashTable与ConcurrentHashMap死锁问题

区别简述

HashMap是JAVA中最常见的键值对存储对象,但是存在线程安全问题,如果有多个线程同时操作HashMap对象,有可能出现锁问题。早期解决这个问题是使用HashTable,后来又推出性能更高的ConcurrentHashMap,HashTable和ConcurrentHashMap都是线程安全的类。

HashMap为啥会出现死锁

HashMap使用【数组+链表】的方式实现,其中有个功能是自动扩容。在操作HashMap的put方法时,如果发现table容量超过阀值时,会自动扩大数组长度(即自动扩容)。这样的设计出发点是合理利用内存空间,但也是导致死循环的问题所在。
扩容并不是简单的增加原有数组大小,HashMap是根据Key的Hash值计算出key应该在数组中的下标,不同的key值计算出数组下标相同时,叫做Hash碰撞,这种情况下会形成一个链表。在【自动扩容】时,由于数组容量变动,所以原来根据key的hash计算出来的数组下标会变化,相对来说原来数组中的数据成了散乱放置。必须新建一个数组把原来数组中的值转移到新的数组中,问题就在转移的这段代码。如下:


void transfer(Entry[] newTable)
{
    Entry[] src = table;
    int newCapacity = newTable.length;
    //从OldTable将元素一个个拿出来,然后放到NewTable中
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                //计算节点在新的Map中的位置
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

在链表中的每一个元素都会记录它的下一个值,转移数据的过程是把链表中的值从头部取出(取出的值叫做e),计算出e在新的数组中新的下标,e的next指向链表的原有的头部,成为链表新的头部,原来链表的头部就成了链表的第二个元素。这个逻辑产生的现象是,原先链表A->B ->C 的顺序,转移完成之后可能是C->B->A,链表顺序完全颠倒。顺序可以颠倒是多线程情况下,出现死锁的漏洞所在。
还原死循环的场景,假如有两个线程T1,T2都将操作同一个HashMap对象,T1在执行get方法,运行到已经查询到A的下一个值是B,将要查B的下一个值时挂起。T2运行执行扩容,扩容之后B的下一个值是A。这时T2挂起T1继续运行,查找B的下一个值,这时发现B的下一个值是A,就出现A-B-A-B…死循环。

HashMap 在table中下标的计算

【取模】【按位与】两种数学算法在计算机中【按位与】效率远远高于【取模】,为了提高效率table的大小都设置的是2的N次方。JDK1.8用【Hash值高16位与hash本身异或的值】与【table数组大小减1后的值】按位与,实现与取模一样的效果。相比JDK1.7做了4次扰动函数,JDK1.8只用了一次干扰函数,效率得到了提高。降低了干扰次数会不会导致获取的数组小标不够均匀?由于JDK1.8使用的是【Hash值高16位】与【hash本身】异或,最大程度保留了高位和低位的特性,使得数组下标值的获取分布更加均匀。

JDK1.8
// 代码1
static final int hash(Object key) { // 计算key的hash值
    int h;
    // 1.先拿到key的hashCode值; 2.将hashCode的高16位参与运算
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 代码2
int n = tab.length;
// 将(tab.length - 1) 与 hash值进行&运算
int index = (n - 1) & hash;
ava中有三种移位运算符
<<  :     左移运算符,num << 1,相当于num乘以2
>>  :     右移运算符,num >> 1,相当于num除以2
>>> :     无符号右移,忽略符号位,空位都以0补齐
& (按位与)    :同为1则为1,否则为0
| (按位或)    :任意一个为1则为1,否则为0
^ (按位异或)   :相同则为1,否则为0
JDK1.7
final int hash(Object k) {
	int h = hashSeed;
	if (0 != h && k instanceof String) {
		return sun.misc.Hashing.stringHash32((String) k);
	}

	h ^= k.hashCode();

	// This function ensures that hashCodes that differ only by
	// constant multiples at each bit position have a bounded
	// number of collisions (approximately 8 at default load factor).
    // 这里是扰动函数
	h ^= (h >>> 20) ^ (h >>> 12);
	return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
        return h & (length-1);
}

HashTable如何解决死锁问题

HashTable在put和get方法都加了sychonized关键字。
sychonized用在方法上时,作用于方法,锁住的是方法所在的对象,因此put方法和get方法不能被同时调用。加锁之后解决了死锁问题,但是在并发的情况下这种设计效率比较低。
HashTable与HashMap的区别
HashTable的Key和Value都不能为null,HashMap的key可以有一个null,value可以有多个null.

ConcurrentHashMap如何解决死锁问题

ConcurrentHashMap的实现在JDK1.7和JDK1.8中有所区别。

JDK1.8

ConcurrentHashMap是,在保证线程安全前提下,最高效的Map实现类,因此它的实现也最复杂。关于死锁问题的解决方式,简单来说就是灰度生效,get方法访问数据时,扩容过程中数组的每个值,列表或红黑树,作为原子操作的单位,扩容前访问新旧的数组,扩容后访问的是新的数组。

    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != 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;
    }
JDK1.7

相比JDK1.7的ConcurrentHashMap, JDK1.8做了非常大的改进。JDK1.7使用分数组(Segment) + 数组(HashEntry) + 链表(HashEntry节点)的结构,而JDK1.8是Node数组+链表 / 红黑树,所以1.8的性能更高。JDK1.8使用CAS+volatile 来控制并发, 锁的粒度在Node,对数据的影响很小。而JDK1.7的分段锁所的对象是Segment,颗粒度更大。

总结:

HashMap在扩容过程中会出现死锁,JDK1.8ConcurrentHashMap通过ForwordingNode解决查询时的数据源问题,同时volatile关键字的是使用保证CAS后的数据可及时同步到其他线程。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值