java - 多线程 06 并发容器与同步容器

一、并发容器和同步容器

  • 同步容器:一般的同步容器线程不安全,或者是线程安全但性能较差;例如 ArrayList 和 Vector(直接在方法上加synchronized,多线程情况下性能较差)、 HashMap 和 HashTable; 可以通过 Collections 类来对线程不安全的集合对象进行包装,使其变得线程安全;

  • 并发容器:CopyOnWriteArrayList、 ConcurrentHashMap、 ConcurrentLinkedQueue、 阻塞队列

 

    1、CopyOnWriteArrayList 的实现

  • 当其他读线程在遍历 array 时,此时遍历的是此时获取到的 array 的引用。而写线程操作时是复制原 array 来新建的一个集合操作,在写操作完成后更新 CopyOnWriteArrayList 的 array 的引用,而读线程获取的 array 引用还是原先的引用。故不会引起线程安全性问题;

public boolean add(E e) {

    final ReentrantLock lock = this.lock;

    lock.lock(); // 加锁后 复制一份 array 来进行 add,保证了其他线程对 array 的遍历的线程安全性

    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();

    }

}

// 读取时,直接 get,不用加锁

public E get(int index) {

    return get(getArray(), index);

}

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

    return (E) a[index];

}

// =============对比 Vector ======================

// 在方法上加 synchronized,锁住了整个 array,使其他线程读写都不能操作,在多线程情况下,这样导致了变成多线程运行

public synchronized boolean add(E e) {

    modCount++;

    ensureCapacityHelper(elementCount + 1);

    elementData[elementCount++] = e;

    return true;

}

// 读取也加了 synchronized

public synchronized E get(int index) {

    if (index >= elementCount)

        throw new ArrayIndexOutOfBoundsException(index);



    return elementData(index);

}

 

    2、非阻塞队列 ConcurrentLinkedQueue

  • 非阻塞队列的实现方式使用 CAS 来实现

  • ConcurrentLinkedQueue 是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当添加一个元素的时候,它会添加到队列的尾部;当获取一个元素时,它会返回队列头部的元素。它采用了“wait-free” 算法(即CAS算法)来实现,该算法在 Michael & Scott 算法上进行了一些修改。

 

        2.1、入队列

            入队列就是将入队节点添加到队列的尾部。入队主要做两件事情:第一是将入队节点设置成当前队列尾节点的下一个节点;第二是更新tail尾节点,如果 tail 节点的 next 节点不为空(tail 节点不是真正意义上的尾节点时),则将入队节点设置成 tail 节点;如果 tail 节点的 next 节点为空(tail 节点就是尾节点),则将入队节点设置成 tail 的 next 节点(但没有把入队节点设置为 tail 节点),所以 tail 节点不总是尾节点;

            在多线程的进行入队的情况下,可能会出现插队的情况。如果有一个线程正在入队,那么它必须先获取尾节点,然后设置尾节点的下一个节点为入队节点,但这时可能有另一个线程插队,那么队列的尾节点就会发生变化,这时当前线程要暂停入队操作,然后重新获取尾节点。

// 入队的过程主要做两件事情:第一是定位出尾节点;第二是使用CAS算法将入队节点设置成尾节点的 next 节点,如不成功则重试

public boolean offer(E e) {

    checkNotNull(e);

    // 创建一个入队节点

    final ConcurrentLinkedQueue.Node<E> newNode = new ConcurrentLinkedQueue.Node<E>(e);

    // 死循环,入队不成功则反复入队

    // 创建一个 t tail节点的引用,p 表示队列的尾节点,默认情况下等于 tail 节点

    for (ConcurrentLinkedQueue.Node<E> t = tail, p = t;;) {

        // 获取 p 节点的下一个节点

        ConcurrentLinkedQueue.Node<E> q = p.next;

        // 若 p 的next 节点为空,说明p 为尾节点,否则 p 不是尾节点,需要更新 p 后再将它指向 next 节点

        if (q == null) {

            // p 是尾节点,将 p 的 next 节点设置为 入队节点(CAS)

            if (p.casNext(null, newNode)) {

                if (p != t) // 判断此时 p 是否不等于 t 尾节点;若此时 p 已被其他线程改变( t -> p -> newNode),则更新尾节点

                    casTail(t, newNode);  // (CAS判断当前t 尾节点是否为真的 尾节点)更新 入队节点 为 尾节点

                return true;

            }

            // Lost CAS race to another thread; re-read next

        }

        // 更新 t p q 的值(定位尾节点)再重复循环

        else if (p == q)

            p = (t != (t = tail)) ? t : head;

        else

            p = (p != t && t != (t = tail)) ? t : q;

    }

}

        2.2、出队列

    • 并不是每次出队时都更新 head 节点,当 head 节点里有元素时,直接弹出 head 节点里的元素,而不会更新 head 节点。只有当 head 节点里没有元素时,出队操作才会更新 head 节点。这种做法也是通过 hops 变量来减少使用 CAS 更新 head 节点的消耗;

    • 出队源码:首先获取头结点的元素,然后判断头结点元素是否为空,如果为空,表示另外一个线程已经进行了一次出队操作将该节点的元素取走,如果不为空,则使用 CAS 的方式将头结点的引用设置成  null ,如果 CAS 成功,则直接返回头结点的元素,如果不成功,表示另外一个线程已经进行了一次出队操作更新了  head 节点,导致元素发生了变化,需要重新获取头结点。

public E poll() {

    restartFromHead:

    for (;;) {

        for (ConcurrentLinkedQueue.Node<E> h = head, p = h, q;;) {

            // 获取 p 节点的元素

            E item = p.item;

            // 如果 p 节点的元素不为空,使用 CAS 设置 p 节点引用的元素为 null

            // 如果成功则返回 p 节点的元素

            if (item != null && p.casItem(item, null)) {

                if (p != h) // hop two nodes at a time

                    // 更新 头结点

                    updateHead(h, ((q = p.next) != null) ? q : p);

                return item;

            }

            else if ((q = p.next) == null) {

                updateHead(h, p);

                return null;

            }

            else if (p == q)

                continue restartFromHead;

            else

                p = q;

        }

    }

}

 

    3、阻塞队列  BlockingQueue

        提供阻塞方法 put()、take() ;使用add()、remove()方法时会抛出异常;offer()、poll()方法有返回值;

// 创建一个 ArrayBlockingQueue 的实例

public ArrayBlockingQueue(int capacity) { 

    this(capacity, false); // 指定队列的大小和是否公平锁

}



// put 方法 - 将元素入队,若队列已满,则将此入队线程 wait

public void put(E e) throws InterruptedException {

    checkNotNull(e);

    final ReentrantLock lock = this.lock;

    lock.lockInterruptibly();

    try {

        while (count == items.length) // 判断是否队满

            notFull.await(); // 将线程等待

        enqueue(e); // 入队,将出队线程唤醒

    } finally {

        lock.unlock();

    }

}



// take 方法 - 将元素出队,若队列为空,则将此出队线程 wait

public E take() throws InterruptedException {

    final ReentrantLock lock = this.lock;

    lock.lockInterruptibly();

    try {

        while (count == 0) // 判断队列是否为空

            notEmpty.await(); // 将出队线程等待

        return dequeue(); // 出队,将入队线程唤醒

    } finally {

        lock.unlock();

    }

}

 

    4、消息队列的实现

        模式:点对点、发布-订阅

        将大量的消息存到消息队列中,处理消息的服务一次能处理多少条消息就从消息队列中取出多少条来处理(若一次性有大量的消息到处理的service服务,可能会导致消息的丢失甚至是服务器宕机);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值