JAVA并发编程 并发容器 HashMap这一块 JDK1.7HashMap死循环原因

认识hash

hash

就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变 换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是, 散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所 以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压 缩到某一固定长度的消息摘要的函数。常用 HASH 函数:直接取余法、乘法取整 法、平方取中法。

处理冲突方法:

1.开放寻址法;

2. 再散列法:

3. 链地址法(拉链法) 常用 hash 算法的介绍:(1)MD4(2)MD5 它对输入仍以 512 位分组,其输 出是 4 个 32 位字的级联(3)SHA-1 及其他。

位运算

二 进 制

从小学一年级开始,我们就开始学加减乘除,特别是学加法的时候,刚开始算超过十的时候,如果不熟练,我们会掰着手指头数,有时候恨不得把脚趾头也加上。正是因为我们一般人有十个手指头,所以,从我们老老。。。祖先开始就 用十进制。那计算机怎么办?计算机又没十个手指头。自然界要找出有十种状态 的东西很难,但是只有两种状态的东西或者现象还是很容易的,所以计算机中要用二进制。 

逢十进一,意味着至少两点,1,每个位上的数字不能超过 10,最大只能到 9,第二,如果某个位上的数字因为运算超过了 10,怎么办?往高位进位。那么 二进制也是一样的,不能说你二进制比较二,规则就不一样了,基本法还是要遵 守的。所以二进制里同样要遵循:1,每个位上的数字不能超过 2,最大只能到 1, 第二,如果某个位上的数字因为运算超过了 2,怎么办?往高位进位。

 常用位运算 

位与 &(1&1=10&0=01&0=0)

位或 | (1|1=10|0=01|0=1)

位非 ~(~1=0 ~0=1)

位异或 ^(1^1=01^0=1 0^0=0)

有符号右移>>(若正数,高位补 0,负数,高位补 1)

有符号左移<<

无符号右移>>>(不论正负,高位均补 0)

有趣的取模性质:取模 a%(2^n) 等价于 a&(2^n-1),所以在 map 里的数 组个数一定是 2 的乘方数,计算 key 值在哪个元素中的时候,就用位运算来快速定位。  

/*
* 位运算
* */
public class IntToBinary {
	
    public static void main(String[] args) throws UnsupportedEncodingException {

        System.out.println("the 4 is : " + Integer.toBinaryString(4));
        System.out.println("the 6 is : " + Integer.toBinaryString(6));
        //位与&(真真为真 真假为假 假假为假)
        System.out.println("the 4&6 is : " + Integer.toBinaryString(6&4));
        //位或|(真真为真 真假为真 假假为假)
        System.out.println("the 4|6 is : " + Integer.toBinaryString(6|4));
        //位非~
        System.out.println("the ~4 is : " + Integer.toBinaryString(~4));
        //位异或^(真真为假 真假为真 假假为假)
        System.out.println("the 4^6 is : " + Integer.toBinaryString(6^4));
        //有符号右移>>(若正数,高位补0,负数,高位补1)
        System.out.println("the 4>>1 is : " + Integer.toBinaryString(4>>1));
        //有符号左移<<(若正数,高位补0,负数,高位补1)
        System.out.println("the 4<<1 is : " + Integer.toBinaryString(4<<1));
        //无符号右移>>>(不论正负,高位均补0)
        System.out.println("the 234567 is : " + Integer.toBinaryString(234567));
        System.out.println("the 234567>>>4 is : " + Integer.toBinaryString(234567>>>4));
        //无符号右移>>>(不论正负,高位均补0)
        System.out.println("the -4 is : " + Integer.toBinaryString(-4));
        System.out.println("the -4>>>4 is : " + Integer.toBinaryString(-4>>>4));
        System.out.println(Integer.parseInt(Integer.toBinaryString(-4>>>4), 2));
        
        //取模a % (2^n) 等价于 a & (2^n - 1) 
        System.out.println("the 345 % 16 is : " + (345%16)+" or "+(345&(16-1)));
        
        System.out.println("Mark hashCode is : "+"Mark".hashCode()+"="
        		+Integer.toBinaryString("Mark".hashCode()));
        System.out.println("Bill hashCode is : "+"Bill".hashCode()+"="
        		+Integer.toBinaryString("Bill".hashCode()));        
        
    } 
}

位运算运用场景 

Java 中的类修饰符、成员变量修饰符、方法修饰符,比如 Class 类中

Java 容器中的 HashMap 和 ConcurrentHashMap 的实现

权限控制或者商品属性 简单可逆加密,比如异或运算(1^1=0 ;0^1=1)

实战:将位运算用在权限控制、商品属性上

/**
 * 位运算的运用-权限控制,add,query,modify,del
 */
public class Permission {

    private static final int ALLOW_SELECT = 1<<0;
    private static final int ALLOW_INSERT = 1<<1;
    private static final int ALLOW_UPDATE = 1<<2;
    private static final int ALLOW_DELETE = 1<<3;

    //当前的权限状态
    private int flag;

    public void setPermission(int permission){
        flag = permission;
    }


    /*增加权限,可以一项或者多项*/
    public void addPermission(int permission){
        flag = flag|permission;
    }

    /*删除权限,可以一项或者多项*/
    public void disablePermission(int permission){
        flag = flag&~permission;
    }

    /*是否拥有某些权限*/
    public boolean isAllow(int permission){
        return (flag&permission)==permission;
    }

    /*是否不拥有某些权限*/
    public boolean isNotAllow(int permission){
        return (flag&permission)==0;
    }


    public static void main(String[] args) {
		int flag = 15;
		Permission permission = new Permission();
		permission.setPermission(flag);
		permission.disablePermission(ALLOW_DELETE|ALLOW_INSERT);
		System.out.println("ALLOW_SELECT="+permission.isAllow(ALLOW_SELECT));
		System.out.println("ALLOW_INSERT="+permission.isAllow(ALLOW_INSERT));
		System.out.println("ALLOW_UPDATE="+permission.isAllow(ALLOW_UPDATE));
		System.out.println("ALLOW_DELETE="+permission.isAllow(ALLOW_DELETE));
	}
}

使用位运算的优劣势:

节省很多代码量、效率高、属性变动影响小、不直观 

为什么要使用ConcurrentHashMap

JDK1.7中HashMap死循环分析

在多线程环境下,使用 HashMap 进行 put 操作会引起死循环,导致 CPU 利 用率接近 100%,HashMap 在并发执行 put 操作时会引起死循环,是因为多线程 会导致 HashMap 的 Entry 链表 形成环形数据结构,一旦形成环形数据结构, Entry 的 next 节点永远不为空, 就会产生死循环获取 Entry。

那么这个死循环是如何生成的呢?我们来仔细分析下。

HashMap扩容流程
原理
引发死循环,是在 HashMap 的扩容操作中。 

正常的扩容操作是这个流程。HashMap 的扩容在 put 操作中会触发扩容,主要是三个方法:

综合来说,HashMap 一次扩容的过程:

1、取当前 table 的 2 倍作为新 table 的大小

2、根据算出的新 table 的大小 new 出一个新的 Entry 数组来,名为 newTable

3、轮询原 table 的每一个位置,将每个位置上连接的 Entry,算出在新 table 上的位置,并以链表形式连接

4、原 table 上的所有 Entry 全部轮询完毕之后,意味着原 table 上面的所有 Entry 已经移到了新的 table 上,HashMap 中的 table 指向 newTable

实例
现在 hashmap 中有三个元素,Hash 表的 size=2, 所以 key=3,7,5,在 mod2 以后都冲突在 table[1]这里了。

按照方法中的代码

对 table[1]中的链表来说,进入 while 循环,此时 e=key(3),那么 next=key(7), 经过计算重新定位 e=key(3)在新表中的位置,并把 e=key(3)挂在 newTable[3]的位 置

这样循环下去,将 table[1]中的链表循环完成后,于是 HashMap 就完成了扩容

并发下的扩容
上面都是单线程下的扩容,当多线程进行扩容时,会是什么样子呢? 初始的 HashM 还是:

我们现在假设有两个线程并发操作,都进入了扩容操作,
我们以颜色进行区分两个线程。
我们假设,线程 1 执行到 Entry<K,V>next=e.next;时 被操作系统调度挂起了,而线程 2 执行完成了扩容操作

于是,在线程 1,2 看来,就应该是这个样子

接下来,线程 1 被调度回来执行: 

1)

2)

3)

4)

 5)

6)

7)

循环列表产生后,一旦线程 1 调用 get(11,15 之类的元素)时,就会进入一 个死循环的情况,将 CPU 的消耗到 100%。

总结 

HashMap 之所以在并发下的扩容造成死循环,是因为,多个线程并发进行 时,因为一个线程先期完成了扩容,将原 Map 的链表重新散列到自己的表中, 并且链表变成了倒序,后一个线程再扩容时,又进行自己的散列,再次将倒序链 表变为正序链表。于是形成了一个环形链表,当 get 表中不存在的元素时,造成 死循环。 

发布了30 篇原创文章 · 获赞 32 · 访问量 618

猜你喜欢

转载自blog.csdn.net/weixin_42081445/article/details/105104118
今日推荐