V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
gramyang
V2EX  ›  Java

ConcurrentHashMap 的使用问题

  •  
  •   gramyang · 2019-06-12 07:41:42 +08:00 · 3967 次点击
    这是一个创建于 1752 天前的主题,其中的信息可能已经有所发展或是发生改变。

    昨天在 netty 的 handler 里碰到了一个非常奇怪的问题: 1、首先,handler 没有加 sharable 注解 2、我在 handler 的外部生成了一个 concurrenthashmap 实例并传入 handler 3、在 handler 的一个方法中调用 concurrenthashmap 的 remove(player.getNum()),然后再调用 player=null 将 player 清空。

    这个时候奇迹出现了,remove 报空指针,也就是 remove 的时候 player 是 null。 我检查完了所有的代码,再没有其他地方把 player 设置为 null,并且 remove 的操作前还判断了 player!=null。

    思来想去,只有两个可能: Java 中的指令重排序,导致 player 在 remove 之前就被空置,但是感觉不太可能啊。。。 concurrenthashmap 是多个线程共享的变量,直接 remove 会出现并发问题。。。

    请大神指导!!

    第 1 条附言  ·  2019-06-12 08:42:35 +08:00
    private void handleAfterExitOrException() {
    if(player != null && player.getSeatNum() > -1) {
    apiHandler.exitOrException();
    }
    if (player != null && player.getUserName() != null) {
    userName2Player.remove(player.getUserName());
    log.info("{}退出系统了", player.getUserName());
    }
    player = null;
    }

    public void exitOrException() {
    playerMap.remove(player.getSeatNum());
    Table thisTable = tableMap.get(player.getTableNum());
    thisTable.getPlayers().remove(player);
    int count = thisTable.getPlayCount();
    thisTable.setPlayCount(count - 1);
    thisTable.setPlay(false);
    thisTable.setRob(false);
    thisTable.setWait(true);
    //通知游戏房间里其他玩家
    ExitSeatResponse response = new ExitSeatResponse(player.getUserName(), player.getSeatNum(), refreshSeatNum2UserName(thisTable));
    batchSendMsg(response.getClass().getSimpleName() + JSON.toJSONString(response),
    thisTable.getPlayers(), true);
    //通知游戏大厅里所有玩家有人退出房间
    for(HallTable hallTable : hallList) {
    if(hallTable.getTableNum() == thisTable.getTableNum()) {
    hallTable.setFull(false);
    hallTable.setPlay(false);
    hallTable.getUserNames().remove(player.getUserName());
    }
    }
    RefreshHallResponse response1 = new RefreshHallResponse(hallList);
    batchSendMsg(response1.getClass().getSimpleName() + JSON.toJSONString(response1),
    userName2Player.values(), true);
    userName2Player.remove(player.getUserName());
    }
    29 条回复    2019-06-13 16:11:12 +08:00
    nazor
        1
    nazor  
       2019-06-12 07:44:09 +08:00 via iPhone
    remove 空指针,是因为 hashmap 为 null
    mejee
        2
    mejee  
       2019-06-12 07:45:39 +08:00
    1. player 是如何生成的?是否会有多个 handler 去 remove 同一个 player 的情况,这种情况可能会导致 null 异常
    mejee
        3
    mejee  
       2019-06-12 07:52:20 +08:00
    2.看了#1 的回复,楼主确定下到底是因为什么为 null 导致的 null 异常?或者 debug 一下?
    nazor
        4
    nazor  
       2019-06-12 07:54:05 +08:00 via iPhone
    确定不是 geuNum 返回 null? player 为 null,执行不到 remove 吧
    gramyang
        5
    gramyang  
    OP
       2019-06-12 07:56:55 +08:00
    @nazor 不会吧,不可能啊,remove 出空指针不是 remove 传入的变量为 null 吗?
    gramyang
        6
    gramyang  
    OP
       2019-06-12 07:58:53 +08:00
    @mejee player 是 handler 的私有变量,concurrenthashmap 是 handler 外部传入的变量。会有 concurrenthashmap 同时 remove 多个 player 的情况
    gramyang
        7
    gramyang  
    OP
       2019-06-12 07:59:50 +08:00
    @nazor 不会,因为前面代码有 player!=null 和 player.getNum>1 的判断
    luckylo
        8
    luckylo  
       2019-06-12 08:03:20 +08:00 via Android
    @gramyang 应该是 map 本身为空。remove 会返回 remove 的值,如果没有对应的 key,应该不会报空指针,最多应该就是返回 null。
    gramyang
        9
    gramyang  
    OP
       2019-06-12 08:06:29 +08:00
    @luckylo 代码层面上,map 不可能为空,我是初始化之后才传进去的。另外 concurrenthashmap 的 remove 方法源码上的注释:
    @throws NullPointerException if the specified key is null
    luckylo
        10
    luckylo  
       2019-06-12 08:08:37 +08:00 via Android
    luckylo
        11
    luckylo  
       2019-06-12 08:09:11 +08:00 via Android
    @gramyang 我刚去翻文档了😂
    mejee
        12
    mejee  
       2019-06-12 08:16:58 +08:00
    @luckylo
    @gramyang 刚去验证了下,
    @luckylo 说的对,没有对应 key 不会报 null 异常,是我记错了
    YzSama
        13
    YzSama  
       2019-06-12 08:32:49 +08:00 via iPhone
    show me the code。XD
    gramyang
        14
    gramyang  
    OP
       2019-06-12 08:42:14 +08:00
    @YzSama 贴在问题后面了,但是代码很多很杂,还是文字描述更精炼一些
    xuanbg
        15
    xuanbg  
       2019-06-12 09:11:02 +08:00
    好多个 remove,到底是哪一行抛了空指针?
    anzu
        16
    anzu  
       2019-06-12 09:41:03 +08:00
    handleAfterExitOrException 没有锁,当并发执行的时候,player 随时会被其它线程置 null,检查是否为 null 没用。
    passerbytiny
        17
    passerbytiny  
       2019-06-12 10:00:16 +08:00
    不太确定没有 sharable 注解的时候,handler 就是单个连接通道独占的。问题可能出在这里。
    Macolor21
        18
    Macolor21  
       2019-06-12 10:01:40 +08:00
    代码是 playerMap.remove( player.getSeatNum() );
    这里抛出空指针异常,要不就是 map 空,要不就是 player 空,标题起的有歧义,应该是执行 apiHandler.exitOrException();时,player 被其他线程置 null
    passerbytiny
        19
    passerbytiny  
       2019-06-12 10:05:55 +08:00
    这里建议用 ChannelContext 或者 Channel 的属性去保存 player,它们确定是线程安全或者单个通道独享的。
    imzhoukunqiang
        20
    imzhoukunqiang  
       2019-06-12 10:07:15 +08:00
    楼主说了,Handler 没有 sharable,所以 Handler 不会并发被调用,一个 handler 总是在同一个线程中被执行。所以在同一个线程中,就不存在重排序的问题。这个问题看起来比较诡异,建议打断点观看变量的值。
    gramyang
        21
    gramyang  
    OP
       2019-06-12 10:19:33 +08:00
    @xuanbg exitOrException 的第一个 remove
    gramyang
        22
    gramyang  
    OP
       2019-06-12 10:21:25 +08:00
    @imzhoukunqiang 是的,很诡异。说实话,上面的代码已经是我修改过了的,不过意思没变,都是很诡异的空指针。
    passerbytiny
        23
    passerbytiny  
       2019-06-12 10:25:51 +08:00
    去翻了一下 https://netty.io/4.0/api/io/netty/channel/ChannelHandler.Sharable.html,没有 Sharable 的时候,Handle 是单个通道独占的。

    到目前为止,根据楼主已放出来的消息,找不出其他原因了。
    gramyang
        24
    gramyang  
    OP
       2019-06-12 10:31:59 +08:00
    @passerbytiny 也足够了,起码帮助排除了重排序和并发错误的可能性。修改代码后如果再出现这种错误再另说
    firefffffffffly
        25
    firefffffffffly  
       2019-06-12 10:41:50 +08:00
    建议把 exception 信息贴出来,这样能轻松确定是 map 为空还是传入的 key 值为空。
    从描述的 exception 来看 player 最不可能为空,因为这样的话报错 message 和 traces 里是不会包含 remove 相关内容的,因为在 player.getNum()时就会报错了,remove 函数还没有入栈。
    key 值为空的情况,就是 player.getNum()的结果为 null,这个 player 内部属性需要再检查一下是否有多线程修改。
    rainmakeroly
        26
    rainmakeroly  
       2019-06-12 10:44:57 +08:00 via Android
    player 的获取,设置,初始化。报错信息的话主要是它吧
    alamaya
        27
    alamaya  
       2019-06-12 11:48:40 +08:00
    你这个 apiHandler 是怎么来的?没看出来你的 player 是怎么传入的
    senninha
        28
    senninha  
       2019-06-12 16:34:55 +08:00
    - -player 在其他线程并发置 null 了?有其他线程在操作这个 player ?如果其他线程要操作,可以丢到 eventloop 里转成同步执行,保证并发安全。
    ps:直接在 handler 里写业务代码的吗?这么强悍。。
    laodao1990
        29
    laodao1990  
       2019-06-13 16:11:12 +08:00
    要不这样试试:
    if player!=null
    锁{
    if player!=null {
    remove
    }
    }
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   5844 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 41ms · UTC 01:56 · PVG 09:56 · LAX 18:56 · JFK 21:56
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.