目录
一、多线程环境使用ArrayList
二、多线程环境使用队列
三、多线程环境使用哈希表
1、HashMap
2、Hashtable
3、ConcurrentHashMap
(1)缩小了锁的粒度
(2)充分使用了CAS原子操作,减少一些加锁
(3)针对扩容操作的一些优化(化整为零)
四、相关面试题
大部分集合类都是线程不安全的,Vector,Stack,Hashtable是线程安全的,但不建议使用,因为无论什么情况都要加锁,甚至单线程也是,这样就很不合理;并且这几个集合类官方已经不推荐使用了,可能在未来的版本中就被删掉了。
下面介绍一些线程不安全的集合类。
一、多线程环境使用ArrayList
1、自己使用同步机制(synchronized或者ReentrantLock)
2、Collections.synchronizedList(new ArrayList);
相当于给ArrayList套了个壳,ArrayList各种操作本身是不带锁的,通过上述操作套壳后,得到了新的对象,新的对象里面的关键方法都是带有锁的。
3、使用CopyOnWriteArrayList
CopyOnWrite容器即写时复制的容器,多线程对这个顺序表进行读操作时,不会有线程安全问题,但是当多线程进行写操作时,就会有线程安全问题,CopyOnWriteArrayList会复制一份原来的顺序表,并且修改新的顺序表内容,再把原来的引用指向新的顺序表(此操作是原子的,不需要加锁)。
二、多线程环境使用队列
1、自己加锁
2、使用BlockingQueue
1. ArrayBlockingQueue
基于数组实现的阻塞队列
2. LinkedBlockingQueue
基于链表实现的阻塞队列
3. PriorityBlockingQueue
基于堆实现的带优先级的阻塞队列
4. TransferQueue
最多只包含⼀个元素的阻塞队列
三、多线程环境使用哈希表
1、HashMap
HashMap本身就是线程不安全的。
2、Hashtable
在一些关键方法上加了锁
这也相当于对this加了锁,也就是针对Hashtable对象本身加锁,如果尝试修改Hash表中两个不同Hash值里的链表,会发生锁冲突。如图:
3、ConcurrentHashMap
相对于Hashtable,进行了些优化。
(1)缩小了锁的粒度
多线程如果修改Hash表里Hash值不同的链表都发生锁冲突,是不合理的,而且锁冲突是很耗时的,所以ConcurrentHashMap是对Hash表里每个链表都进行加锁,这样,不同的链表有不同的锁对象,多线程修改两个不同的链表,就不会发生锁冲突了,如图:
注意:更多的锁并不意味着要耗费更多的空间,因为在java中的任何对象都可以作为锁对象,而本身Hash表中就得有数组,数组元素都已经存在,即链表的头结点,每个链表都有一个头结点,可以直接把这个头结点作为链表的锁对象。
(2)充分使用了CAS原子操作,减少一些加锁
比如,针对Hash表元素个数的维护。
(3)针对扩容操作的一些优化(化整为零)
负载因子:描述了每个桶(Hash表)平均有多少个元素;公式:实际个数 / 数组长度(桶的个数)。0.75是默认的扩容阈值(也可以是其他数字值),如果我们算出的负载因子超过规定的扩容阈值,Hash表就会进行扩容。
进行扩容时,如果不是concurrentHashMap,会创建一个更大是数组,把旧的数组元素搬运到新的数组中,一次性的全部搬运完,如果Hash表本身的元素就非常多,这里扩容就会非常耗时,但可能过一会儿就又好了,存在不稳定因素,我们无法控制Hash表何时触发扩容。
concurrentHashMap则不是一次性的全部搬运完,而是把Hash表中的元素分为若干次搬运完,而不是直接一次性梭哈完,假设Hash表有1kw个元素,每次就只搬运5k哥元素,一共花费2k次搬运完成(搬运的时间会更长一些),但能确保每次搬运消耗的时间不会很长,避免出现很卡的情况。
总的来说,扩容是一个低频的操作(前提把扩容阈值设置合理),运行整个程序,可能一天都不会触发扩容,触发了每次可能会花费几分钟的时间进行搬运。
注意:在扩容过程中,存在两份Hash表,一份是新的,一份是旧的。
进行插入操作,直接往新的Hash表上插入。
进行删除操作,新的旧的都要删除。
进行查找操作,新的旧的都要查找。
四、相关面试题
1.ConcurrentHashMap的读是否要加锁,为什么?
读操作没有加锁.目的是为了进一步降低锁冲突的概率.为了保证读到刚修改的数据,搭配了volatile关键字.
2.介绍下ConcurrentHashMap的锁分段技术?
这个是Java1.7所采取的技术.Java1.8中已经不再使用了.简单的说就是把若干个哈希桶分成一个"段"(Segment),针对每个段分别加锁.
目的也是为了降低锁冲突的概率.当两个线程访问的数据恰好在同一个段上时,才会触发锁竞争
3.ConcurrentHashMap在jdk1.8做了哪些优化?
取消了分段锁,直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头节点对象作为锁对象).
将原来的数组 + 链表的实现方式改进成 数组 + 链表 /红黑树的方式.当链表较长的时候(大于等于8个元素)就转换成红黑树.
4.HashMap和HashTable,ConcurrentHashMap之间的区别?
HashMap: 线程不安全.key允许为null
HashTable:线程安全.使用synchronized锁HashTable对象,效率较低.key不允许设置为null.
ConcurrentHashMap: 线程安全.使用synchronized锁每个链表的头节点,锁冲突概率较低,充分利用CAS机制,优化了扩容方式.key不允许为null.