乐趣区

关于springboot:记录本周问题

我的项目中应用 hashMap

我的项目里两个中央都用到了 hashmap。然而感觉本人用的时候并没有感觉十分的清晰。同时发现 hashmap 有线程不平安问题,而本人用的时候就是多线程来应用。于是在这里介绍一下。

我的项目中两个中央用到了 hashmap。

1. 策略模式

一个是应用 hashmap 来贮存 service, 依据不同的事件调用不同的 service。

在构造函数中,把 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 中有评论并敞开的按钮

点击后钉钉机器人会发送两条告诉

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

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

实现逻辑

而后就写了个办法。

因为 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;
}

合并后果,后面加上已敞开

HashMap 线程不平安

尽管写完了能实现,然而找材料的时候,发现 HashMap 线程不平安。就想着我也是多线程拜访,我这么写是不是会出 bug。

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

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

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

transient Entry<K,V>[] table;

咱们向 HashMap 中所搁置的对象实际上是存储在该数组当中;
而 Map 中的 key,value 则以 Entry 的模式寄存在数组中

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 包中的一个类, 效率比拟高

退出移动版