编辑
阿华代码,不是逆风,就是我疯
你们的点赞收藏是我前进最大的动力!!
希望本文内容能够帮助到你!!
目录
一:多线程环境使用ArrayList
引入:
原来的集合类,大部分都是线程不安全的,但是有几个例外:Vector,Stack,HashTable(这几个类)但是现在官方已经不太推荐使用了,后续可能会删掉——因为哪怕是在单线程下也要加锁,这种情况不合理(往下看)
在这些类内部中,把一些关键的方法都加锁了,导致它们不仅在多线程场景下要加锁,而且在单线程场景下也要加锁。虽然JVM中有“锁消除”机制,但这也不是万能的,加锁带来的资源消耗依旧是不可忽视的(单线程下就没必要加锁了嘛)
1:顺序表使用同步机制
使用synchronized和ReentrantLock进行加锁,上一篇文章有提及两者的区别,往回翻翻~~
2:套壳封装
使用Collections.synchronizedList(new ArrayList)
因为ArrayList本身各种操作都是不带锁的,我们把它作为参数传入,相当于给ArrayList封装一下,套入Collections.synchronizedList()这个壳中,得到一个新的对象,这个新的对象调用关键的方法操作都是带有锁的
3:写时拷贝
使用CopyOnWriteArrayList
(1)添加/修改元素操作
如果我们往一个容器里面添加元素,我们不往这个容器中添加,而是先copy一份新容器,往新的容容里面添加或者修改元素
添加修改完元素之后,在将引用指向新的容器
(2)优点
①可以进行并发读,在“读多写少”的场景下效率非常高(在引用指向新的容器之前,读操作都可以在旧容器上完成)
(3)缺点
①相应的顺序表如果太大,copy的开销也变高了
②“写操作”非常频繁,copy的频率就会非常高,资源的消耗和占用就比较严重
③不能第一时间读到新写的数据
(4)使用场景
服务器加载配置文件的时候,就会把文件内容解析出来放到内存的数据结构中,配置文件体积小,而且修改频率低
二:多线程环境使用队列
这边以前的文章有总结过就不再加以详述
主要以自己加锁和使用BlockingQueue为主
三:多线程环境使用哈希表(面试高频)
引入
在多线程环境下,Hashtbale是线程安全的,因为在Hashtable内部的关键方法中都有进行synchronized加锁操作。
但是HashMap就不行了,在数组的基础上还涉及到链表和树化
1:ConcurrentHashMap
于是我们就加以改进,引入了ConcurrentHashMap(并发HashMap),以下是我们的改进过程
(1)缩小锁粒度
①HashMap中的加锁
当我们尝试对HashMap中的不同链表下的不同元素进行修改操作的时候,就会触发锁竞争
因为这个锁是针对整个HashMap(this)而言的
如下图中我们想修改元素1和元素2,就会触发锁竞争,
重点:
但实际上修改不同链表上的元素操作,并不会触发线程安全问题(加大了加锁的频率,资源浪费)
只有在修改同一个链表下的(相邻必触发:因为操作会涉及到同一个引用)元素可能会触发线程安全问题
于是我们在ConcurrentHashMap中进行了优化
编辑
②ConcurrentHashMap中的加锁
锁对象为每个数组中的元素(链表的头结点),此时如果修改同一个链表下的元素,就会触发锁竞争。
理解:相当于把一个大锁拆分成了好多把小锁(这就是缩小锁粒度)
优点:不仅解决了线程安全问题,还降低了加锁的频率,节约了资源
注:这里的锁的数量虽然很多,但并不会增加太多的资源消耗,因为加锁对象(头结点)是现成的,不需要我们再去创建了
编辑
(2)使用CAS原子操作
在ConcurrentHashMap中,比如针对哈希表中的元素个数的维护,我们使用CAS就可以减少一些加锁。
用synchronized加锁,咱们不知道加锁处于那种阶段(程度)的加锁——可能是偏向锁,轻量级化加锁,甚至是最后升级为重量级化加锁,这件事都是不可预估的
(3)扩容的优化
负载因子=元素个数/数组长度 ,0.75是一个扩容阈值指标
①HashMap扩容机制
如果数组元素个数太多会进行扩容,链表下元素个数太多会进行树化
扩容:创建一个更大数组,把旧Hash表上的元素一下子搬过去(一把梭哈),如果元素数量非常多,这里的copy操作就会非常的耗费时间,实际表现就是突然间某个操作非常卡
②ConcurrentHashMap扩容机制
扩容时,每次只搬运一部分元素,随着每次的插入/删除/添加/查找操作,都会搬运一部分元素。
内部机制:扩容时,有两份哈希表
插入操作——往新表上插
删除操作——新表旧表都删
查找操作——新表旧表都查
优点:确保每次操作耗费的时间都不长,避免出现卡顿的情况
缺点:整体扩容的时间变长了