Java多线程专题6: Queue和List
- 合集目录
- Java多线程专题6: Queue和List
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 oflinking 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的区别
- LinkedBlockingQueue是基于链表实现的初始化, 可以不指定队列大小(默认Integer.MAX_VALUE), 而ArrayBlockingQueue是基于数组实现, 初始化时必须指定大小
- LinkedBlockingQueue在puts操作会生成新的Node对象, takes操作Node对象在某一时间会被gc, 可能会影响gc性能, ArrayBlockingQueue是固定的数组长度循环使用, 不会出现对象的产生与回收
- LinkedBlockingQueue基于链表, 在remove操作时不需移动数据, ArrayBlockingQueue是基于数组, 在remove时需要移动数据, 影响性能
- LinkedBlockingQueue使用两个锁将puts操作与takes操作分开, 而ArrayBlockingQueue使用一个锁的两个条件, 在高并发的情况下LinkedBlockingQueue的性能较好
Java多线程专题6: Queue和List的更多相关文章
- Java多线程专题1: 并发与并行的基础概念
合集目录 Java多线程专题1: 并发与并行的基础概念 什么是多线程并发和并行? 并发: Concurrency 特指单核可以处理多任务, 这种机制主要实现于操作系统层面, 用于充分利用单CPU的性能 ...
- Java多线程专题2: JMM(Java内存模型)
合集目录 Java多线程专题2: JMM(Java内存模型) Java中Synchronized关键字的内存语义是什么? If two or more threads share an object, ...
- Java多线程专题3: Thread和ThreadLocal
合集目录 Java多线程专题3: Thread和ThreadLocal 进程, 线程, 协程的区别 进程 Process 进程提供了执行一个程序所需要的所有资源, 一个进程的资源包括虚拟的地址空间, ...
- Java多线程专题4: 锁的实现基础 AQS
合集目录 Java多线程专题4: 锁的实现基础 AQS 对 AQS(AbstractQueuedSynchronizer)的理解 Provides a framework for implementi ...
- Java多线程专题5: JUC, 锁
合集目录 Java多线程专题5: JUC, 锁 什么是可重入锁.公平锁.非公平锁.独占锁.共享锁 可重入锁 ReentrantLock A ReentrantLock is owned by the ...
- Java多线程15:Queue、BlockingQueue以及利用BlockingQueue实现生产者/消费者模型
Queue是什么 队列,是一种数据结构.除了优先级队列和LIFO队列外,队列都是以FIFO(先进先出)的方式对各个元素进行排序的.无论使用哪种排序方式,队列的头都是调用remove()或poll()移 ...
- Java多线程20:多线程下的其他组件之CountDownLatch、Semaphore、Exchanger
前言 在多线程环境下,JDK给开发者提供了许多的组件供用户使用(主要在java.util.concurrent下),使得用户不需要再去关心在具体场景下要如何写出同时兼顾线程安全性与高效率的代码.之前讲 ...
- Java多线程总结之线程安全队列Queue
在Java多线程应用中,队列的使用率很高,多数生产消费模型的首选数据结构就是队列.Java提供的线程安全的Queue可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是BlockingQueue,非 ...
- 使用goroutine+channel和java多线程+queue队列的方式开发各有什么优缺点?
我感觉很多项目使用java或者c的多线程库+线程安全的queue数据结构基本上可以实现goroutine+channel开发能达到的需求,所以请问一下为什么说golang更适合并发服务端的开发呢?使用 ...
随机推荐
- 【LeetCode】199. Binary Tree Right Side View 解题报告(Python)
[LeetCode]199. Binary Tree Right Side View 解题报告(Python) 标签: LeetCode 题目地址:https://leetcode.com/probl ...
- 【LeetCode】222. Count Complete Tree Nodes 解题报告(Python)
[LeetCode]222. Count Complete Tree Nodes 解题报告(Python) 标签(空格分隔): LeetCode 作者: 负雪明烛 id: fuxuemingzhu 个 ...
- Spurious Local Minima are Common in Two-Layer ReLU Neural Networks
目录 引 主要内容 定理1 推论1 引理1 引理2 Safran I, Shamir O. Spurious Local Minima are Common in Two-Layer ReLU Neu ...
- ☕【难点攻克技术系列】「海量数据计算系列」如何使用BitMap在海量数据中对相应的进行去重、查找和排序
BitMap(位图)的介绍 BitMap从字面的意思,很多人认为是位图,其实准确的来说,翻译成基于位的映射,其中数据库中有一种索引就叫做位图索引. 在具有性能优化的数据结构中,大家使用最多的就是has ...
- C++多线程并发---异步编程
线程同步主要是为了解决对共享数据的竞争访问问题,所以线程同步主要是对共享数据的访问同步化(按照既定的先后次序,一个访问需要阻塞等待前一个访问完成后才能开始).这篇文章谈到的异步编程主要是针对任务或线程 ...
- 单芯片替代PS176 DP转HDMI 4K60HZ DP转HDMI2.0转换芯片CS5263
PS176是一个显示端口 (DP)至HDMI 2.0视频接口转换器适用于需要视频协议转换的电缆适配器.电视接收器.监视器和其他应用.它将接受任何显示端口输入格式,包括DP 1.1a.dp1.2a.dp ...
- 编写Java程序,创建一个 Person 类,该类中有一个类成员变量 country、一个实例变量 name 和另一个实例变量 age。
返回本章节 返回作业目录 需求说明: 创建一个 Person 类,该类中有一个类成员变量 country.一个实例变量 name 和另一个实例变量 age. country 表示地区,name 表示姓 ...
- 编写Java程序,演练匿名内部类应用
返回本章节 返回作业目录 需求说明: 定义一个抽象类 Bird,创建使用匿名内部类的操作类Action. 实现思路: 定义抽象类Bird.在其中定义一个String类型的name属性,一个返回类型是i ...
- linux下备份mysql数据
一.业务场景 自己现在做的项目基本上已经开发完成,正式开始上线运行,主要包含两个子项目一个是小程序的后台,一个是后台管理系统. 正式开始运行一段时间后,基本上也没什么BUG了,整个项目都已经能够正常的 ...
- docker安装minio
目录 一.简介 二.docker安装 三.java中使用minio上传与下载 一.简介 MinIO 是在 GNU Affero 通用公共许可证 v3.0 下发布的高性能对象存储. 它是与 Amazon ...