Java 中的容器主要可以分为四个大类,分别是 List、Map、Set 和 Queue(都是接口)。
线程不安全:ArrayList、HashMap,HashSet。
同步容器
如何将容器变为线程安全?
List list = Collections. synchronizedList(new ArrayList());
Set set = Collections. synchronizedSet(new HashSet());
Map map = Collections. synchronizedMap(new HashMap());
组合操作需要注意竞态条件,一个容易被忽视的“坑”是用迭代器遍历容器。
多线程情况下,会出bug. 需要加锁。
List list = Collections.synchronizedList(new ArrayList());
synchronized (list) {
Iterator i = list.iterator();
while (i.hasNext()){
foo(i.next());
}
}
Java 提供的同步容器还有 Vector、Stack 和 Hashtable,这三个容器不是基于包装类实现的,但同样是基于 synchronized 实现的,对这三个容器的遍历,同样要加锁保证互斥。
Java 在 1.5 版本之前所谓的线程安全的容器,主要指的就是同步容器。不过同步容器有个最大的问题,那就是性能差,所有方法都用 synchronized 来保证互斥,串行度太高了。因此 Java 在 1.5 及之后版本提供了性能更高的容器,我们一般称为并发容器。
并发容器
(一)List
CopyOnWriteArrayList:写的时候会将共享变量新复制一份出来,这样做的好处是读操作完全无锁。
1,应用场景,CopyOnWriteArrayList 仅适用于写操作非常少的场景,而且能够容忍读写的短暂不一致,写入的新元素并不能立刻被遍历到。
2,CopyOnWriteArrayList 迭代器是只读的,不支持增删改。因为迭代器遍历的仅仅是一个快照,而对快照进行增删改是没有意义的。
(二)Map
Map 接口的两个实现是 ConcurrentHashMap 和 ConcurrentSkipListMap,它们从应用的角度来看,主要区别在于ConcurrentHashMap 的 key 是无序的,而 ConcurrentSkipListMap 的 key 是有序的。所以如果你需要保证 key 的顺序,就只能使用 ConcurrentSkipListMap。
hashMap的key,value 都可以为null, treeMap的value 允许为null 。并发容器key, value都不允许为null .
ConcurrentSkipListMap 里面的 SkipList 本身就是一种数据结构,中文一般都翻译为“跳表”。跳表插入、删除、查询操作平均的时间复杂度是 O(log n),理论上和并发线程数没有关系,所以在并发度非常高的情况下,若你对 ConcurrentHashMap 的性能还不满意,可以尝试一下 ConcurrentSkipListMap。
(三)Set
CopyOnWriteArraySet :参考 CopyOnWriteArrayList
ConcurrentSkipListSet:参考ConcurrentSkipListMap
(四)Queue
java 并发包里面 Queue 这类并发容器是最复杂的,两个维度来分类。一个维度是阻塞与非阻塞,所谓阻塞指的是当队列已满时,入队操作阻塞;当队列已空时,出队操作阻塞。另一个维度是单端与双端,单端指的是只能队尾入队,队首出队;而双端指的是队首队尾皆可入队出队。Java 并发包里阻塞队列都用 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 的隐患。
原子类
CPU 为了解决并发问题,提供了 CAS 指令(CAS,全称是 Compare And Swap,即“比较并交换”)
CAS 指令包含 3 个参数:共享变量的内存地址 A、用于比较的值 B 和共享变量的新值 C;并且只有当内存中地址 A 处的值等于 B 时,才能将内存中地址 A 处的值更新为新值 C。作为一条 CPU 指令,CAS 指令本身是能够保证原子性的。
原理:CAS+ 自旋
在 CAS 方案带来ABA问题?
ABA问题解释:原始值为A , 被线程T1改成了B,然后又被线程T2改成了A 。 线程T3会觉得原始值为A没有被修改过。
原子化的更新对象很可能就需要关心 ABA 问题,因为两个 A 虽然相等,但是第二个 A 的属性可能已经发生变化了。所以在使用 CAS 方案的时候,一定要先 check 一下。
分为五个类别:原子化的基本数据类型、原子化的对象引用类型、原子化数组、原子化对象属性更新器和原子化的累加器。
1. 原子化的基本数据类型
相关实现有 AtomicBoolean、AtomicInteger 和 AtomicLong
getAndIncrement() // 原子化 i++
getAndDecrement() // 原子化的 i--
incrementAndGet() // 原子化的 ++i
decrementAndGet() // 原子化的 --i
// 当前值 +=delta,返回 += 前的值
getAndAdd(delta)
// 当前值 +=delta,返回 += 后的值
addAndGet(delta)
//CAS 操作,返回是否成功
compareAndSet(expect, update)
atomicInteger.compareAndSet(100,110);
2. 原子化的对象引用类型
相关实现有 AtomicReference、AtomicStampedReference 和 AtomicMarkableReference
需要注意的是,对象引用的更新需要重点关注 ABA 问题,AtomicStampedReference 和 AtomicMarkableReference 这两个原子类可以解决 ABA 问题。
用法:
private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);
atomicStampedReference.compareAndSet(100,120,stamp,stamp + 1)
AtomicReference<Person> ar = new AtomicReference<Person>(p1);
// 通过CAS设置ar。如果ar的值为p1的话,则将其设置为p2。
if (ar.compareAndSet(p1, p2)) {
System.out.println("设置成功");
}
AtomicStampedReference:
boolean compareAndSet( V expectedReference, V newReference, int expectedStamp, int newStamp)
AtomicMarkableReference:
boolean compareAndSet( V expectedReference, V newReference, boolean expectedMark, boolean newMark)
3. 原子化数组
相关实现有 AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray
可以原子化地更新数组里面的每一个元素。这些类提供的方法和原子化的基本数据类型的区别仅仅是:每个方法多了一个数组的索引参数
4. 原子化对象属性更新器
相关实现有 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater
可以原子化地更新对象的属性。
常用来优化代码,节省内存。AtomicIntegerFieldUpdater 替代 AtomicInteger
这三个方法都是利用反射机制实现的,创建更新器的方法如下:
public static <U> AtomicXXXFieldUpdater<U> newUpdater(Class<U> tclass, String fieldName)
// AtomicIntegerFieldUpdater 用法
public static class Candidate {
int id;
volatile int score = 0;
}
public static final AtomicIntegerFieldUpdater<Candidate> scoreUpdater =
AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");
scoreUpdater.incrementAndGet(candidate);
需要注意的是
1,对象属性必须是 volatile 类型的,只有这样才能保证可见性;如果对象属性不是 volatile 类型的,newUpdater() 方法会抛出 IllegalArgumentException 这个运行时异常。
2,只能是实例变量,不能是类变量,也就是说不能加static关键字。
3,不能是final变量
4,字段的描述类型(修饰符public/protected/default/private)是与调用者与操作对象字段的关系一致。
5,对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型的字段,不能修改其包装类型(Integer/Long)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater。
5. 原子化的累加器
DoubleAccumulator、DoubleAdder、LongAccumulator 和 LongAdder
仅仅用来执行累加操作,相比原子化的基本数据类型,速度更快,但是不支持 compareAndSet() 方法。如果你仅仅需要累加操作,使用原子化的累加器性能会更好。
LongAdder 是针对 Cell 数组的某个 Cell 进行 CAS 操作 ,把线程的名字的 hash 值,作为 Cell 数组的下标,然后对 Cell[i] 的 long 进行 CAS 操作。简单粗暴的分散了高并发下的竞争压力。 分段锁+CAS