原来的集合类, 大部分都不是线程安全的.
Vector, Stack, HashTable, 是线程安全的,关键方法带有 synchronized
(
不建议用,类似于无脑加锁
),
其他的集合类不是线程安全的
.
多线程环境使用 ArrayList
1)
自己加锁,
synchronized
或者
ReentrantLock
2)
Collections.synchronizedList(new ArrayList),这里会提供一些 ArrayList 相关的方法,同时是带锁的。
3)
使用
CopyOnWriteArrayList(COW,也叫做“写时拷贝”),针对 ArrayList 进行读操作,不做任何额外的工作;如果进行写操作,则拷贝一份新的 ArrayList,针对新的进行修改,如果修改过程中有读操作,就继续读旧的这份数据,当修改完毕了,就使用新的替换旧的。
例如服务器程序的配置文件,修改配置可能需要重启服务器,但是每次重启服务器耗费很长时间,重启多台耗费的时间就更多,因此,很多服务器就提供了“热加载”(reload)这样的功能。热加载的实现使用到了 写时拷贝 的思路:新的配置放到新的对象中,加载过程里,请求仍基于旧配置进行工作,当新的对象加载完毕,就用新的代替旧的(替换完成,旧的就可以释放了)。
优点
:
在读多写少的场景下
,
性能很高
,
不需要加锁竞争
.
缺点
:
1.
占用内存较多,要求这个 ArrayList 不能太大。
2.
新写的数据不能被第一时间读取到。
多线程环境使用队列
1) ArrayBlockingQueue
基于数组实现的阻塞队列
2) LinkedBlockingQueue
基于链表实现的阻塞队列
3) PriorityBlockingQueue
基于堆实现的带优先级的阻塞队列
4) TransferQueue
最多只包含一个元素的阻塞队列
多线程环境使用哈希表 ¥
HashMap
本身是不线程安全的
.
在多线程环境下使用哈希表可以使用:
Hashtable 是线程安全的
ConcurrentHashMap 是更优化的线程安全的哈希表
此时,元素 1 和 2 在同一个链表上,如果线程 A 修改元素 1,线程 B 修改元素 2,那么就会有线程安全,就需要加锁。如果是线程 A 修改元素 3,线程 B 修改元素 4,就不会有线程问题,也就不需要加锁
Hashtable
只是简单的把关键方法加上了 synchronized
关键字,等于给 this 加锁,
这相当于直接针对Hashtable
对象本身加锁
.
这时候就是加了一个大锁,锁冲突的概率太大了,任何两个元素的操作都会有锁冲突,即使是在不同的链表上。
ConcurrentHashMap
相比于 Hashtable
做出了一系列的改进和优化
.
以
Java1.8 为例。ConcurrentHashMap 的做法是每个链表都有各自的锁,就是以每个链表的头节点作为锁对象,把锁的粒度降低了。
针对 1 和 2 这个情况,是针对同一把锁进行加锁,会有锁竞争;针对 3 和 4 这个情况,是针对不同对象进行加锁,不会有锁竞争,没有阻塞等待。
ConcurrentHashMap 比 HashTable 好在哪里
1. ConcurrentHashMap 相比于 HashTable 大大缩小了锁冲突的概率
2. ConcurrentHashMap 做了一个比较激进的操作:针对读操作不加锁,只针对写操作加锁。这就意味着读和写之间就没有冲突了(出现脏读),因此ConcurrentHashMap 采用了 volatile + 一些原子的写操作来保证读和写之间没有脏读问题。
3. ConcurrentHashMap 内部充分使用了 CAS,通过这个也来进一步减少加锁操作的数目,比如 size++ 。
4. 针对扩容:
HashMap / HashTable 是创建一个更大的数组空间,把旧的数组上的链表中的每个元素搬运到新的数组上,这个操作会在某次 put 的时候触发。如果某次的数据特别多,这个操作就会比较耗时就会卡顿。
ConcurrentHashMap 采取的是“化整为零”的方式,即每次搬运一小部分的元素。创建新的数组,旧的数组也会保留,每次进行 put 的时候往新数组上添加元素,同时把一小部分旧的元素搬运到新的数组上,每次 get 的时候则新旧数组都进行查询,每次 remove 的时候把元素删除即可。一段时间后旧的元素搬运完了,此时旧数组就可以释放了。
相关面试题
1) ConcurrentHashMap
的读是否要加锁,为什么
?
读操作没有加锁
.
目的是为了进一步降低锁冲突的概率
.
为了保证读到刚修改的数据
,
搭配了
volatile
关键字
.
2)
介绍下
ConcurrentHashMap
的锁分段技术
?
这个是
Java1.7
中采取的技术
. Java1.8
中已经不再使用了
.
简单的说就是把若干个哈希桶分成一个
"
段
" (Segment),
针对每个段分别加锁
.
目的也是为了降低锁竞争的概率
.
当两个线程访问的数据恰好在同一个段上的时候
,
才触发锁竞争
.
3) ConcurrentHashMap
在
jdk1.8
做了哪些优化?
取消了分段锁
,
直接给每个哈希桶
(
每个链表
)
分配了一个锁
(
就是以每个链表的头结点对象作为锁对
象
).
将原来 数组
+
链表 的实现方式改进成 数组
+
链表
/
红黑树 的方式
.
当链表较长的时候
(
大于等于
8
个元素
)
就转换成红黑树
.
4) Hashtable
和
HashMap
、
ConcurrentHashMap
之间的区别
?
HashMap:
线程不安全
. key
允许为
null
Hashtable:
线程安全
.
使用
synchronized
锁
Hashtable
对象
,
效率较低
. key
不允许为
null.
ConcurrentHashMap:
线程安全
.
使用
synchronized
锁每个链表头结点
,
锁冲突概率低
,
充分利用
CAS
机制
.
优化了扩容方式
. key
不允许为
null