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

Java : 方法接受一个 Map 参数,需要对这个 map 做遍历。怎样才是安全的?

  •  1
     
  •   theworldsong · 2018-09-28 16:21:30 +08:00 · 6559 次点击
    这是一个创建于 2240 天前的主题,其中的信息可能已经有所发展或是发生改变。

    举例:

    test(Map<String, String> m) {
    
    }
    

    现在要在 test 内遍历 m。

    但是:

    • m 是外部传入的参数,外部随时可能异步 remove 元素,导致你遍历出错。
    • 你不能修改外部业务,因为极为复杂。因此你只能在 test 方法内发挥。
    • 拷贝?拷贝的过程用到遍历,同样会出错。
    • 使用 SynchronizedMap ? SynchronizedMap 使用的仍然是这个 m 的引用,外部不受限制。

    老铁们,有办法吗

    第 1 条附言  ·  2018-09-30 09:57:43 +08:00

    注意审题:你不能修改外部代码。包括入参的类型,你也不能修改。你只能在 test 内发挥。

    42 条回复    2018-10-07 14:05:05 +08:00
    af463419014
        1
    af463419014  
       2018-09-28 16:26:18 +08:00
    入参 m 的类型改成 ImmutableMap
    yinzhili
        2
    yinzhili  
       2018-09-28 16:32:56 +08:00   ❤️ 2
    com.google.common.collect.ImmutableMap
    xuhaoyangx
        3
    xuhaoyangx  
       2018-09-28 16:46:47 +08:00
    你这问题 让我想起了 Java 形参和实参

    删除操作用 Iterator 去做
    z3jjlzt
        4
    z3jjlzt  
       2018-09-28 16:49:52 +08:00
    final 修饰入参
    johnj
        5
    johnj  
       2018-09-28 17:34:15 +08:00
    Collections.unmodifiableMap() 包一下
    johnj
        6
    johnj  
       2018-09-28 17:35:33 +08:00
    我说的不对
    hand515
        7
    hand515  
       2018-09-28 18:04:19 +08:00
    @z3jjlzt #4 这个不对,参数的 final 搞错了
    linlinismine
        8
    linlinismine  
       2018-09-28 18:28:57 +08:00
    copy 一份
    psuwgipgf
        9
    psuwgipgf  
       2018-09-28 18:37:52 +08:00
    不明白怎么解决,关注一下。
    aa6563679
        10
    aa6563679  
       2018-09-28 18:43:28 +08:00 via iPhone
    @linlinismine 可能 copy 中被改了
    crayygy
        11
    crayygy  
       2018-09-28 18:45:05 +08:00 via Android
    8l 的方法是可以的,在传入之前先从原 collection 中复制一份,然后再传入复制出来的对象
    elgae
        12
    elgae  
       2018-09-28 18:55:59 +08:00
    加个锁,遍历时独占
    sagaxu
        13
    sagaxu  
       2018-09-28 19:22:10 +08:00 via Android   ❤️ 1
    Map 只是个接口,你需要一个支持并发读写的实现
    talen666
        14
    talen666  
       2018-09-28 19:36:28 +08:00
    把声明 Map 的地方改成线程安全的 Map
    psuwgipgf
        15
    psuwgipgf  
       2018-09-28 20:43:40 +08:00
    @talen666 方法可以,不过他说的业务复杂用的地方也多,这样会影响整个系统的速度吧??
    oaix
        16
    oaix  
       2018-09-28 20:54:40 +08:00
    重试
    boywang004
        17
    boywang004  
       2018-09-28 21:45:42 +08:00
    只要调用 iterator()都会有机会上抛 ConcurrentModifyException,先做保护性拷贝,拷贝期间肯定会调用 iterator(),只要捕获 CME 重新拷贝,直到某次拷贝时没有改动成功拷出来为止……

    上面说的加锁包 unmodifiableMap 啥的……=__=b
    micean
        18
    micean  
       2018-09-28 22:38:38 +08:00
    只能在 test 里面操作的话根本做不到
    ysweics
        19
    ysweics  
       2018-09-28 22:45:56 +08:00
    明确一下问题,具体的需求是怎么样,比如这个 map 开始传递过来的时候只有 10 个 kv ,然后你在便利的时候,remove 两个 kv,你需要的结果是开始的 10 个 kv,还是剩余的 8 个 kv
    zzorzz
        20
    zzorzz  
       2018-09-28 23:24:51 +08:00   ❤️ 1
    只是在 test 遍历,没有新启动一个新线程进行遍历的话,应该无须考虑同步问题,谁调用谁负责(方法调用者在自己的线程去考虑和别的线程同步)
    reeco
        21
    reeco  
       2018-09-28 23:40:46 +08:00
    外部 remove 了跟你内部怎么遍历没有冲突啊,至于数据同步的问题就是楼上说的谁调用谁负责
    miao1007
        22
    miao1007  
       2018-09-28 23:42:02 +08:00 via Android
    最开始应该上写时复制
    lovedebug
        23
    lovedebug  
       2018-09-28 23:46:16 +08:00
    所有调用 test 函数的地方强制使用 Collections.unmodifiedXX
    Ginsai
        24
    Ginsai  
       2018-09-29 00:54:07 +08:00
    既然只是传入 Map,外部能随时修改的话对 test()方法操作本身就已经不安全了。。如果没法将传入 Map 的类型线程安全的,那么就在 test()里面做 try catch,设置遍历失败尝试次数,异常捕获之后继续遍历处理。
    laxenade
        25
    laxenade  
       2018-09-29 04:01:51 +08:00
    就和 spinlock 的原理一样不断拷贝咯,直到没有异常为止。
    fengdianxun
        26
    fengdianxun  
       2018-09-29 06:59:05 +08:00 via Android
    用 kotlin 的只读 map 呢
    hearfish
        27
    hearfish  
       2018-09-29 07:02:29 +08:00
    并发的环境下多线程共用一个不知道是不是线程安全的 Map,而且还不知道别的线程是怎么用它的?
    MoHen9
        28
    MoHen9  
       2018-09-29 07:53:33 +08:00 via Android
    把 map 改为 ConcurrentHashMap 呢,如果不允许,那么在传过来的时候就应该是一个拷贝的 map,而不是原始的 map
    mifly
        29
    mifly  
       2018-09-29 08:05:41 +08:00 via Android
    这样的代码应该要避免,考虑看看是不是可以用 blockquene 替代 map
    assiadamo
        30
    assiadamo  
       2018-09-29 08:38:25 +08:00 via Android
    上面的 boywang 貌似对不可变集合有不同的观点,我为了避免遍历中对原始集合有增改的这个问题,一般都不会去操作原始集合,而是直接做一个新的集合然后赋值,直接切换引用,这个是绝对线程安全的,而且遍历时切换也对遍历毫无影响
    D3EP
        31
    D3EP  
       2018-09-29 08:42:37 +08:00 via iPhone
    @assiadamo 你这说的不就是 copyonwrite 的集合么?都是现成的线程安全的集合
    D3EP
        32
    D3EP  
       2018-09-29 08:43:33 +08:00 via iPhone
    多线程条件下,不用线程安全的集合,我也是醉了
    ilaipi
        33
    ilaipi  
       2018-09-29 08:50:35 +08:00
    感觉你这既然外部可以随时修改这个 map,是不是这个 map 是个常量?那是不是不需要传进来?统一的地方去维护这个 map 然后上锁?
    assiadamo
        34
    assiadamo  
       2018-09-29 09:05:08 +08:00 via Android
    @D3EP copyonwrite 帮你省掉了切换引用这步操作,让你可以直接 add remove,但需要原始集合声明为 copyonwrite
    TommyLemon
        35
    TommyLemon  
       2018-09-29 09:42:03 +08:00
    序列化再反序列化就行了。
    可以用 fastjson( https://github.com/topics/fastjson)
    ColinWang
        36
    ColinWang  
       2018-09-29 10:06:46 +08:00
    Copy-On-Write 正解
    ZSeptember
        37
    ZSeptember  
       2018-09-29 11:25:14 +08:00
    设计有问题,在多个地方都会写的不应该传参。
    改用共享的,然后用线程安全的。
    lihongjie0209
        38
    lihongjie0209  
       2018-09-29 11:51:14 +08:00   ❤️ 1
    你这个接口有问题, 一个接口连自己的参数都无法处于一个确定的状态, 那你怎么写业务逻辑?
    passerbytiny
        39
    passerbytiny  
       2018-09-29 15:00:34 +08:00
    引用以下前面的回复

    #19 只是在 test 遍历,没有新启动一个新线程进行遍历的话,应该无须考虑同步问题,谁调用谁负责(方法调用者在自己的线程去考虑和别的线程同步)

    # 37 你这个接口有问题, 一个接口连自己的参数都无法处于一个确定的状态, 那你怎么写业务逻辑?

    如果你这个方法的参数类型是 java.util.Map ,又没有对外部调用做任何附加规范限制,那就意味着该方法声明了它处理的是不安全的 Map,出错是正常的,需要外部系统确认自己处理 Map 的线程安全问题。如果你需要这个方法自己考虑同步问题,那么它至少需要在规范上限制传入的参数类型是线程同步的或者不可变的。

    从你的描述看,调用方非常明确的告诉你传入的是一个没有线程同步还被多线程共享的 java.util.Map 对象。你赶紧把这个 BUG 提交上去吧,技术层面解决不了。

    解释一下:只有多线程共享、并且线程不安全的 Map 对象,才有可能出现这边正在处理着,那边 put/remove 的情况。
    luozic
        40
    luozic  
       2018-09-29 19:59:08 +08:00 via iPhone
    这种多线程请求还不同步的,脑袋被踢了? 暂时可以抽象为,只有一个线程有取数据和移除数据的操作,其他线程都从提交到这个线程处理。
    troywinter
        41
    troywinter  
       2018-09-30 00:48:07 +08:00
    我记得没错的话,你对 java 传参有什么误解,实现上来说,java will make a copy of this parameter,加 final 修饰并不能解决问题,final 只能禁止重新赋值,不能禁止修改这个对象。
    otmoc
        42
    otmoc  
       2018-10-07 14:05:05 +08:00
    # 如果只是解决报错的问题的话
    String[] keys = map.keySet().toArray(new String[map.size()]);
    for (String key : keys) {
    String value = map.get(key);
    if (value == null) {
    // do something
    }
    //do others something
    }

    # 如果不只是解决报错的话,那设计的接口有问题
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2222 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 00:46 · PVG 08:46 · LAX 16:46 · JFK 19:46
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.