CopyOnWriteArrayList 如何通过写时拷贝实现并发安全的 List?

CopyOnWrite(COW), 是计算机程序设计领域中的一种优化策略, 即写入时复制. 其机制当有多个线程同时去请求一个资源时(可以是内存中的一个数据), 当其中一个线程要对资源进行修改, 系统会copy一个副本给该线程, 让其进行修改, 而其他线程所拥有的资源并不会由于该线程对资源的改动而发生改变.

如果用代码来描述的话,就是创建多个线程, 在每个线程中如果修改共享变量, 那么就将此变量进行一次拷贝操作, 每次的修改都是对副本进行.

java.util.concurrent包中提供了两个CopyOnWrite机制容器,分别为CopyOnWriteArrayList和CopyOnWriteArraySet.

CopyOnWriteArrayList添加元素:在添加元素之前进行加锁操作,保证数据的原子性。在添加过程中,进行数组复制,修改操作,再将新生成的数组复制给集合中的array属性. 最后释放锁. 由于array属性被volatile修饰, 所以当添加完成后, 其他线程就可以立刻查看到被修改的内容.

CopyOnWriteArrayList读取元素get方法没有进行加锁处理.

机制的优缺点: 保证了数据在多线程操作时的最终一致性, 缺点就是内存空间的浪费, 不能保证实时的数据一致性.

随机数生成器 Random 类如何使用 CAS 算法保证多线程下新种子的唯一性?

Random里的seed用于控制生成的随机数, 每次生成后都会更新, 而这个seed又是一个AtomicLong类型对象, 所以在多线程下是可以保证seed的唯一性的.

谈下对基于链表的非阻塞无界队列 ConcurrentLinkedQueue 原理的理解?

An unbounded thread-safe queue based on linked nodes. This queue orders elements FIFO (first-in-first-out). The head of the queue is that element that has been on the queue the longest time. The tail of the queue is that element that has been on the queue the shortest time. New elements are inserted at the tail of the queue, and the queue retrieval operations obtain elements at the head of the queue. A ConcurrentLinkedQueue is an appropriate choice when many threads will share access to a common collection. Like most other concurrent collection implementations, this class does not permit the use of null elements.

This implementation employs an efficient non-blocking algorithm based on one described in Simple, Fast, and Practical Non-Blocking and Blocking Concurrent Queue Algorithms by Maged M. Michael and Michael L. Scott.

Iterators are weakly consistent, returning elements reflecting the state of the queue at some point at or since the creation of the iterator. They do not throw java.util.ConcurrentModificationException, and may proceed concurrently with other operations. Elements contained in the queue since the creation of the iterator will be returned exactly once.

Beware that, unlike in most collections, the size method is NOT a constant-time operation. Because of the asynchronous nature of these queues, determining the current number of elements requires a traversal of the elements, and so may report inaccurate results if this collection is modified during traversal. Additionally, the bulk operations addAll, removeAll, retainAll, containsAll, equals, and toArray are not guaranteed to be performed atomically. For example, an iterator operating concurrently with an addAll operation might view only some of the added elements.

This class and its iterator implement all of the optional methods of the Queue and Iterator interfaces.

Memory consistency effects: As with other concurrent collections, actions in a thread prior to placing an object into a ConcurrentLinkedQueue happen-before actions subsequent to the access or removal of that element from the ConcurrentLinkedQueue in another thread.

内部类Node实现了一些CAS方法, 用于节点操作

private static class Node<E> {
volatile E item;
volatile Node<E> next; /**
* Constructs a new node. Uses relaxed write because item can
* only be seen after publication via casNext.
*/
Node(E item) {
UNSAFE.putObject(this, itemOffset, item);
} boolean casItem(E cmp, E val) {
return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
} void lazySetNext(Node<E> val) {
UNSAFE.putOrderedObject(this, nextOffset, val);
} boolean casNext(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}

offer() 方法的源代码

public boolean offer(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e); for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) {
// p is last node
if (p.casNext(null, newNode)) {
// Successful CAS is the linearization point
// for e to become an element of this queue,
// and for newNode to become "live".
if (p != t) // hop two nodes at a time
casTail(t, newNode); // Failure is OK.
return true;
}
// Lost CAS race to another thread; re-read next
}
else if (p == q)
// We have fallen off list. If tail is unchanged, it
// will also be off-list, in which case we need to
// jump to head, from which all live nodes are always
// reachable. Else the new tail is a better bet.
p = (t != (t = tail)) ? t : head;
else
// Check for tail updates after two hops.
p = (p != t && t != (t = tail)) ? t : q;
}
}

ConcurrentLinkedQueue 内部是如何使用 CAS 非阻塞算法来保证多线程下入队出队操作的线程安全?

casItem和casNext, 如果设置不成功就再到新位置再试一次, 直到成功

基于链表的阻塞队列 LinkedBlockingQueue 原理

通过ReentrantLock的lock()和await()实现的阻塞, 再底下的阻塞实现是用的AQS的acquireQueued()方法

阻塞队列LinkedBlockingQueue 内部是如何使用两个独占锁 ReentrantLock 以及对应的条件变量保证多线程先入队出队操作的线程安全?

A variant of the "two lock queue" algorithm. The putLock gates entry to put (and offer), and has an associated condition for waiting puts. Similarly for the takeLock. The "count" field that they both rely on is maintained as an atomic to avoid needing to get both locks in most cases. Also, to minimize need for puts to get takeLock and vice-versa, cascading notifies are used. When a put notices that it has enabled at least one take, it signals taker. That taker in turn signals others if more items have been entered since the signal. And symmetrically for takes signalling puts. Operations such as remove(Object) and iterators acquire both locks. Visibility between writers and readers is provided as follows:

Whenever an element is enqueued, the putLock is acquired and count updated. A subsequent reader guarantees visibility to the enqueued Node by either acquiring the putLock (via fullyLock) or by acquiring the takeLock, and then reading n = count.get(); this gives visibility to the first n items.

To implement weakly consistent iterators, it appears we need to keep all Nodes GC-reachable from a predecessor dequeued Node. That would cause two problems:

  • allow a rogue Iterator to cause unbounded memory retention
  • cause cross-generational linking of old Nodes to new Nodes if a Node was tenured while live, which generational GCs have a hard time dealing with, causing repeated major collections.

    However, only non-deleted Nodes need to be reachable from dequeued Nodes, and reachability does not necessarily have to be of the kind understood by the GC. We use the trick of linking a Node that has just been dequeued to itself. Such a self-link implicitly means to advance to head.next.

    删除时为了避免GC回收问题, 会把被删除节点的next指向自己

底层用单向链表存储数据, 可以用作有界队列或者无界队列, 默认无参构造函数的容量为Integer.MAX_VALUE. 从类图中可以看到, LinkedBlockingQueue使用了takeLock和putLock两把锁, 分别用于阻塞队列的读写线程,

  • head用来管理元素出队, 有 take(), poll(), peek() 三个操作
  • tail用来管理元素入队, 有 put(), offer() 两个操作
    private final ReentrantLock takeLock = new ReentrantLock();    /* 读锁 */
private final Condition notEmpty = takeLock.newCondition(); /* 读锁对应的条件 */
private final ReentrantLock putLock = new ReentrantLock(); /* 写锁 */
private final Condition notFull = putLock.newCondition(); /* 写锁对应的条件 */
  • 在head上take时, 需要拿到takeLock, 如果队列为空, 就notEmpty.await(), 如果队列不为空, 就notFull.signal()
  • 在tail上put时, 需要拿到puLock, 如果队列满了, 就notFull.await(), 如果队列还没满, 就notEmpty.signal()

也就是说读线程和写线程可以同时运行, 在多线程高并发场景, 应该可以有更高的吞吐量, 性能比单锁更高.

ArrayBlockingQueue

ArrayBlockingQueue,底层用数组存储数据,属于有界队列,初始化时必须指定队列大小,count记录当前队列元素个数,takeIndex和putIndex分别记录出队和入队的数组下标边界,都在[0,items.length-1]范围内循环使用,同时满足0<=count<=items.length。在提供的阻塞方法put/take中,共用一个Lock实例,分别在绑定的不同的Condition实例处阻塞,如put在队列满时调用notFull.await(),take在队列空时调用notEmpty.await()

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();
}
} private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}

ArrayBlockingQueue和LinkedBlockingQueue的区别

  1. LinkedBlockingQueue是基于链表实现的初始化, 可以不指定队列大小(默认Integer.MAX_VALUE), 而ArrayBlockingQueue是基于数组实现, 初始化时必须指定大小
  2. LinkedBlockingQueue在puts操作会生成新的Node对象, takes操作Node对象在某一时间会被gc, 可能会影响gc性能, ArrayBlockingQueue是固定的数组长度循环使用, 不会出现对象的产生与回收
  3. LinkedBlockingQueue基于链表, 在remove操作时不需移动数据, ArrayBlockingQueue是基于数组, 在remove时需要移动数据, 影响性能
  4. LinkedBlockingQueue使用两个锁将puts操作与takes操作分开, 而ArrayBlockingQueue使用一个锁的两个条件, 在高并发的情况下LinkedBlockingQueue的性能较好

Java多线程专题6: Queue和List的更多相关文章

  1. Java多线程专题1: 并发与并行的基础概念

    合集目录 Java多线程专题1: 并发与并行的基础概念 什么是多线程并发和并行? 并发: Concurrency 特指单核可以处理多任务, 这种机制主要实现于操作系统层面, 用于充分利用单CPU的性能 ...

  2. Java多线程专题2: JMM(Java内存模型)

    合集目录 Java多线程专题2: JMM(Java内存模型) Java中Synchronized关键字的内存语义是什么? If two or more threads share an object, ...

  3. Java多线程专题3: Thread和ThreadLocal

    合集目录 Java多线程专题3: Thread和ThreadLocal 进程, 线程, 协程的区别 进程 Process 进程提供了执行一个程序所需要的所有资源, 一个进程的资源包括虚拟的地址空间, ...

  4. Java多线程专题4: 锁的实现基础 AQS

    合集目录 Java多线程专题4: 锁的实现基础 AQS 对 AQS(AbstractQueuedSynchronizer)的理解 Provides a framework for implementi ...

  5. Java多线程专题5: JUC, 锁

    合集目录 Java多线程专题5: JUC, 锁 什么是可重入锁.公平锁.非公平锁.独占锁.共享锁 可重入锁 ReentrantLock A ReentrantLock is owned by the ...

  6. Java多线程15:Queue、BlockingQueue以及利用BlockingQueue实现生产者/消费者模型

    Queue是什么 队列,是一种数据结构.除了优先级队列和LIFO队列外,队列都是以FIFO(先进先出)的方式对各个元素进行排序的.无论使用哪种排序方式,队列的头都是调用remove()或poll()移 ...

  7. Java多线程20:多线程下的其他组件之CountDownLatch、Semaphore、Exchanger

    前言 在多线程环境下,JDK给开发者提供了许多的组件供用户使用(主要在java.util.concurrent下),使得用户不需要再去关心在具体场景下要如何写出同时兼顾线程安全性与高效率的代码.之前讲 ...

  8. Java多线程总结之线程安全队列Queue

    在Java多线程应用中,队列的使用率很高,多数生产消费模型的首选数据结构就是队列.Java提供的线程安全的Queue可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是BlockingQueue,非 ...

  9. 使用goroutine+channel和java多线程+queue队列的方式开发各有什么优缺点?

    我感觉很多项目使用java或者c的多线程库+线程安全的queue数据结构基本上可以实现goroutine+channel开发能达到的需求,所以请问一下为什么说golang更适合并发服务端的开发呢?使用 ...

随机推荐

  1. 【剑指Offer】二进制中1的个数 解题报告(Python)

    题目地址:https://www.nowcoder.com/ta/coding-interviews 题目描述 输入一个整数,输出该数二进制表示中1的个数.其中负数用补码表示. 解题方法 这个题如果使 ...

  2. [Elasticsearch] ES聚合场景下部分结果数据未返回问题分析

    背景 在对ES某个筛选字段聚合查询,类似groupBy操作后,发现该字段新增的数据,聚合结果没有展示出来,但是用户在全文检索新增的筛选数据后,又可以查询出来, 针对该问题进行了相关排查. 排查思路 首 ...

  3. SOFA 数据透析

    数据透传: 在 RPC调用中,数据的传递,是通过接口方法参数来传递的,需要接口方定义好一些参数允许传递才可以,在一些场景下,我们希望,能够更通用的传递一些参数,比如一些标识性的信息.业务方可能希望,在 ...

  4. 【Java例题】3.4求a+aa+aaa+aaaa+... ...+aa...a(n个

    4. package chapter3; import java.util.*; public class demo4 { public static void main(String[] args) ...

  5. 第十六个知识点:描述DSA,Schnorr,RSA-FDH的密钥生成,签名和验证

    第十六个知识点:描述DSA,Schnorr,RSA-FDH的密钥生成,签名和验证 这是密码学52件事系列中第16篇,这周我们描述关于DSA,Schnorr和RSA-FDH的密钥生成,签名和验证. 1. ...

  6. Google Chrome调整控制台的位置

    众所周知,控制台是开发必备的工具,学会流畅的使用控制台会给我们的开发带来不一样的体验,但是控制台的位置有时却是困扰我们的一件事,控制台默认是在浏览器内,有时十分妨碍我们,那么有没有什么办法修改控制台的 ...

  7. .NET+Sqlite如何支持加密

    .NET+Sqlite如何支持加密 Sqlite SQLite 来源于公共领域 SQLite Is Public Domain. 确保代码不会受到任何专有或许可内容的污染,没有任何来自互联网上的未知来 ...

  8. python极简教程01:基础变量

    测试奇谭,BUG不见. 其实很久之前,就有身边的同事或者网友让我分享一些关于python编程语言的教程,他们同大多数自学编程语言的人一样,无外乎遇到以下这些问题: 网络上的资料过多且良莠不全,不知道如 ...

  9. Oracle数据库导入csv文件(sqlldr命令行)

    1.说明 Oracle数据库导入csv文件, 当csv文件较小时, 可以使用数据库管理工具, 比如DBevaer导入到数据库, 当csv文件很大时, 可以使用Oracle提供的sqlldr命令行工具, ...

  10. Ranger-Kylin插件安装

    Ranger-Kylin插件安装, 从Ranger1.1.0版本开始支持Ranger Kylin插件, 从Kylin2.3.0版本开始支持Ranger Kylin插件的权限控制. 1.获取安装包 sc ...