Java并发性—改进读取时拷贝集合
我有一个多线程应用程序,其中共享列表具有经常写入、偶尔读取的行为 具体地说,许多线程将把数据转储到列表中,然后——稍后——另一个工作线程将抓取快照以持久化到数据存储中 这与讨论类似 这里提供了以下解决方案:Java并发性—改进读取时拷贝集合,java,concurrency,Java,Concurrency,我有一个多线程应用程序,其中共享列表具有经常写入、偶尔读取的行为 具体地说,许多线程将把数据转储到列表中,然后——稍后——另一个工作线程将抓取快照以持久化到数据存储中 这与讨论类似 这里提供了以下解决方案: class CopyOnReadList<T> { private final List<T> items = new ArrayList<T>(); public void add(T item) { synchron
class CopyOnReadList<T> {
private final List<T> items = new ArrayList<T>();
public void add(T item) {
synchronized (items) {
// Add item while holding the lock.
items.add(item);
}
}
public List<T> makeSnapshot() {
List<T> copy = new ArrayList<T>();
synchronized (items) {
// Make a copy while holding the lock.
for (T t : items) copy.add(t);
}
return copy;
}
}
类CopyOnReadList{
私有最终列表项=新的ArrayList();
公共作废新增(T项){
已同步(项目){
//在按住锁的同时添加项目。
项目。添加(项目);
}
}
公共列表makeSnapshot(){
列表副本=新建ArrayList();
已同步(项目){
//拿着锁复制一份。
对于(T:项目)副本,添加(T);
}
返回副本;
}
}
然而,在这个场景中(正如我从问题中学到的),在任何给定的时间,只有一个线程可以写入支持列表
是否有一种方法允许对备份列表进行高并发写入,而这些写入仅在
makeSnapshot()
调用期间锁定?是的,有一种方法。如果您知道的话,它与ConcurrentHashMap的制作方式类似
您不应该从一个列表中为所有写入线程创建自己的数据结构,而应该使用几个独立的列表。每个这样的列表都应该有自己的锁来保护。add()方法应根据Thread.currentThread.id(例如,仅id%ListScont)为追加当前项选择列表。这将为.add()提供良好的并发属性——ListScont线程最多能够在没有争用的情况下进行写入
在makeSnapshot()上,您应该迭代所有列表,并为每个列表获取其锁定和复制内容
这只是一个想法,有很多地方可以改进 您可以使用
ConcurrentDoublyLinkedList
。这里有一个很好的实现
只要在创建快照时向前遍历列表,一切都会很好。此实现始终保留前向链。反向链有时是不准确的。同步(~20 ns)非常快,即使其他操作可以允许并发,它们也可能较慢
private final Lock lock = new ReentrantLock();
private List<T> items = new ArrayList<T>();
public void add(T item) {
lock.lock();
// trivial lock time.
try {
// Add item while holding the lock.
items.add(item);
} finally {
lock.unlock();
}
}
public List<T> makeSnapshot() {
List<T> copy = new ArrayList<T>(), ret;
lock.lock();
// trivial lock time.
try {
ret = items;
items = copy;
} finally {
lock.unlock();
}
return ret;
}
public static void main(String... args) {
long start = System.nanoTime();
Main<Integer> ints = new Main<>();
for (int j = 0; j < 100 * 1000; j++) {
for (int i = 0; i < 1000; i++)
ints.add(i);
ints.makeSnapshot();
}
long time = System.nanoTime() - start;
System.out.printf("The average time to add was %,d ns%n", time / 100 / 1000 / 1000);
}
这意味着,如果每秒创建3000万个条目,平均会有一个线程访问列表。如果每秒创建6000万个,则会出现并发问题,但此时可能会出现更多的资源分配问题
当争用率较高时,使用Lock.Lock()和Lock.unlock()会更快。但是,我怀疑您的线程将花费大部分时间构建要创建的对象,而不是等待添加对象。首先,您应该调查这是否真的太慢。添加到
ArrayList
s是O(1)
,因此如果列表具有适当的初始大小,CopyOnReadList.add
基本上只是一个边界检查和对数组插槽的分配,速度非常快。(请记住,CopyOnReadList
的编写是为了让人理解,而不是为了表现。)
如果您需要非锁定操作,您可以有如下功能:
class ConcurrentStack<T> {
private final AtomicReference<Node<T>> stack = new AtomicReference<>();
public void add(T value){
Node<T> tail, head;
do {
tail = stack.get();
head = new Node<>(value, tail);
} while (!stack.compareAndSet(tail, head));
}
public Node<T> drain(){
// Get all elements from the stack and reset it
return stack.getAndSet(null);
}
}
class Node<T> {
// getters, setters, constructors omitted
private final T value;
private final Node<T> tail;
}
类ConcurrentStack{
私有最终AtomicReference堆栈=新的AtomicReference();
公共无效添加(T值){
节尾、节头;
做{
tail=stack.get();
头部=新节点(值,尾部);
}而(!stack.compareAndSet(tail,head));
}
公共节点排水管(){
//从堆栈中获取所有元素并重置它
返回stack.getAndSet(null);
}
}
类节点{
//省略了getter、setter和构造函数
私人最终T值;
私有最终节点尾部;
}
注意,虽然添加到这个结构中应该可以很好地处理高争用,但它有几个缺点。drain
的输出迭代速度非常慢,它使用了相当多的内存(就像所有的链表一样),而且插入顺序也相反。(此外,它还没有经过真正的测试或验证,可能会影响到您的应用程序。但在Intertube上使用来自某个随机用户的代码总是有风险的。)您可以使用允许多个线程在备份列表上并行执行添加操作,但只有一个线程可以创建快照。在准备快照的过程中,所有其他添加和快照请求都处于挂起状态
ReadWriteLock维护一对关联锁,其中一个用于
只读操作和一个用于写入的操作。可以保持读取锁
由多个读卡器线程同时执行,只要没有
作家。写锁是独占的
类CopyOnReadList{
//可以自由使用任何并发数据结构,以ConcurrentLinkedQueue为例
私有最终ConcurrentLinkedQueue项=新ConcurrentLinkedQueue();
private final ReadWriteLock rwLock=new ReentrantReadWriteLock();
private final Lock shared=rwLock.readLock();
private final Lock exclusive=rwLock.writeLock();
公共作废新增(T项){
shared.lock();//多个线程可以获得读锁
//如果items.add()从不抛出异常,那么try finally就太过了
试一试{
//在按住锁的同时添加项目。
项目。添加(项目);
}最后{
shared.unlock();
}
}
公共列表makeSnapshot(){
List copy=new ArrayList();//最好使用具有初始大小的LinkedList或ArrayList构造函数
exclusive.lock();//只有一个线程可以获得写锁,所有读锁也被阻塞
//如果for循环从不抛出异常,那么try finally就太过了
试一试{
//拿着锁复制一份。
对于(T:项目){
副本。添加(t);
class ConcurrentStack<T> {
private final AtomicReference<Node<T>> stack = new AtomicReference<>();
public void add(T value){
Node<T> tail, head;
do {
tail = stack.get();
head = new Node<>(value, tail);
} while (!stack.compareAndSet(tail, head));
}
public Node<T> drain(){
// Get all elements from the stack and reset it
return stack.getAndSet(null);
}
}
class Node<T> {
// getters, setters, constructors omitted
private final T value;
private final Node<T> tail;
}
class CopyOnReadList<T> {
// free to use any concurrent data structure, ConcurrentLinkedQueue used as an example
private final ConcurrentLinkedQueue<T> items = new ConcurrentLinkedQueue<T>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock shared = rwLock.readLock();
private final Lock exclusive = rwLock.writeLock();
public void add(T item) {
shared.lock(); // multiple threads can attain the read lock
// try-finally is overkill if items.add() never throws exceptions
try {
// Add item while holding the lock.
items.add(item);
} finally {
shared.unlock();
}
}
public List<T> makeSnapshot() {
List<T> copy = new ArrayList<T>(); // probably better idea to use a LinkedList or the ArrayList constructor with initial size
exclusive.lock(); // only one thread can attain write lock, all read locks are also blocked
// try-finally is overkill if for loop never throws exceptions
try {
// Make a copy while holding the lock.
for (T t : items) {
copy.add(t);
}
} finally {
exclusive.unlock();
}
return copy;
}
}