多线程进阶:常见数据结构的安全性分析


提示:以下是本篇文章正文内容,Java系列学习将会持续更新

一、常见数据结构

非线程安全的数据结构:ArrayList,LinkedList,ArrayQueue,HashMap,HashSet

线程安全的数据结构:Vector,Stack,Hashtable,CopyOnWriteArrayList,ConcurrentHashMap

二、ArrayList

2-1 线程不安全的原因

看源码

public boolean add(E e) {
	ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 该方法是容量保障,当容纳不下新增的元素时会进行扩容
    elementData[size++] = e;
    return true;
}

分析:

  1. 当数组长度为10,而size = 9时,此时A线程判断可以容纳,B线程也来判断发现可以容纳(这是因为add非原子操作)。当A添加完之后,B线程再添加的话,就会报错(数组越界异常)
  2. 而且这一步elementData[size++] = e也非原子性的.
    可以拆分为elementData[size] = e 和 size ++;
    在多线程的情况下很容易出现elementData[size] = e1; elementData[size] = e2; size++; size++; 的情况

2-2 Vector实现安全

Vector的add()源码

	public synchronized void addElement(E obj) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = obj;
    }

分析:
Vector的add方法加了synchronized ,而ArrayList没有,所以ArrayList线程不安全,但是,由于Vector加了synchronized ,变成了串行,所以效率低。

回到目录…

三、CopyOnWriteArrayList

CopyOnWrite容器即写时复制的容器。

// java.util.concurrent包下
List<String> list = new CopyOnWriteArrayList<String>();

3-1 如何实现线程安全?

 通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器

 这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

3-2 特征

  1. CopyOnWriteArrayList(写数组的拷贝)是ArrayList的一个线程安全的变体,CopyOnWriteArrayList和CopyOnWriteSet都是线程安全的集合,其中所有可变操作(add、set等等)都是通过对底层数组进行一次新的复制来实现的。

  2. 它绝对不会抛出ConcurrentModificationException的异常。因为该列表(CopyOnWriteArrayList)在遍历时将不会被做任何的修改。

  3. CopyOnWriteArrayList适合用在“读多,写少”的并发场景中,比如缓存、白名单、黑名单。它不存在“扩容”的概念,每次写操作(add or remove)都要copy一个副本,在副本的基础上修改后改变array引用,所以称为“CopyOnWrite”,因此在写操作要加锁,并且对整个list的copy操作时相当耗时的,过多的写操作不推荐使用该存储结构。

  4. 读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为开始读的那一刻已经确定了读的对象是旧对象。

3-3 缺点

  1. 在写操作时,因为复制机制,会导致内存占用过大
  2. 不能保证实时性的数据一致,“脏读”。

回到目录…

四、HashMap

4-1 底层原理

不清楚的小白看看之前两篇文章,就可以很容易搞懂HashMap的底层实现原理了。

Java数据结构之哈希表

JDK中的Set和Map解析

4-2 线程不安全的原因

单看 HashMap 中的 put 操作:

  • JDK1.7头插法 –> 将链表变成环 –> 死循环
  • JDK1.8尾插法 –> 数据覆盖

回到目录…

五、ConcurrentHashMap

// java.util.concurrent包下
Map<Integer, String> map = new ConcurrentHashMap<>();

5-1 实现原理

  • JDK1.7时,采用分段锁,将一个大哈希表默认分为16段的哈希表,同一个小表内互斥。用的是 Lock 锁。
    在这里插入图片描述
  • JDK1.8时加入了红黑树,且只针对同一链表内互斥,不是同一链表内的操作就不需要互斥。用的是synchronized锁。
    但是一旦遇到需要扩容的时候,涉及到所有链表,此时就不是简单的互斥了。

扩容的过程:

  1. 当A线程put 操作时发现需要扩容,则它自己创建一个扩容后的新数组。
  2. A线程只把当前桶中的节点重新计算哈希值放入新数组中,并且标记该桶元素已经迁移完成。由于其它桶中的元素还没有迁移,所以暂时还不删除旧数组。
  3. 等其它线程抢到锁并在桶内做完操作时,需要义务的将该桶节点全部搬移并标记桶。
  4. 直到最后一个线程将最后一桶节点搬移完毕,则它需要把旧数组删除。

5-2 与Hashtable的区别

HashTable和HashMap的实现原理几乎一样,差别在于:

  1. HashTable不允许key和value为null;
  2. HashTable是线程安全的。

 但是HashTable线程安全的策略实现代价却比较大,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把全表锁。当一个线程访问或操作该对象,那其他线程只能阻塞。

 所以说,Hashtable 的效率低的离谱,几近废弃。

回到目录…


总结:
提示:这里对文章进行总结:
以上就是今天的学习内容,本文是Java数据结构和多线程的应用学习,认识常用数据结构的线程安全性,理解某些集合不安全的原因,以及某些集合底层如何实现的线程安全性。之后的学习内容将持续更新!!!

  • 8
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只咸鱼。。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值