线程安全的集合类
标准库里大部分的集合类都是线程不安全的。
少数几个安全的:
Vector, Stack, HashTable, 是线程安全的(不建议用), 其他的集合类不是线程安全的.
如果需要在多线程环境下保证集合类的线程安全,最简单的做法就是自己手动加锁
多线程环境使用 ArrayList
1、自己使用同步机制
自己使用同步机制 (synchronized 或者 ReentrantLock)
2、synchronizedList
套壳,给壳上加锁: Collections.synchronizedList(new ArrayList);
synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List.
synchronizedList 的关键操作上都带有 synchronized
3、写时拷贝CopyOnWriteArrayList
不加锁保证线程安全,CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
CopyOnWrite容器即写时复制【修改时复制】的容器:
- 当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy一份,复制出一个新的容器,然后新的容器里添加元素
- 添加完元素之后,再将原容器的引用指向新的容器。
CopyOnWriteArrayList又称为“双缓冲机制”
优点:
在读多写少的场景下, 性能很高, 不需要加锁竞争.
缺点:
占用内存较多
新写的数据不能被第一时间读取到
这种写时拷贝的思想,很多地方都会用到:
1.显卡渲染画面
2.比如编译器的热加载就类似这样,新修改的数据先在内存中加载好,然后把引用改成在内存当中的新数据。
好用虽然好用,但是操作适用非常有限!
如果元素特别多/特别修改频繁,就不太适合使用这种方式。
多线程环境使用队列
ArrayBlockingQueue
基于数组实现的阻塞队列
LinkedBlockingQueue
基于链表实现的阻塞队列
PriorityBlockingQueue
基于堆实现的带优先级的阻塞队列
TransferQueue
最多只包含一个元素的阻塞队列
多线程环境使用哈希表
HashMap 本身不是线程安全的.
在多线程环境下使用哈希表可以使用:
- Hashtable
- ConcurrentHashMap
1) Hashtable
只是简单的把关键方法加上了 synchronized 关键字.
造成的问题:
这相当于直接针对 Hashtable 对象本身加锁
- 如果多线程访问同一个 Hashtable 就会直接造成锁冲突.
- size 属性也是通过 synchronized 来控制同步, 也是比较慢的
- 一旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 效率会非常低
一个HashTable对象只有一把锁,两个线程访问HashTable中的任意数据都会出现锁竞争
2)ConcurrentHashMap
多线程下五星级推荐使用!背后有很多的优化。
相比于 Hashtable 做出了一系列的改进和优化. 以 Java1.8 为例:
1.锁粒度的控制
HshTable直接在方法上加synchronized,相当于是对this加锁,也就相当于对整个哈希表对象加锁,导致了一个哈希表只有一把锁。
CouncurrentHashMap,每个哈希桶都有自己的锁 (用每个链表的头结点作为锁对象), ,大大降低了锁冲突的概率,性能也大大提高。
2.ConcurrentHashMap只给写加锁,读操作不加锁。
回顾一下读写操作和这里的加锁操分析问题:
两个线程同时修改,才会有锁冲突,
两个线程读操作,没有锁冲突,
一个线程读和一个线程写,没有锁冲突。
问题:一个线程读和一个线程写之前说过可能导致线程读的是修改了一半的数据么,concurrentHashMap这里为什么敢这样,它难道就不会么?
在ConcurrentHashMap还真的就不会,它在设计的时候就非常慎重考虑到了这个操作带来的问题,使用了 volatile 保证从内存读取结果。
3.充分利用到了CAS的特性
1、维护size元素个数属性利用了CAS实现,而不是加锁
2、有的地方使用CAS实现的轻量级锁来实现 避免出现重量级锁的情况.
ConcurrentHashMap希望做到的是能不加锁就不加锁。
4、ConcurrentHashMap的扩容操作优化【化整为零】
- 发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.
- 扩容期间, 新老数组同时存在.
- 后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小部分元素.
- 搬完最后一个元素再把老数组删掉.
- 这个期间, 插入只往新数组加.
- 这个期间, 查找需要同时查新数组和老数组
直到最终搬运完成,再释放旧的空间。
【相关面试题】
ConcurrentHashMap的读是否要加锁,为什么?
读操作没有加锁,ConcurrentHashMap为了进一步降低锁冲突的概率,同时为了保证读到刚修改的数据,使用了volatile关键字。
介绍下 ConcurrentHashMap的锁分段技术?
若干个哈希桶分成一个"段" (Segment), 针对每个段分别加锁.
目的也是为了降低锁竞争的概率. 当两个线程访问的数据恰好在同一个段上的时候, 才触发锁竞争.
分段锁:好几个链表共用同一个锁,锁冲突概率比每个哈希桶一个锁高。
ConcurrentHashMap在jdk1.8做了哪些优化?
取消了分段锁, 直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对象).
将原来 数组 + 链表 的实现方式改进成 数组 + 链表 / 红黑树 的方式. 当链表较长的时候(大于等于8 个元素)就转换成红黑树.
HashTable和HashMap、ConcurrentHashMap 之间的区别?
1、HashMap线程不安全,HashMap、ConcurrentHashMap是线程安全的;|| key 允许为 null
2、HashTable:在每个方法上加锁,相当于一个HashMap对象只有一把锁 || key 不允许为 null.
3、ConcurrentHashMap:
3.1.每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对象);
3.2.只给写操作加锁,读操作不加锁
3.3.利用CAS来维护数组元素个数
shTable:在每个方法上加锁,相当于一个HashMap对象只有一把锁 || key 不允许为 null.
3、ConcurrentHashMap:
3.1.每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对象);
3.2.只给写操作加锁,读操作不加锁
3.3.利用CAS来维护数组元素个数
3.4.优化了扩容方式