文章目录
一、同步容器
1.将非线程安全的容器变成线程安全的容器
思路其实很简单,只要把非线程安全的容器封装在对象内部,然后控制好访问路径就可以了。
Collections 这个类中提供了一套完备的包装类。
List list = Collections.
synchronizedList(new ArrayList());
Set set = Collections.
synchronizedSet(new HashSet());
Map map = Collections.
synchronizedMap(new HashMap());
2.使用同步容器注意点
- 组合操作需要注意竞态条件问题
- 用迭代器遍历容器要加锁
List list = Collections.
synchronizedList(new ArrayList());
synchronized (list) {
Iterator i = list.iterator();
while (i.hasNext())
foo(i.next());
}
同步容器是基于 synchronized 这个同步关键字实现的。Java 提供的同步容器还有 Vector、Stack 和 Hashtable。
二、并发容器及其注意事项
并发容器分为四大类:List、Map、Set 和 Queue
1. List
List 里面只有一个实现类就是 CopyOnWriteArrayList
。写的时候会将共享变量新复制一份出来,这样做的好处是读操作完全无锁。
如果在遍历 array 的同时,还有一个写操作,例如增加元素,CopyOnWriteArrayList 会将 array 复制一份,然后在新复制处理的数组上执行增加元素的操作,执行完之后再将 array 指向这个新的数组。所以读写是可以并行的,遍历操作一直都是基于原 array 执行,而写操作则是基于新 array 进行。因此可能存在短暂的读写不一致。仅用于写操作很少的场景。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 获取对象持有的数组
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
//更改数组的引用
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
CopyOnWriteArrayList 迭代器是只读的,不支持增删改,因为迭代器遍历的仅仅是一个快照。
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the array */
private final Object[] snapshot;
/** Index of element to be returned by subsequent call to next. */
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
//获取迭代器时读取到的数组内容不会再改变
snapshot = elements;
}
public void add(E e) {
throw new UnsupportedOperationException();
}
}
2. Map
Map 接口的两个实现是 ConcurrentHashMap
和 ConcurrentSkipListMap
,它们从应用的角度来看,主要区别在于 ConcurrentHashMap 的 key 是无序的,而 ConcurrentSkipListMap 的 key 是有序的。所
ConcurrentSkipListMap 底层使用跳表。跳表插入、删除、查询操作平均的时间复杂度是 O(log n)。
3. Set
Set 接口的两个实现是 CopyOnWriteArraySet
和 ConcurrentSkipListSet
,使用场景可以参考CopyOnWriteArrayList 和 ConcurrentSkipListMap。
4. Queue
可以从以下两个维度来分类。一个维度是 阻塞与非阻塞,所谓阻塞指的是当队列已满时,入队操作阻塞;当队列已空时,出队操作阻塞。另一个维度是 单端与双端,单端指的是只能队尾入队,队首出队;而双端指的是队首队尾皆可入队出队。
可以将 Queue 细分为四大类,分别是:
- 单端阻塞队列:其实现有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、LinkedTransferQueue、PriorityBlockingQueue 和 DelayQueue。内部一般会持有一个队列,这个队列可以是数组(其实现是 ArrayBlockingQueue)也可以是链表(其实现是 LinkedBlockingQueue);甚至还可以不持有队列(其实现是 SynchronousQueue),此时生产者线程的入队操作必须等待消费者线程的出队操作。而 LinkedTransferQueue 融合 LinkedBlockingQueue 和 SynchronousQueue 的功能,性能比 LinkedBlockingQueue 更好;PriorityBlockingQueue 支持按照优先级出队;DelayQueue 支持延时出队。
- 双端阻塞队列:其实现是 LinkedBlockingDeque。
- 单端非阻塞队列:其实现是 ConcurrentLinkedQueue。
- 双端非阻塞队列:其实现是 ConcurrentLinkedDeque。
一般都不建议使用无界的队列,只有 ArrayBlockingQueue 和 LinkedBlockingQueue 是支持有界的。
三、原子类
1.概述
原子类是无锁的,相对互斥锁方案,最大的好处就是性能。通过硬件支持。CPU 为了解决并发问题,提供了 CAS 指令(Compare And Swap,即“比较并交换”)。CAS 指令包含 3 个参数:共享变量的内存地址 A、用于比较的值 B 和共享变量的新值 C;并且只有当内存中地址 A 处的值等于 B 时,才能将内存中地址 A 处的值更新为新值 C。
使用 CAS 来解决并发问题,一般都会伴随着自旋,而所谓自旋,其实就是循环尝试。例如,实现一个线程安全的
在 CAS 方案中,可能存在 ABA,大多数情况下我们并不关心 ABA 问题,例如数值的原子递增,但例如原子化的更新对象很可能就需要关心 ABA 问题,因为两个 A 虽然相等,但是第二个 A 的属性可能已经发生变化了。所以在使用 CAS 方案的时候,一定要先 check 一下。
例如原子类 AtomicLong
的 getAndIncrement()
方法内部就是基于 CAS 实现的:
final long getAndIncrement() {
return unsafe.getAndAddLong(
this, valueOffset, 1L);
}
public final long getAndAddLong(
Object o, long offset, long delta){
long v;
do {
// 读取内存中的值
v = getLongVolatile(o, offset);
} while (!compareAndSwapLong(
o, offset, v, v + delta));
return v;
}
//原子性地将变量更新为x
//条件是内存中的值等于expected
//更新成功则返回true
native boolean compareAndSwapLong(
Object o, long offset,
long expected,
long x);
getAndAddLong() 方法的实现,基本上就是 CAS 使用的经典范例。
do {
// 获取当前值
oldV = xxxx;
// 根据当前值计算新值
newV = ...oldV...
}while(!compareAndSet(oldV,newV);
Java SDK 并发包里提供的原子类分为五个类别:原子化的基本数据类型、原子化的对象引用类型、原子化数组、原子化对象属性更新器 和 原子化的累加器。
2.原子化的基本数据类型
相关实现有 AtomicBoolean、AtomicInteger 和 AtomicLong,提供的方法主要有以下这些
getAndIncrement() //原子化i++
getAndDecrement() //原子化的i--
incrementAndGet() //原子化的++i
decrementAndGet() //原子化的--i
//当前值+=delta,返回+=前的值
getAndAdd(delta)
//当前值+=delta,返回+=后的值
addAndGet(delta)
//CAS操作,返回是否成功
compareAndSet(expect, update)
//以下四个方法
//新值可以通过传入func函数来计算
getAndUpdate(func)
updateAndGet(func)
getAndAccumulate(x,func)
accumulateAndGet(x,func)
3.原子化的对象引用类型
相关实现有 AtomicReference、AtomicStampedReference 和 AtomicMarkableReference,利用它们可以实现对象引用的原子化更新。对象引用的更新需要重点关注 ABA 问题,AtomicStampedReference 和 AtomicMarkableReference 这两个原子类可以解决 ABA 问题。增加一个版本号维度就可以了
4.原子化数组
相关实现有 AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray,利用这些原子类,我们可以原子化地更新数组里面的每一个元素。这些类提供的方法和原子化的基本数据类型的区别仅仅是:每个方法多了一个数组的索引参数
5.原子化对象属性更新器
相关实现有 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater,利用它们可以原子化地更新对象的属性,这三个方法都是利用反射机制实现的。需要注意的是,对象属性必须是 volatile 类型的,只有这样才能保证可见性;如果对象属性不是 volatile 类型的,newUpdater() 方法会抛出 IllegalArgumentException 这个运行时异常。
6.原子化的累加器
DoubleAccumulator、DoubleAdder、LongAccumulator 和 LongAdder,这四个类仅仅用来执行累加操作,相比原子化的基本数据类型,速度更快,但是不支持 compareAndSet() 方法。如果你仅仅需要累加操作,使用原子化的累加器性能会更好。
思考题
1、线上系统 CPU 突然飙升,你怀疑有同学在并发场景里使用了 HashMap,因为在 1.8 之前的版本里并发执行 HashMap.put() 可能会导致 CPU 飙升到 100%.
答:hashmap扩容时会调用 transfer 方法迁移数据,并使用头插法反转链表,并发环境下可能导致链表成环,在查询时就可能会死循环。
2、下面的示例代码是合理库存的原子化实现,仅实现了设置库存上限 setUpper() 方法,你觉得 setUpper() 方法的实现是否正确呢?
public class SafeWM {
class WMRange{
final int upper;
final int lower;
WMRange(int upper,int lower){
//省略构造函数实现
}
}
final AtomicReference<WMRange>
rf = new AtomicReference<>(
new WMRange(0,0)
);
// 设置库存上限
void setUpper(int v){
WMRange nr;
WMRange or = rf.get();
do{
// 检查参数合法性
if(v < or.lower){
throw new IllegalArgumentException();
}
nr = new
WMRange(v, or.lower);
}while(!rf.compareAndSet(or, nr));
}
}
答:WMRange or = rf.get();
应该放在do循环里。另外WMRange的属性设成final 很重要。
参考资料:王宝令----Java并发编程实战