zoukankan      html  css  js  c++  java
  • 【并发编程】5.原子类与并发容器

    一、原子类

    1.什么是原子类

    Java的java.util.concurrent包除了提供底层锁、并发集合外,还提供了一组原子操作的封装类,它们位于java.util.concurrent.atomic包。
    Atomic类是通过无锁(lock-free)的方式实现的线程安全(thread-safe)访问

    2.原子类是基于什么实现的

    原子类的实现是基于CPU本身提供的 CAS指令,是一种无锁的实现;
    CAS Compare and Swap 比较并交换,即在更新之前会校验是否于期待的值相等
    执行函数:CAS(V,E,N)
    V表示要更新的变量;E表示预期值;N表示新值

    CAS 容易出现ABA问题
    容易出现在原子变量值不确定容易反复的情况
    假设 count 原本是 A,
    线程1检查数值 期望值为 A,未执行更新操作 ,
    此时 线程2 更新值为B,
    线程3更新值 为 A,
    然后 线程1执行更新操作 ,期望值与实际值相同执行操作。
    解决方案:类似乐观锁,加上版本号。

    3.常用的原子类

    1. 原子化的基本数据类型
    相关实现有 AtomicBoolean、AtomicInteger 和 AtomicLong

    2. 原子化的对象引用类型
    相关实现有 AtomicReference、AtomicStampedReference 和 AtomicMarkableReference,利用 它们可以实现对象引用的原子化更新。
    ABA 问题
    AtomicStampedReference 和 AtomicMarkableReference 这两个原子类可以解决 ABA 问题

    3. 原子化数组
    相关实现有 AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray,利用这些原子 类,我们可以原子化地更新数组里面的每一个元素。这些类提供的方法和原子化的基本数据类型 的区别仅仅是:每个方法多了一个数组的索引参数,所以这里也不再赘述了。

    4. 原子化对象属性更新器
    相关实现有 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater,利用它们可以原子化地更新对象的属性,这三个方法都是利用反 射机制实现的。

    5. 原子化的累加器
    DoubleAccumulator、DoubleAdder、LongAccumulator 和 LongAdder,这四个类仅仅用来执行 累加操作,相比原子化的基本数据类型,速度更快,但是不支持 compareAndSet() 方法。如果你 仅仅需要累加操作,使用原子化的累加器性能会更好

    4.原子类的用途

    适用于计数器,累加器等场景(感觉redis更好用..)

    二、并发容器

    1.同步容器

    java 1.5之前在java.util包中提供了Vector和HashTable两个同步容器
    是对所有的方法都加上了同步关键字synchronized,确保一个实例只有一个线程能操作。
    这样所有的操作就都是串行的,性能较差。

    2.常用的并发容器与实现原理

    2.1 CopyOnWriteArrayList 是唯一的并发List 读操作完全无锁

        /** The lock protecting all mutators */
        final transient ReentrantLock lock = new ReentrantLock();  //可重入锁
    
        /** The array, accessed only via getArray/setArray. */
        private transient volatile Object[] array; //内部维护的对象数组
    
        public E get(int index) {
            return get(getArray(), index);
        }
    
        @SuppressWarnings("unchecked")
        private E get(Object[] a, int index) {
            return (E) a[index];
        }
          
         
          //进行写操作时会把当前数组copy一份,进行写操作后将新的数组赋值给 array 
          public E set(int index, E element) {
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                Object[] elements = getArray();
                E oldValue = get(elements, index);
    
                if (oldValue != element) {
                    int len = elements.length;
                    Object[] newElements = Arrays.copyOf(elements, len);
                    newElements[index] = element;
                    setArray(newElements);
                } else {
                    // Not quite a no-op; ensures volatile write semantics
                    setArray(elements);
                }
                return oldValue;
            } finally {
                lock.unlock();
            }
        }
    
        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 迭代器是只读的,不支持增删改。因为迭代器遍历的仅仅是一个快照,而对快照进行增删改是没 有意义的。

    2.2.ConcurrentHashMap(key 无序)和 ConcurrentSkipListMap(key 有序)

    ConcurrentHashMap与ConcurrentSkipListMap中key value 都不能为空,否则会空指针

    ConcurrentHashMap容器相较于CopyOnWrite容器在并发加锁粒度上有了更大一步的优化,它通过修改对单个hash桶元素加锁的达到了更细粒度的并发控制。

    • 在发生hash冲突时仅仅只锁住当前需要添加节点的头元素即可,可能是链表头节点或者红黑树的根节点,其他桶节点都不需要加锁,大大减小了锁粒度。
    • ConcurrentHashMap容器是通过CAS + synchronized一起来实现并发控制的。
     /** Implementation for put and putIfAbsent */
        final V putVal(K key, V value, boolean onlyIfAbsent) {
            if (key == null || value == null) throw new NullPointerException();
            int hash = spread(key.hashCode()); //计算key的hash值
            int binCount = 0;
            //循环插入元素,避免并发插入失败
            for (Node<K,V>[] tab = table;;) {
                Node<K,V> f; int n, i, fh;//f是hash桶的头结点
                if (tab == null || (n = tab.length) == 0)
                    tab = initTable();
                else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                     //如果当前hash桶无元素,通过CAS操作插入新节点
                    if (casTabAt(tab, i, null,
                                 new Node<K,V>(hash, key, value, null)))
                        break;                   // no lock when adding to empty bin
                }
                //不断循环计算table(散列表)的每个桶位(slot)的散列值i ,直到找到tab[i] 为空的桶位,casTabAt将put(增加)的节点Node 放到空仓(empty bin)中,如果在put 的过程中,别的线程更改了tab[i],导致tab[i] 不为空,那么casTabAt返回false,继续循环找tab[i]== null的桶位。
    
                //如果当前桶正在扩容,则协助扩容
                else if ((fh = f.hash) == MOVED)
                    tab = helpTransfer(tab, f);
                else {
                    V oldVal = null;
               //hash冲突时锁住当前需要添加节点的头元素,可能是链表头节点或者红黑树的根节点
                    synchronized (f) {
                        if (tabAt(tab, i) == f) {
                            if (fh >= 0) {
                                binCount = 1;
                                for (Node<K,V> e = f;; ++binCount) {
                                    K ek;
                                    if (e.hash == hash &&
                                        ((ek = e.key) == key ||
                                         (ek != null && key.equals(ek)))) {
                                        oldVal = e.val;
                                        if (!onlyIfAbsent)
                                            e.val = value;
                                        break;
                                    }
                                    Node<K,V> pred = e;
                                    if ((e = e.next) == null) {
                                        pred.next = new Node<K,V>(hash, key,
                                                                  value, null);
                                        break;
                                    }
                                }
                            }
                            else if (f instanceof TreeBin) {
                                Node<K,V> p;
                                binCount = 2;
                                if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                               value)) != null) {
                                    oldVal = p.val;
                                    if (!onlyIfAbsent)
                                        p.val = value;
                                }
                            }
                        }
                    }
                    if (binCount != 0) {
                        if (binCount >= TREEIFY_THRESHOLD)
                            treeifyBin(tab, i);
                        if (oldVal != null)
                            return oldVal;
                        break;
                    }
                }
            }
            addCount(1L, binCount);
            return null;
        }
    

    2.3 Set

    Set 接口的两个实现是
    CopyOnWriteArraySet 参考CopyOnWriteArrayList
    ConcurrentSkipListSe 参考ConcurrentSkipListMap

    2.4 Queue

    阻塞与非阻塞,所谓阻塞指的是当队列已满时,入队操作阻塞;当队列已空时,出队操作阻塞。
    单端与双端,单端指的是只能队尾入队,队首出队;而双端指的是队首队尾皆可入 队出队。
    阻塞队列都用 Blocking 关键字标识,单端队列使用 Queue 标识,双端队 列使用 Deque 标识。

    1.单端阻塞队列

    • ArrayBlockingQueue、
    • LinkedBlockingQueue、
    • SynchronousQueue、
    • LinkedTransferQueue、
    • PriorityBlockingQueue、
    • DelayQueue。

    内部一般会持有一个队列,这个队列可以是数组(其实现是 ArrayBlockingQueue)也可以是链表(其实 现是 LinkedBlockingQueue);甚至还可以不持有队列(其实现是 SynchronousQueue),
    LinkedTransferQueue融合了LinkedBlockingQueue、 SynchronousQueue功能 性能更好
    PriorityBlockingQueue 支持按优先级出队
    DelayQueue 支持延时出队

    2.双端阻塞队列
    其实现是 LinkedBlockingDeque。

    3.单端非阻塞队列
    其实现是 ConcurrentLinkedQueue。

    4.双端非阻塞队列
    其实现是 ConcurrentLinkedDeque。

    阻塞队列相关API方法的区别

  • 相关阅读:
    Scrapy 概览笔记
    Python 依赖版本控制 (requirements.txt 文件生成和使用)
    Python 虚拟空间的使用
    macOS 所有版本 JDK 安装指南 (with Homebrew)
    鉴权那些事
    Java 位运算符和 int 类型的实现
    ASP.NET Core 入门教程 5、ASP.NET Core MVC 视图传值入门
    如何做好一次知识或技术分享
    ASP.NET Core 入门教程 4、ASP.NET Core MVC控制器入门
    ASP.NET Core 入门教程 3、ASP.NET Core MVC路由入门
  • 原文地址:https://www.cnblogs.com/shinyrou/p/13305629.html
Copyright © 2011-2022 走看看