HashMap纠错

hashMap 相信大家都很熟,数据结构,1.7和1.8的变化啥的我们这篇文章也不去聊它,网上有海量的优秀文章,我们就不去做搬运工了。下面这张图,是我几年前学习时候,收藏的某个大牛写的hashMap文章中的一段。


1.7版本的hashMap在多线程情况下会出现死锁(不了解的同学可以google下),1.8的hashMap因为resize方式的变化已经不会出现死循环了。 这个时候图片中说到,多线程情况下还是推荐使用ConcurrentHashMap,但是它讲述的原因是“无法保证上一秒put的值,下一秒还是原来的值”。

我仔细回味了好几遍这句话,好像ConcurrentHashMap也是做不到的啊,甚至HashTable也是做不到的啊,如果要做的话那需要加锁保证put 和 get 是同一个原子操作啊!

我已经在很多地方看到,讲述在1.8为什么要使用ConcurrentHashMap的时候,有类似上文的描述了。本文写作hashMap纠错,因为1.8下,HashMap是有很多线程不安全的原因的。图中原因的背后,其实是对线程安全和java内存模型在hashMap环境下理解不够深入。

首先,什么是线程安全呢?

当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类时线程安全的。

举个例子

synchronized (this) {    i++;}复制代码

比如如上的代码,如果分别在两个线程中去执行,首先它肯定是线程安全的,但是,你从一个线程中get出来的结果,它就是不一定和你当时刚执行完加操作后的结果是一样的。这个和我们上面说的是一样的,线程安全,并不是用来保证,你取出来的值还是你存的值。

然后,我们开始讲述下HashMap 在1.8下线程不安全的原因,分四个部分。

第一个部分,不详细说部分:

诸如没有加锁的++size,++modcount等。

第二个部分,非原子操作产生的链节点丢失

如下图所示,两个线程同时执行put,两个的key值都hash到同一个桶上,初始状态下,两个线程判断a节点的下一个节点都是null,所以,直接新增节点。此时

线程1:a.next = b
线程2:a.next = c复制代码

线程1的结果被线程2覆盖。


第三个部分,内存可见性问题导致的链表问题

HashMap其实也存在可见性问题导致的并发安全的问题,但是,绝大部分的可见性问题都被非原子操作掩盖

还是场景的场景:

i++;复制代码

如果多线程执行上述代码,有两种情况会产生问题。

Thread1: read i
Thread2: read i
Thread1: i+1;
Thread2: i+1;复制代码

另一种情况:

Thread1: read i
Thread1: i+1; (未写回主内存)
Thread2: read i
Thread2: i+1;复制代码

HashMap涉及链表操作时,其实除了上面第二种情况,也会因为内存可见性问题,产生相关的并发问题。因为有些情况下可见性问题会被非原子操作掩盖(你没有办法确定时非原子操作造成,还是可见性造成),我们可以通过下面的代码,证明内存可见性在链表操作时候的问题。

class ThreadDemo1 implements Runnable {

    static class Node{
        Node next;
        int a;
    }

    private Node node = new Node();

    public Node getNode() {
        return node;
    }

    public void setNode(Node node) {
        this.node = node;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(200);
            node.next =new Node();
        } catch (Exception e) {
        }
        System.out.println("其他线程node.next !=null");
    }

    public static void main(String[] args) throws Exception {
        ThreadDemo1 td = new ThreadDemo1();
        new Thread(td).start();

        while (true) {
            //现在唯一的问题是如果下面是1。那么永远读取不到改变,但是如果是0,会读取到改变
            if (td.getNode().next!=null) {
                System.out.println("主线程node.next = null");
                //break;
            }
        }
    }
}

复制代码

代码里通过一个线程给链表新增节点。主线程去判断节点是否有后继节点。运行代码可以发现,主线程永远都不回读取到有后续节点的情况。

运行结果就不上图了,动手看看印象才会更深。从示例代码延伸开来可以发现,在多线程操作HashMap的时候,也会因为可见性导致节点丢失等问题。

补充一点:如果你把示例代码中td.getNode.next!=null 改成td.getNode.next==null 那么主线程将能够读取到线程的修改。 这个应该与java编译方式有关系。

第四部分,说不完的部分

红黑树在多线程环境下,可能产生自旋,这个再写下去,就天亮了。


升华下,这篇文章尽可能的没有重复讲述HashMap中大家众所周知的知识点,在很多地方也没有去做详细的示例,是希望本文可以引发大家更多的思考,从日常学习工作中,常见的场景中,去引发更多的思考。希望本文能给大家收获。




转载于:https://juejin.im/post/5d580cdae51d45620c1c53b4

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值