4.JUC之二-并发集合

1.AQS

1.1.基础概念

aqs原理:

AQS是AbstractQueuedSynchronizer的简称,本身是抽象类,为其他同步的工具提供基础能力,是concurrent包下类的核心

image

1.2.数据结构

AQS全称 AbstractQueuedSynchronizer。是JUC包下很多并发工具的实现基础,由两部分 数据组成

  1. state,是int类型的,代表了加锁的状态,通过CAS修改state。当state=0时 表示无锁;当state=1时加锁成功;当state>1时表示重入锁重入次数。
  2. 双向列表。保存需要执行任务的线程,以及任务状态。

image

使用LockSupport控制正在排队的线程等待获得机会去获得锁。

1.3.实现原理

  • 锁的类型提供了独占锁与共享锁,可以继承AQS实现相关抽象方法。AQS采用模板模式封装好加锁/解锁的逻辑。
  • aqs默认实现的是非公平锁。线程尝试获得锁失败后,会将线程与等待的状态封装为node对象,使用CAS的方式保存到队尾,调用park阻塞任务线程,等待释放后唤醒。
  • 头节点释放同步状态后,将会唤醒其后继节点,后继节点被唤醒后需要检查前一个节点的状态,并尝试获得锁(采用非公平锁唤醒后还是要竞争)。

双向链表的头结点,在抢锁成功后,会将线程的引用为null,方便GC回收垃圾。

双向链向前是为了方便实现中断,当节点被唤醒后能够检测前面节点的状态,如果是已经被取消的,就可以移除前面的节点,让自己更快获得锁。 向后是也为了方便唤醒后续阻塞的线程。

1.4.任务状态

  • CANCELLED=1;因为超时或者中断,节点会被设置为取消状态,被取消的节点时不会参与到竞争中的,他会一直保持取消状态不会转变为其他状态;
  • SIGNAL=-1;后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以运行
  • CONDITION=-2;节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()后,改节点将会从等待队列中转移到同步队列中,加入到同步状态的获取中
  • PROPAGATE=-3;表示下一次共享式同步状态获取将会无条件地传播下去

1.5.同步模式

  • Exclusive(独占,只有一个线程能执行,如ReentrantLock)
  • Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)

1.6.核心方法

  • 关于state的方法主要有一下三种
    getState():返回同步状态的当前值;
    setState(int newState):设置当前同步状态;
    compareAndSetState(int expect, int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性;
    
  • 自定义同步器实现时主要实现以下几种方法
    tryAcquire(int arg):独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取同步状态
    tryRelease(int arg):独占式释放同步状态;
    tryAcquireShared(int arg):共享式获取同步状态,返回值大于等于0则表示获取成功,否则获取失败;
    tryReleaseShared(int arg):共享式释放同步状态;
    isHeldExclusively():当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程所独占;
    
  • 其余方法 ```java //独占式获取同步状态 acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用可重写的tryAcquire(int arg)方法; acquireInterruptibly(int arg):与acquire(int arg)相同,但是该方法响应中断,当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException异常并返回; tryAcquireNanos(int arg,long nanos):超时获取同步状态,如果当前线程在nanos时间内没有获取到同步状态,那么将会返回false,已经获取则返回true;

//共享式获取同步状态 acquireShared(int arg):共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态; acquireSharedInterruptibly(int arg):共享式获取同步状态,响应中断; tryAcquireSharedNanos(int arg, long nanosTimeout):共享式获取同步状态,增加超时限制;

//释放同步状态 release(int arg):独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒; releaseShared(int arg):共享式释放同步状态;


### 1.7.并发容器

JUC其实指的是java.util.concurrent包下的并发集合类,都是使用lock机制实现的线程安全的集合。相比同步容器,并发容器的效率更高,操作跟简单。

1. 以concurrent开头的集合类

```java
//默认是支持16个线程并发写入,当超过了16个的时候,可能有些线程是需要等待的。它可以设置initialCapacity,设置线程数量
ConcurrentHashMap<K, V>

ConcurrentLinkedDeque<E>

//当使用多个线程访问集合的时候,这个集合是很好的选择。它不允许null,他实现了多线程的高效访问,访问时无需等待。
ConcurrentLinkedQueue<E>

ConcurrentNavigableMap<K, V>

ConcurrentSkipListMap<K, V>

concurrent的集合支持多个线程并发写入访问,写入的时候所有的操作都是线程安全的,但读取的时候不必锁定。 这些集合类采用更加复杂的算法保证远不会锁住整个集合,因此在并发写入的时候有较好的性能。

  1. 以CopyOnWrite开头的集合类

CopyOnWriteArrayList<E>

CopyOnWriteArraySet<E>

CopyOnWriteArraySet它的底层实现是CopyOnWriteArrayList,所以他们的实现原理是相同的。

他们的实现原理:复制底层的数组的方式,来实现写的操作。

当读取集合的时候,线程直接读取集合本身,无需加锁和阻塞。

当写入集合的时候,该集合底层会复制一份新的数组,接下来的操作是对新的数据。整个过程是对原集合的副本进行的,所以线程是安全的。

但是,他的写入时对数组的操作,所以写入到时候,性能是比较差的。但是读取是直接读取原数组,且不需要加锁,因此操作是很快,很安全的。

这两个类适用于读取操作很多的场合。

2.CopyOnWriteArrayList

2.1.介绍

CopyOnWrite,顾名思义:每次都是从主内存中获得最新的数组,copy一个新的数组,程序是操作这个新的数组的值,再将数组赋值给集合。

这样做的好处是读操作不加锁的,为了防止写的操作影响读操作

CopyOnWriteArrayList的add()方法

public boolean add(E e) {
    // 获取独占锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 获取array
        Object[] elements = getArray();
        // 复制array到新数组,添加元素到新数组
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        // 替换数组
        setArray(newElements);
        return true;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

CopyOnWriteArrayList 内部维护了一个数组,成员变量 array 就指向这个内部数组,所有的读操作都是基于新的array对象进行的。

因为上了独占锁,所以如果多个线程调用add()方法只有一个线程会获得到该锁,其他线程被阻塞,知道锁被释放, 由于加了锁,所以整个操作的过程是原子性操作

CopyOnWriteArrayList 会将 新的array复制一份,然后在新复制处理的数组上执行增加元素的操作,执行完之后再将复制的结果指向这个新的数组。

CopyOnWriteArrayList在单线程的情况下,性能远远低于ArrayList

image

2.2.优缺点

优点:

  1. 读写分离
  2. 最终数据结果的一致性(弱一致性)

缺点:

  1. 写操作会复制一个数组出来,频繁的调用写操作,不仅浪费CPU资源,会导致出现大量的GC。影响JVM的性能。
  2. 因为写操作是加锁的,读操作不加锁,这样就导致了,多线程操作的时候,读取到的数据可能不是最新的,实时性差。
  3. 性能不好,写操作消耗的计算机资源较多,如果集合的内容较大,在频繁的调用写操作时会严重影响JVM的性能
  4. 弱一致性。是优点也是缺点
  5. 迭代器不支持增删改

所以CopyOnWriteArrayList适合读多写少的场景

2.3.【缺点】弱一致性

调用iterator方法获取迭代器返回一个COWIterator对象

COWIterator的构造器里主要是 保存了当前的list对象的内容和遍历list时数据的下标。

snapshot是list的快照信息,因为CopyOnWriteArrayList的读写策略中都会使用getArray()来获取一个快照信息,生成一个新的数组。

所以在使用该迭代器元素时,其他线程对该lsit操作是不可见的,因为操作的是两个不同的数组所以造成弱一致性。

代码如下:

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 boolean hasNext() {
        return cursor < snapshot.length;
    }

    public boolean hasPrevious() {
        return cursor > 0;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        if (!hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }
}    

2.4.【缺点】迭代器不支持增删改

CopyOnWriteArrayList 迭代器是只读的,不支持增删操作

CopyOnWriteArrayList迭代器中的 remove()和 add()方法,没有支持增删而是直接抛出了异常

因为迭代器遍历的仅仅是一个快照,而对快照进行增删改是没有意义的。

// 尝试使用iterator删除元素,结果报错了。这个和ArrayList不一样???
private static void CopyOnWriteArrayListTest(){
    CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    list.add("test1");
    list.add("test2");
    list.add("test3");
    list.add("test4");

    Iterator<String> iterator = list.iterator();

    while (iterator.hasNext()){
        if ("test1".equals(iterator.next())){
            iterator.remove();
        }
    }

    System.out.println(list.toString());
}

Exception in thread "main" java.lang.UnsupportedOperationException
 at java.util.concurrent.CopyOnWriteArrayList$COWIterator.remove(CopyOnWriteArrayList.java:1178)

3.CopyOnWriteArraySet

3.1.对比HashSet

因为CopyOnWriteArraySet底层的实现使用的是CopyOnWriteArrayList,所以数据是有序的,且支持排序。

在执行Add/remove操作的时候,调用的还是CopyOnWriteArrayList的addIfAbsent方法。

image

image

需要注意的是CopyOnWriteArraySet的removeAll和addAll都是线程安全的。

4.ConcurrentSkipListSet

4.1.对比TreeSet

TreeSet内部实现使用的是TreeMap,默认使用key的Comparable实现进行排序

ConcurrentSkipListSet内部实现使用的是ConcurrentSkipListMap,默认使用key的Comparable实现进行排序。

由于ConcurrentSkipListMap内部的现实是没有使用锁的。所以在add和remove操作的时候性能比较好,是并发集合,却不是线程安全的集合。比如进行addAll和removeAll操作的时候是可能被多个线程同时操作的。

image

image

5.ConcurrentHashMap

是hashMap的并发容器,内部采用分段锁的方式进行并发操作。每一个段内保存一个Node<K, V>。 写操作的时候只对需要修改的node进行同步锁(synchronized),读的时候会将上锁的node最后读取。

6.ConcurrentSkipListMap

和ConcurrentHashMap相比,ConcurrentSkipListMap性能要差很多,但是ConcurrentSkipListMap是有序的。

7.Queue 和 Deque

7.1.区别

  • Queue 是 继承自 Collection 的接口,代表先进先出(FIFO)的数据结构,即元素按照添加的顺序依次被取出。
  • Deque 是 Queue 的子接口,全称为双端队列(Double - Ended Queue)。它不仅支持元素的先进先出,还支持在队列的两端进行插入和删除操作, 因此可以当作栈(后进先出,LIFO)或者普通队列(先进先出,FIFO)来使用。

7.2.常见的Queue

阻塞队列的数据结构符合FIFO规则。所以是线程安全的(使用put/take)

image

image

常见的阻塞队列有

  • ArrayBlockingQueue,有界的阻塞队列,初始化时制定界限,之后不可修改
  • DelayQueue, 会按照线程过期时间进行排序,比如定时关闭连接,缓存对象,请求超时处理等场景。
  • LinkedBlockingQueue,可指定界限的阻塞队列
  • PriorityBlockingQueue,按照保持对象的Comparable现实进行排序的队列
  • SynchronousQueue,只允许存入一个对象的对象

7.3.Queue核心方法

使用建议:他们各自代表了一种数据类型或数据的处理方式,这样能够使我们在处理数据的时候,保证逻辑清楚,效果可预测。

  1. 直接响应结果,抛出异常:add和remove配合使用
  2. 直接响应结果,不抛异常:offer和poll配合使用,
  3. 阻塞等待:put和take配合使用。
1. 插入数据
抛出异常 :add(e)//队列未满时,返回true;队列满则抛出IllegalStateException("Queue full")异常——AbstractQueue
返回值(非阻塞):offer(e)//队列未满时,返回true;队列满时返回false。非阻塞立即返回。
一定时间内返回值:offer(e, time, unit)//设定等待的时间,如果在指定时间内还不能往队列中插入数据则返回false,插入成功返回true。
返回值(阻塞):put(e)//队列未满时,直接插入没有返回值;队列满时会阻塞等待,一直等到队列未满时再插入。

2.获得数据
抛出异常:remove()//队列不为空时,返回队首值并移除;队列为空时抛出NoSuchElementException()异常——AbstractQueue
返回值(非阻塞):poll()//队列不为空时返回队首值并移除;队列为空时返回null。非阻塞立即返回。
一定时间内返回值 :poll(time,unit)//设定等待的时间,如果在指定时间内队列还未孔则返回null,不为空则返回队首值
返回值(阻塞) :take(e)//队列不为空返回队首值并移除;当队列为空时会阻塞等待,一直等到队列不为空时再返回队首值。

7.4.设计一个不会内存溢出的队列

https://github.com/apache/dubbo/pull/9722/files


文章信息

张雪随笔
master
Homepage
Issues
Download

This Software is under the terms of The Unlicense.