优秀的编程知识分享平台

网站首页 > 技术文章 正文

多线程-004-同步容器与并发容器,原子类

nanyue 2024-08-25 10:39:25 技术文章 7 ℃

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 接口的两个实现是 ConcurrentHashMapConcurrentSkipListMap,它们从应用的角度来看,主要区别在于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。


注意:

使用队列时,需要格外注意队列是否支持有界(所谓有界指的是内部的队列是否有容量限制),只有 ArrayBlockingQueueLinkedBlockingQueue 是支持有界的,所以在使用其他无界队列时,一定要充分考虑是否存在导致 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 问题,AtomicStampedReferenceAtomicMarkableReference 这两个原子类可以解决 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

最近发表
标签列表