20. 并发容器:都有哪些坑需要我们填?- 并发工具类

1. 同步容器及其注意事项

12. 如何用面向对象思想写好并发程序?介绍了面向对象思想写并发程序。


class SafeArrayList<T> {
//	封装	ArrayList
	List<T> c = new ArrayList<>();

//	控制访问路径
	synchronized T get(int idx) {
		return c.get(idx);
	}

	synchronized void add(int idx, T t) {
		c.add(idx, t);
	}

	synchronized boolean addIfNotExist(T t) {
		if (!c.contains(t)) {
			c.add(t);
			return true;
		}
		return false;
	}
}

这是很简单的实现线程安全。
Java SDk里面已经有现成的线程安全类,分别把 ArrayList、
HashSet 和 HashMap 包装成了线程安全的 List、Set 和 Map。

List list = Collections.synchronizedList(new ArrayList());
Set set = Collections.synchronizedSet(new HashSet());
Map map = Collections.synchronizedMap(new HashMap());

组合操作需要注意竞态条件问题,即便每个操作都能保证原子性,也并不能保证组合操作的原子性。

在容器领域一个容易被忽视的“坑”是用迭代器遍历容器,下面的作法不保证组合操作的原子性。

List list = Collections.synchronizedList(new ArrayList());
Iterator i = list.iterator();
while (i.hasNext())
foo(i.next());

正确作法如下:

List list =	Collections.synchronizedList(new ArrayList());
synchronized (list)	{		
Iterator	i	=	list.iterator();	
while	(i.hasNext())
foo(i.next());
}				

包装类的公共方法锁的是对象的 this,也就是这里的list。

2. 并发容器及其注意事项

同步容器所有方法都是用synchronized加锁,性能差,java1.5之后提供性能更好的容器,我们一般称为并发容器。

常用的并发容器如下:
在这里插入图片描述

2.1 List

CopyOnWriteArrayList, CopyOnWrite写的时候会将共享变量新复制一份出来,这样做的好处是读操作完全无锁。

CopyOnWriteArrayList内部维护了一个数组,成员变量 array就指向这个内部数组,所有的读操作都是基于 array进行的,如下图所示,迭代器 Iterator 遍历的就是 array数组。
在这里插入图片描述
如果在遍历array的同时,还有一个写操作,例如增加元素,CopyOnWriteArrayList是如何处理的呢?CopyOnWriteArrayList会将array复制一份,然后在新复制处理的数组上执行增加元素的操作,执行完之后再将array指向这个新的数组。通过下图你可以看到,读写是可以并行的,遍历操作一直都是基于原array执行,而写操作则是基于新array进行。
在这里插入图片描述
注意的坑:

  • 应用场景,CopyOnWriteArrayList 仅适用于写操作非常少的场景,而且能够容忍读写的短暂不一致。
  • CopyOnWriteArrayList迭代器是只读的,不支持增删改。

2.2 Map

两个实现是 ConcurrentHashMap 和 ConcurrentSkipListMap,主要区别在于ConcurrentHashMap 的 key 是无序的,而 ConcurrentSkipListMap 的 key 是有序的。两者的key和value都不允许为null,否则报错。
在这里插入图片描述
ConcurrentSkipListMap使用跳表,性能更好点。跳表插入、删除、查询操作平均的时间复杂度是 O(log n),理论上和并发线程数没有关系,所以在并发度非常高的情况下,若你对ConcurrentHashMap 的性能还不满意,可以尝试一下ConcurrentSkipListMap。

2.3 Set

Set接口的两个实现是CopyOnWriteArraySet和ConcurrentSkipListSet,使用场景可以参考前面讲述的CopyOnWriteArrayList和ConcurrentSkipListMap,它们的原理都是一样的。

2.4 Queue

从两个维度来分类。

  • 阻塞与非阻塞,所谓阻塞指的是当队列已满时,入队操作阻塞;当队列已空时,出队操作阻塞;
  • 单端与双端,单端指的是只能队尾入队,队首出队;而双端指的是队首队尾皆可入队出队。

阻塞队列都用Blocking关键字标识,单端队列使用Queue 标识,双端队列使用Deque 标识

  1. 单端阻塞队列:其实现有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、LinkedTransferQueue、PriorityBlockingQueue和DelayQueue。内部一般会持有一个队列,这个队列可以是数组(其实现是ArrayBlockingQueue)也可以是链表(其实现是LinkedBlockingQueue);甚至还可以不持有队列(其实现是SynchronousQueue),此时生产者线程的入队操作必须等待消费者线程的出队操作。而LinkedTransferQueue融合LinkedBlockingQueue和SynchronousQueue的功能,性能比LinkedBlockingQueue更好;PriorityBlockingQueue支持按照优先级出队;DelayQueue支持延时出队。
    在这里插入图片描述
  2. 双端阻塞队列:其实现是 LinkedBlockingDeque。
    在这里插入图片描述
  3. 单端非阻塞队列:其实现是 ConcurrentLinkedQueue。
  4. 双端非阻塞队列:其实现是 ConcurrentLinkedDeque。

只有 ArrayBlockingQueue 和 LinkedBlockingQueue 是支持有界的,在使用其他无界队列时,一定要充分考虑是否存在导致 OOM 的隐患。

3.总结

实际工作,熟悉容器特性,选对容器很关键。

4.课后思考

线上系统 CPU 突然飙升,你怀疑有同学在并发场景里使用了 HashMap,因为在 1.8 之前的版本
里并发执行 HashMap.put() 可能会导致 CPU 飙升到 100%,你觉得该如何验证你的猜测呢?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值