6

项目里两个地方都用到了hashmap。但是感觉自己用的时候并没有感觉非常的清晰。同时发现hashmap有线程不安全问题,而自己用的时候就是多线程来使用。于是在这里介绍一下。

项目中两个地方用到了hashmap。

1.策略模式

一个是使用hashmap来储存service, 根据不同的事件调用不同的service。

image.png

在构造函数中,把service添加到map中, 然后使用的时候,根据eventName获取相应的service进行处理。

@Service
1 public class GitLabNotifyServiceImpl implements GitLabNotifyService {

2   private Map<String, EventService> map = new HashMap<>();

3  public GitLabNotifyServiceImpl(PushEventService pushEventService,
4                                 IssueEventService issueEventService,
5                                 CommentEventService) {
6    this.addService(issueEventService);
7    this.addService(pushEventService););
  }

  @Override
8  public void handleEventData(String json, String eventName,String access_token) throws IOException {
9    EventService eventService = this.map.get(eventName);
   }

2.合并事件

开发背景:

gitlab中有评论并关闭的按钮

image.png

点击后钉钉机器人会发送两条通知
image.png

于是就有需求是合并事件。

测试发现,总是评论事件优于issue事件发送。
image.png

实现逻辑

然后就写了个方法。

由于spring boot的bean默认都是单例的,所以定义了一个hashmap, 记录当前要执行的gitlab的事件

 /**
     * key: access_token, github钉钉机器人的token
     * value GitlabEvent,  gitlab事件
     */
    private static final HashMap<String, String> hashMap = new HashMap<>();

两个事件发送后,会有comment请求和issue close请求短间隔被服务器接受。

服务器会开启两个线程。

两个线程都对hashmap进行访问。

当前事件为comment事件的时候: 向hashMap中记录对应钉钉机器人事件是comment,线程先睡眠两秒,若后续有issue close事件,则合并事件。即不发送isssue close事件,并在comment事件上增加已关闭的提示信息。

当前事件为issue close事件的时候: 若2s前有comment事件,并且是同一个issue,则不发送issue close事件。

public String commentAndIssueClose(String json, String eventName, String access_token) throws IOException {

if (Objects.equals(eventName, GitlabEvent.issueHook)) {
    // 若issue事件前面有comment事件,且为issue为close事件 则不发送该事件
    if (Objects.equals(hashMap.get(access_token), GitlabEvent.noteHook) && this.judgeIsIssueClose(json)) {
        hashMap.put(access_token, GitlabEvent.issueHook);
        // 返回null 表示不发送
        return null;
    }
} else if (Objects.equals(eventName, GitlabEvent.noteHook)) {
    try {
        hashMap.put(access_token, GitlabEvent.noteHook);
        // 因为评论事件先于issue事件发送, 所以评论事件等待两秒
        TimeUnit.SECONDS.sleep(2);
        // 若2s后存在issue事件 则事件合并
        if (Objects.equals(hashMap.get(access_token), GitlabEvent.issueHook)) {
            hashMap.put(access_token, "");
            return this.setIssueTittleClose(json);
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

}
// 清空value
hashMap.put(access_token, "");
return json;
}

合并结果,前面加上已关闭

image.png

HashMap线程不安全

虽然写完了能实现,但是找资料的时候,发现HashMap线程不安全。 就想着我也是多线程访问,我这么写是不是会出bug。

先来说一下为什么hashMap的结构以及为什么线程不安全。

结构
HashMap是采用“链表数组”的数据结构,即数组和链表的结合体(在JDK1.8中还引入了红黑树结构,当一个链表上的结点达到8时改为红黑树结构)。
image.png

HashMap底层维护一个数组,数组中的每一项都是一个Entry, 如下

transient Entry<K,V>[] table;

我们向 HashMap 中所放置的对象实际上是存储在该数组当中。

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

而这个Entry应该放在数组的哪一个位置上,是通过key的hashCode来计算的。

final int hash(Object k) {
        int h = 0;
        h ^= k.hashCode();
 
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
}

通过hash计算出来的值将会使用indexFor方法找到它应该所在的table下标:

static int indexFor(int h, int length) {
        return h & (length-1);
}

当两个key通过hashCode计算相同时,则发生了hash冲突(碰撞),HashMap解决hash冲突的方式是用链表。

总结:
当向 HashMap 中 put 一对键值时,

  1. 根据 key的 hashCode 值计算出一个位置, 该位置就是此对象准备往数组中存放的位置。
  2. 如果该位置没有对象存在,就将此对象直接放进数组当中;
  3. 如果该位置已经有对象存在了,则顺着此存在的对象的链开始寻找(为了判断是否是否值相同,map不允许<key,value>键值对重复)
  4. 进行尾插法,插入到链表后面。(jdk1.8之前是头插法)

HashMap线程不安全的体现:

JDK1.7 HashMap线程不安全体现在:死循环、数据丢失

JDK1.8 HashMap线程不安全体现在:数据覆盖

JDK1.7 中,由于多线程对HashMap进行扩容,调用了HashMap.transfer(),具体原因:某个线程执行过程中,被挂起,其他线程已经完成数据迁移,等CPU资源释放后被挂起的线程重新执行之前的逻辑,数据已经被改变,造成死循环、数据丢失


JDK1.8 中,由于多线程对HashMap进行put操作,调用了HashMap.putVal(),具体原因:假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行中后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。
改善:

数据丢失、死循环已经在在JDK1.8中已经得到了很好的解决,因为JDK1.8直接在HashMap#resize()中完成了数据迁移。

如何线程安全的使用HashMap

了解了 HashMap 为什么线程不安全,那现在看看如何线程安全的使用 HashMap。以下三种方式:

  • Hashtable
  • ConcurrentHashMap
  • Synchronized Map
//Hashtable
Map<String, String> hashtable = new Hashtable<>();
//synchronizedMap
Map<String, String> synchronizedHashMap = Collections.synchronizedMap(new HashMap<String, String>());
//ConcurrentHashMap
Map<String, String> concurrentHashMap = new ConcurrentHashMap<>();

前两种是使用,synchronized 来保证线程安全的。 最后一种是是 JUC 包中的一个类, 效率比较高


weiweiyi
1k 声望123 粉丝