并发编程学习笔记4——同步容器、并发容器、原子类


一、同步容器

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 接口的两个实现是 ConcurrentHashMapConcurrentSkipListMap,它们从应用的角度来看,主要区别在于 ConcurrentHashMap 的 key 是无序的,而 ConcurrentSkipListMap 的 key 是有序的。所
在这里插入图片描述

ConcurrentSkipListMap 底层使用跳表。跳表插入、删除、查询操作平均的时间复杂度是 O(log n)。

3. Set

Set 接口的两个实现是 CopyOnWriteArraySetConcurrentSkipListSet,使用场景可以参考CopyOnWriteArrayList 和 ConcurrentSkipListMap。

4. Queue

可以从以下两个维度来分类。一个维度是 阻塞与非阻塞,所谓阻塞指的是当队列已满时,入队操作阻塞;当队列已空时,出队操作阻塞。另一个维度是 单端与双端,单端指的是只能队尾入队,队首出队;而双端指的是队首队尾皆可入队出队。

可以将 Queue 细分为四大类,分别是:

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

例如原子类 AtomicLonggetAndIncrement() 方法内部就是基于 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并发编程实战

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值