并发容器

ConcurrentHashMap

JDK1.6采用Segment分段锁技术提高并发访问效率,首先将数据分成一段一段的存储,然后给每一个段数据配一个锁,这样就可以多线程访问不同段的数据而不受竞争影响。
JDK1.8采用CAS+Synchronized保证并发更新安全,采用数组+链表+红黑树存储结构,默认table[16],put等操作会采用CAS更新,找到table[i]后会将此索引处的链表进行锁定后操作

   public V put(K key, V value) {
        return putVal(key, value, false);
    }
   /** 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());//计算hash值,两次hash操作
        int binCount = 0;//统计链表长度
        for (Node<K,V>[] tab = table;;) {//类似于while(true),死循环,直到插入成功 
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)//检查是否初始化了,如果没有,则初始化
                tab = initTable();
                /*
                    i=(n-1)&hash 等价于i=hash%n(前提是n为2的幂次方).即取出table中位置的节点用f表示。
                    有如下两种情况:
                    1、如果table[i]==null(即该位置的节点为空,没有发生碰撞),则利用CAS操作直接存储在该位置,
                        如果CAS操作成功则退出死循环。
                    2、如果table[i]!=null(即该位置已经有其它节点,发生碰撞)
                */
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)//检查table[i]的节点的hash是否等于MOVED,如果等于,则检测到正在扩容,则帮助其扩容
                tab = helpTransfer(tab, f);//帮助其扩容
            else {//运行到这里,说明table[i]的节点的hash值不等于MOVED。
                V oldVal = null;
                synchronized (f) {//锁定,(hash值相同的链表的头节点)
                    if (tabAt(tab, i) == f) {//避免多线程,需要重新检查
                        if (fh >= 0) {//链表节点
                            binCount = 1;
                            /*
                            下面的代码就是先查找链表中是否出现了此key,如果出现,则更新value,并跳出循环,否则将节点加入到索引处链表的尾部
                            */
                            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)//仅putIfAbsent()方法中onlyIfAbsent为true
                                        e.val = value;//putIfAbsent()包含key则返回get,否则put并返回  
                                    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)//实则是>8,执行else,说明该桶位本就有Node
                        treeifyBin(tab, i);//若length<64,直接tryPresize,两倍table.length;不转树 
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

put流程

  1. 未初始化则初始化
  2. hash计算得到索引i,table[i]处头节点为null,则插入新节点 return
  3. table[i]处节点已有值,synchronized锁定此链表后遍历此节点处的链表,已存在则更新,不存在key则在此链表尾部插入

扩容流程

默认table大小16,若某索引位置的链表长度达到8,则把此链表转换成红黑树。若某索引位置的树达到64个节点,则进行table扩容,扩容大小是原先的2倍。
第一个线程进行扩容操作,假如操作到table[2]处,线程2进行put操作,会发现此时table正在进行扩容且扩容到该索引处(table[i].hash = MOVE),则线程2也开始进行扩容(扩容索引为下一个没有进行过的索引处),可能会有多个线程辅助进行扩容完成。

CopyOnWriteArrayList

Copy-On-Write简称COW,写时复制,也是一种读写分离的容器
优点:
1. 读写分离,读与写操作的不是同一个容器,并发时读操作没有同步机制具有很高的性能 ,写操作会重新生成个数组替换原数组
2. 集合遍历时无同步机制,性能高(遍历的是原数组)

缺点:
1. 内存占用问题:添加元素时会进行原数组的复制,数组比较大时性能开销很大
2. 数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

使用场景
对容器的写操作比较少,读操作多的情况下使用,比如白名单,黑名单,商品类目的访问和更新场景

    private transient volatile Object[] array;

    final Object[] getArray() {
        return array;
    }


    final void setArray(Object[] a) {
        array = a;
    }

    /**
     * 读操作
     * 读取的是底层数组array
     */
    public E get(int index) {
        return get(getArray(), index);
    }

    private E get(Object[] a, int index) {
        return (E) a[index];
    }

   /**
    * Set操作 重入锁 locak上进行锁定,保证多线程下只有一个线程进行写操作
    * copy一个新数组,然后新数组引用替换原数组引用
    */
    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();
        }
    }  

BlockingQueue、BlockingDeque (FIFO)

BlockingQueue阻塞队列,有基于数组、链表两类实现

BlockingDeque阻塞双端队列只有链表实现LinkedBlockingDeque

1. ArrayBlockingQueue

基于数组实现的阻塞队列,包括非阻塞入队、出队方法,阻塞入队出队方法,
相应方法又包括失败返回false与失败抛出异常两类

阻塞方法采用ReentrantLock的Condition实现(默认ArrayBlockingQueue采用非公平锁),阻塞实现如下:

     final ReentrantLock lock;
    /** Condition for waiting takes */
    private final Condition notEmpty = lock.newCondition();

    /** Condition for waiting puts */
    private final Condition notFull = lock.newCondition();

   //入队操作 队列满condition等待 
   public void put(E e) throws InterruptedException {
        checkNotNull(e);
        //同一时刻只能有一个线程进行put
        final ReentrantLock lock = this.lock;
        //重入锁,单个线程可以获取多次锁
        lock.lockInterruptibly();
        try {
            //队列满,执行等待,只可能有一个线程进行等待
            //多个线程的话会在获取锁处自旋
            while (count == items.length)
                notFull.await();
            //enqueue操作不需要考虑多线程
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }   

  //入队
  private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        //如果有等待出队的线程则唤醒等待出队的线程
        notEmpty.signal();
    }   



   /*
    * 出队操作,队列为空则等待
    */
   public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }


  private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        //如果有等待入队的线程则唤醒
        notFull.signal();
        return x;
    }

**采用ReentrantLock保证同一时刻只能有一个线程操作队列,使用lock的Conndition控制线程的唤醒与阻塞,ReentrantLock若使用公平锁,唤醒顺序与阻塞顺序一致,
阻塞队列FIFO,先进先出**

链表实现的阻塞队列与数组实现的不同点就是节点类型,一个是数组,一个是链表
LinkedBlockingQueue基于链表实现,与ALinkedBlockingQueue阻塞原理一致

2.PriorityBlockingQueue

优先级阻塞队列,基于数组实现。通过实现Comparator接口控制队列元素的优先级,优先级排序采用二分法计算,阻塞实现同ArrayBlockingQueue一样

3.LinkedTransferQueue

LinkedTransferQueue采用的一种预占模式。意思就是消费者线程取元素时,如果队列为空,那就生成一个节点(节点元素为null)入队,然后消费者线程park住,后面生产者线程入队时发现有一个元素为null的节点,生产者线程就不入队了,直接就将元素填充到该节点,唤醒该节点上park住线程,被唤醒的消费者线程拿货走人。这就是预占的意思:有就拿货走人,没有就占个位置等着,等到或超时。

4.SynchronousQueue

如果当前没有人想要消费产品(即当前没有线程执行take),此生产线程必须阻塞,等待一个消费线程调用take操作,take操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程(当然也可以先take后put,原理是一样的)。

可以认为这是一种线程与线程间一对一传递消息的模型

公平模式下: 基于队列实现
队尾匹配队头出队,take操作匹配队尾(1对1)然后队头元素出队,FIFO

非公平模式下: 基于栈实现

put入栈,take入栈,put、take匹配,栈指针向下移动两位

-----
take  ------栈指针    take、put匹配 指针下移两位指向putA
-----
putB
-----
putA
-----

并发Queue、Deque

lock-free: 无锁

An algorithm is lock-free if, when the program threads are run for a sufficiently long time, at least one of the threads makes progress (for some sensible definition of progress). 

如果所有线程运行了足够长时间后,至少有一个线程能获得进展,那么这个算法是无锁的。

用人话讲就是:系统中无论何时,始终有一个线程在工作。

wait-free:无等待

An algorithm is wait-free if every operation has a bound on the number of steps the algorithm will take before the operation completes.

假如一个方法是无等待的,那么它保证了每一次调用都可以在有限的步骤内结束。

用人话讲就是,系统中的所有线程,都会在有限时间内结束,无论如何也不可能出现饿死(starving)的情况。

1. ConcurrentLinkedQueue

采用CAS实现并发队列,wait-free

2. ConcurrentLinkedDeque

采用CAS实现并发队列,wait-free

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值