线程安全的集合类

原来的集合类, 大部分都不是线程安全的.

        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
  • 31
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值