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

ConcurrentHashMap 的使用问题

  •  
  •   gramyang · 6 天前 · 1673 次点击

    昨天在 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 条附言  ·  6 天前
    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
        1
    nazor   6 天前 via iPhone
    remove 空指针,是因为 hashmap 为 null
        2
    mejee   6 天前
    1. player 是如何生成的?是否会有多个 handler 去 remove 同一个 player 的情况,这种情况可能会导致 null 异常
        3
    mejee   6 天前
    2.看了#1 的回复,楼主确定下到底是因为什么为 null 导致的 null 异常?或者 debug 一下?
        4
    nazor   6 天前 via iPhone
    确定不是 geuNum 返回 null? player 为 null,执行不到 remove 吧
        5
    gramyang   6 天前
    @nazor 不会吧,不可能啊,remove 出空指针不是 remove 传入的变量为 null 吗?
        6
    gramyang   6 天前
    @mejee player 是 handler 的私有变量,concurrenthashmap 是 handler 外部传入的变量。会有 concurrenthashmap 同时 remove 多个 player 的情况
        7
    gramyang   6 天前
    @nazor 不会,因为前面代码有 player!=null 和 player.getNum>1 的判断
        8
    luckylo   6 天前 via Android
    @gramyang 应该是 map 本身为空。remove 会返回 remove 的值,如果没有对应的 key,应该不会报空指针,最多应该就是返回 null。
        9
    gramyang   6 天前
    @luckylo 代码层面上,map 不可能为空,我是初始化之后才传进去的。另外 concurrenthashmap 的 remove 方法源码上的注释:
    @throws NullPointerException if the specified key is null
        10
    luckylo   6 天前 via Android
        11
    luckylo   6 天前 via Android
    @gramyang 我刚去翻文档了😂
        12
    mejee   6 天前
    @luckylo
    @gramyang 刚去验证了下,
    @luckylo 说的对,没有对应 key 不会报 null 异常,是我记错了
        13
    YzSama   6 天前 via iPhone
    show me the code。XD
        14
    gramyang   6 天前
    @YzSama 贴在问题后面了,但是代码很多很杂,还是文字描述更精炼一些
        15
    xuanbg   6 天前
    好多个 remove,到底是哪一行抛了空指针?
        16
    anzu   6 天前
    handleAfterExitOrException 没有锁,当并发执行的时候,player 随时会被其它线程置 null,检查是否为 null 没用。
        17
    passerbytiny   6 天前
    不太确定没有 sharable 注解的时候,handler 就是单个连接通道独占的。问题可能出在这里。
        18
    Macolor21   6 天前
    代码是 playerMap.remove( player.getSeatNum() );
    这里抛出空指针异常,要不就是 map 空,要不就是 player 空,标题起的有歧义,应该是执行 apiHandler.exitOrException();时,player 被其他线程置 null
        19
    passerbytiny   6 天前
    这里建议用 ChannelContext 或者 Channel 的属性去保存 player,它们确定是线程安全或者单个通道独享的。
        20
    imzhoukunqiang   6 天前
    楼主说了,Handler 没有 sharable,所以 Handler 不会并发被调用,一个 handler 总是在同一个线程中被执行。所以在同一个线程中,就不存在重排序的问题。这个问题看起来比较诡异,建议打断点观看变量的值。
        21
    gramyang   6 天前
    @xuanbg exitOrException 的第一个 remove
        22
    gramyang   6 天前
    @imzhoukunqiang 是的,很诡异。说实话,上面的代码已经是我修改过了的,不过意思没变,都是很诡异的空指针。
        23
    passerbytiny   6 天前
    去翻了一下 https://netty.io/4.0/api/io/netty/channel/ChannelHandler.Sharable.html,没有 Sharable 的时候,Handle 是单个通道独占的。

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