HashMap非线程安全的表现

前言

我们都知道HashMap是线程不安全的,但是具体有哪些表现呢?

例子1 死循环

这个是最常在面试中问到的问题,然而其实这个问题已经在java1.8版本被修复了,只在1.7版本之前存在这个问题。
大致原因是在HashMap扩容的时候链表采用了头插法会使链表反序,两个线程同时扩容的话,在某种场景下会出现循环链表导致死循环。网上有很多文章介绍,有一篇写的很好,不再重复

java1.8修复的方法是将头插法改成了尾插法,避免了这个死循环问题。参考文章,也可以看一下resize的代码,很容易理解。

那么java1.8就不会出现死循环问题了吗?不是的,在多线程操作红黑树的时候依然有可能会导致死循环,参考文章,不过这个地方的具体原因没有找到详细文章说明,待证明。

例子2 哈希冲突导致覆盖

场景:如果一个空的HashMap当前的Capacity=8,两个线程分别put进key=1和key=9的数据,在某种场景下可能会导致只有一个值能被传入,且size会增长2。
代码如下,需要单步调试进入

	public static void main(String[] args) throws InterruptedException {
        final HashMap<Integer, String> hashMap = new HashMap<>(8);

        CountDownLatch countDownLatch = new CountDownLatch(2);
        new Thread(() -> {
            hashMap.put(1, "1");
            countDownLatch.countDown();
        }).start();

        new Thread(() -> {
            hashMap.put(9, "9");
            countDownLatch.countDown();
        }).start();

        countDownLatch.await();
    }

两个断点如下,进行多线程调试(idea断点类型和多线程调试可以在网上搜索)
在这里插入图片描述
令两个线程都走到这一行代码,然后分别执行完后边的代码,回到main方法。
在这里插入图片描述
最后会发现HashMap的size=2,但是只有一个元素。
在这里插入图片描述
这个原因其实就是HashMap保存元素用的是一个Node数组,hash取值相同的Node会放在数组的同一个位置,然后采用拉链法或者是红黑树保存元素。
当数组的某个索引处还没有元素的时候,会去初始化一个元素,然后赋值到该数组的索引处。如果两个线程放的元素hash值相同,且都发现该索引处没有值的时候,都会初始化一个值然后进行赋值,这时候就会出现二者互相覆盖的情况。所以最终只有一个元素被放到了HashMap中,另一个成了垃圾等待回收……

共性

其实要说明HashMap线程不安全,有非常多的表现,但是没有必要一一列举。

之前写的文章中已经说了:线程不安全的本质原因就是有竞态条件。对于共享资源的修改不能做到原子化、对顺序敏感的时候就会导致线程不安全,上面的所有情况根因都是这个,只是表现形式不同而已。

所以,基本上掌握了这个本质,就可以分析所有线程不安全的情况。

HashTable

我们知道,HashTable是线程安全的,它是给所有方法增加了synchronized。

  • 这个方法可以达到线程安全吗?可以
  • 这种方法的原理是什么?原理是synchronized都需要获取当前实例的锁,static synchronized都需要获取当前类的锁。至于再深入的说,monitorenter和monitorexit这种指令具体怎么实现,就不是很了解了。
  • synchronized和static synchronized修饰的两个方法之间是同步的吗?不是,因为需要获取的锁不一样。如下代码所示,func1和func4是同步的,func2和func3是同步的,断点调试会发现当一个线程获取到锁之后,另一个线程在尝试获取锁的时候会进入到MONITOR状态等待锁。
import java.util.HashMap;
import java.util.concurrent.CountDownLatch;

public class H {
    public static synchronized void func1() {
        System.out.println("func1");
    }

    public synchronized void func2() {
        System.out.println("func2");
    }

    public void func3() {
        synchronized (this) {
            System.out.println("func3");
        }
    }

    public void func4() {
        synchronized (H.class) {
            System.out.println("func4");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        H h = new H();
        new Thread(() -> {
            h.func1();
        }).start();
        new Thread(() -> {
            h.func2();
        }).start();
        new Thread(() -> {
            h.func3();
        }).start();
        new Thread(() -> {
            h.func4();
        }).start();

        CountDownLatch countDownLatch = new CountDownLatch(1);
        countDownLatch.await();
    }
}

  • HashTable的实现有什么问题?问题很明显,加锁的粒度太粗,我们如果只是为了线程安全,只需要在产生竞态条件的地方加锁就可以了,不需要全部代码加锁,会使性能表现不好。

ConcurrentHashMap

我们知道,多线程下一定要用ConcurrentHashMap。但是它是怎么做到线程安全的呢?这个还是很有意思的。

有一篇ConcurrentHashMap是如何实现线程安全的写的很好
还有看的一个公众号的文章写的很好,记录一下
死磕 java集合之ConcurrentHashMap源码分析(一)
死磕 java集合之ConcurrentHashMap源码分析(二)
死磕 java集合之ConcurrentHashMap源码分析(三)

待自己手撸一遍代码。。

ConcurrentHashMap一定是线程安全的吗?

首先,是的。如果不是,是因为你对ConcurrentHashMap的线程安全有误解。看下面的例子,这段代码不是线程安全的,最终输出的结果未必是2000。

但这不能说明ConcurrentHashMap不是线程安全的,只能说明下面这段代码不是线程安全的

线程安全是需要考虑一个代码范围或者逻辑范围的,在这个范围内如果程序对多线程下各个环节的执行顺序敏感的话,就存在竞态条件,存在竞态条件的代码不做同步的话,就会出现线程不安全的情况。

所以,ConcurrentHashMap提供的方法是线程安全的,但是先get数据、进行计算、然后再put数据这种逻辑是线程不安全的。

public class Test {
    static final ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

    public static void increment(String key) {
        map.put(key, map.getOrDefault(key, 0) + 1);
    }

    public static void main(String[] args) throws InterruptedException, IOException {
        CountDownLatch latch = new CountDownLatch(2);
        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                increment("test");
                latch.countDown();
            }
        });

        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                increment("test");
                latch.countDown();
            }
        });

        latch.await();
        System.out.pringln(map.size());
    }
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值