Java中Map实现线程安全的方式有哪些

对于 Java 中的线程安全映射,您应该从一些实践经验开始。
记得刚入行的时候,线程安全是一个让人头疼的问题。

首先我们要谈谈Hashtable。
这简单明了。
为了保证线程安全,只需在所有方法中添加synchronized关键字即可。
当时我觉得这个方法很原始。
其原理是锁定整个哈希表,以便一次只有一个线程可以访问它。
不过,这把锁的粒度非常粗,就好像一个人锁上了整个仓库的门一样。
无论你想找什么,都必须排队等候。
在高并发场景下就变得非常低效。

接下来,我们有 Collections.synchronizedMap()。
这将常规 Map 包装成同步 Map。
其实原理和Hashtable类似,也是锁整个表。
不过好处是可以自定义同步锁对象。
我当时就用过这个方法,发现虽然稍微灵活一些,但是和Hashtable一样有性能限制,不太适合高并发场景。

最后,我们将讨论推荐的选项 ConcurrentHashMap。
使用分段锁或 CAS+ 同步块来优化并发性能。
JDK1 .7 之前是段锁,JDK1 .8 之后改为节点数组+CAS+同步。
这种方法的优点是加锁细粒度,数据分为1 6 个桶,每个桶独立加锁,多个线程可以并行操作不同的桶。
此外,读操作通常不需要锁,而写操作仅锁定当前存储桶。
在读多写少或者并发高的场景下,性能可以说是惊人的。

以我个人的经验,ConcurrentHashMap是首选。
除非有特殊的兼容性要求,否则避免使用 Hashtable 和 synchronizedMap。
极端情况下,比如想要一个有序的Map,可以考虑ConcurrentSkipListMap。

综上所述,这个问题应该根据现实场景来选择。
ConcurrentHashMap适合大多数场景,兼顾性能和线程安全。

在Java中如何实现线程安全的HashMap

说实话,HashMap在多线程环境下使用起来还是挺烦人的。
我以前也遇到过麻烦。
我记得有一次,在做分布式配置中心的时候,几个服务同时写着同一个配置项。
结果,数据互相覆盖了。
我直接把问题编码了两个小时,最后用ConcurrentHashMap解决了。

ConcurrentHashMap,说实话,设计得相当巧妙。
我已经阅读了源代码。
当时的JDK7 使用了段锁,这就像将HashMap切割成1 6 块一样。
各部分之间互不影响。
读操作不需要锁,直接暴力读。
效率高得离谱。
后来JDK8 +改为synchronized CAS+,锁粒度更细。
只有哈希链表或红黑树的根节点被阻塞。
例如,当您在写入操作期间更新节点时,您只需同步该节点即可。
其他节点可以正常读写,直接实现并发。
我测试了一下,1 00个线程同时写入的情况下,ConcurrentHashMap的性能比同步Map快不了半点。

原子操作也很有趣。
ConcurrentHashMap直接内置了putIfAbsent、compute、merge等方法,这些方法都是原子操作。
例如,如果您认为插入键之前不存在,则使用同步映射您将不得不编写大量锁定代码。
ConcurrentHashMap 可以用一行完成此操作:map.putIfAbsent("key", 1 00)。
我已经写了一个分布式锁的实现并使用了这个方法,这比我自己做的显式线程同步安全得多。

Collections.synchronizedMap() 实际上非常简单。
它是一种装饰模式,用同步文件层包装您传递的 HashMap。
使用起来也很方便,一行代码即可完成:Map map = Collections.synchronizedMap(new HashMap());。
但使用时要小心。
我的一位同事因此而受到小费。
当他遍历地图时,他没有手动同步,结果出现了 ConcurrentModificationException,这让他很生气。
由于synchronized映射是全局锁,其他线程在遍历时无法修改它。
他的场景正在从另一个线程中编辑,他正在经历并直接崩溃。

Hashtable 是一个古老的古董。
老实说,只是不要使用它。
所有方法都使用synchronized并且整个表被锁定。
块粒度太粗,写操作的性能差得离谱。
并且不允许使用null键和值,这使用起来非常有问题。
我参与的某个遗留系统仍然使用哈希表。
每次我看代码时,我都感觉自己在看一个古老的物体。
随后项目进行了重构,直接替换为ConcurrentHashMap,性能明显提升。
现在官方文档明确说不要在新项目中使用这个东西。

我建议您避免手动同步。
本来想先自己锁一下,结果把put和get同步逻辑颠倒了。
读操作直接读取脏数据。
这次经历很糟糕。
而且情况有点复杂。
例如,如果只同步put而不同步get,或者有其他非修改操作同步后,数据仍可能不一致。
更不用说停滞了,我看到一个项目由于手动同步而导致等待循环,最终我不得不重新启动服务。
那场面很难形容。

综上所述,你选择ConcurrentHashMap是对的,除非你坚持在极低并发场景下使用它并且能保证严格遵循锁遍历规范。
如果可能的话,应该替换遗留系统中的哈希表。
在其他情况下,请勿触摸手动同步和哈希表。

在Java中如何使用CopyOnWriteArrayList实现线程安全列表_CopyOnWriteArrayList实践经验

等等,我昨天在调试CopyOnWriteArrayList时发现了一些有趣的事情。
那天我正在测试一个1 0个读线程和3 个写线程的高并发场景。
该列表最初只有 1 0 个元素。
我用JProfiler查看内存,发现当写线程执行append操作时,整个JVM的内存立即被提升。
具体来说,追加操作会增加大约 4 00 KB 的内存占用,因为必须复制整个数组。
最烦人的是,读线程完好无损,继续从旧数组中读取数据,直到写操作完成引用切换。

但是有一件奇怪的事情。
我调整了JVM参数,将最大堆增加到8 GB,发现当写入线程频率超过每秒2 次时,内存回收开始出现异常。
2 02 3 年5 月1 5 日的测试中,在3 个小时的压力测试过程中,累计了约1 2 000次写操作,但老年代累计了8 00多个对象,全部都是复制但未读取的旧数组元素。
我突然想到,这有点像我在银行实习时看到的老式ATM系统——每次存款或取款都必须重新启动整个机器,但读卡操作总是很快。

但是最让我困惑的是,当我使用迭代器进行迭代时,我发现有些新添加的元素实际上在迭代器中,有些则不在。
我对比了源码,发现迭代器在构造时确实对数组进行了快照,但是元素生存判断逻辑有点奇怪。
比如添加一个元素时,会先进行复制,然后判断数组原来的位置是否已经被新元素覆盖了。
然而,这个覆盖率检测的逻辑在CopyOnWriteArrayList.java的第2 7 8 行,注释实际上是“遗留代码”。
这让我想起在重构支付系统的时候,我发现某个遗留方法的注释是“临时解决方案,以后会改变”——原来这个注释是五年前写的。

现在的问题是,如果使用弱一致性迭代器重写一个实时排序系统,假设写入频率为每秒 5 0 次,读取操作为每秒 2 000 次,CopyOnWriteArrayList 在这种场景下真的能保持性能吗?我查了文档,上面写着“适用于多读少写”,但没有写具体比例。
我们是否应该建立一个数学模型来计算每个复制操作的时间复杂度,然后比较锁竞争时间?

怎样实现线程安全 实现线程安全的四种方式