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写的时候会将共享变量新复制一份出来,这样做的好处是读操作完全无锁。

扫描二维码关注公众号,回复: 10729756 查看本文章

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使用跳表,性能更好点。

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。
    在这里插入图片描述
    只有 ArrayBlockingQueue 和 LinkedBlockingQueue 是支持有界的,在使用其他无界队列时,一定要充分考虑是否存在导致 OOM 的隐患。
发布了97 篇原创文章 · 获赞 3 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_39530821/article/details/102655814