我有一个多线程应用程序,其中共享列表具有写入,偶尔读取行为。
具体来说,许多线程会将数据转储到列表中,然后 - 稍后 - 另一个工作人员将获取快照以持久存储到数据存储区。
这类似于讨论over on this question。
在那里,提供了以下解决方案:
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;
}
}
但是,在这种情况下,(并且,正如我从我的问题here中学到的),在任何给定时间只有一个线程可以写入支持列表。
是否有办法允许对并入列表进行高并发写入,这些写入仅在makeSnapshot()
调用期间被锁定?
答案 0 :(得分:3)
synchronized(~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);
}
打印
The average time to add was 28 ns
这意味着如果您每秒创建3000万个条目,您将有一个线程平均访问该列表。如果您每秒创建6000万,那么您将遇到并发问题,但此时您可能会遇到更多的资源问题。
当争用率很高时,使用Lock.lock()和Lock.unlock()会更快。但是,我怀疑你的线程将花费大部分时间来构建要创建的对象,而不是等待添加对象。
答案 1 :(得分:2)
您可以使用ConcurrentDoublyLinkedList
。这里有一个很好的实现ConcurrentDoublyLinkedList。
只要你在制作快照时在列表中向前迭代,一切都应该很好。此实现始终保留前向链。后向链有时是不准确的。
答案 2 :(得分:1)
首先,您应该调查这是否真的太慢了。在快乐的情况下,ArrayList
的{{1}}是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;
}
请注意,虽然添加到此结构应该很好地处理高争用,但它有几个缺点。来自drain
的输出迭代很慢,它使用了相当多的内存(就像所有链接列表一样),并且你也得到相反的插入顺序。 (此外,它还没有真正经过测试或验证,实际上可能会在您的应用程序中出现问题。但是,使用来自intertubes上的一些随机数据的代码总是存在风险。)
答案 3 :(得分:0)
是的,有办法。它类似于ConcurrentHashMap的方式,如果你知道的话。
您应该为所有写入线程创建自己的数据结构,而不是使用多个独立列表。每个这样的列表都应该由它自己的锁保护。 .add()方法应该根据Thread.currentThread.id选择追加当前项目的列表(例如,只是id%listsCount)。这将为.add()提供良好的并发属性 - 充其量,listsCount线程将能够无争用地写入。
在makeSnapshot()上,您应该遍历所有列表,并为每个列表抓取它的锁定和复制内容。
这只是一个想法 - 有很多地方可以改进它。
答案 4 :(得分:0)
您可以使用ReadWriteLock允许多个线程并行地在后备列表上执行添加操作,但只有一个线程可以创建快照。正在准备快照时,所有其他添加和快照请求都将被暂停。
ReadWriteLock维护一对关联的锁,一个用于 只读操作和写操作。可以保持读锁定 同时由多个读者线程,只要没有 作家。写锁是独占的。
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;
}
}
编辑:
读写锁是如此命名的,因为它基于读者编写者的问题,而不是如何使用它。使用读写锁,我们可以让多个线程实现读锁,但只有一个线程专门实现写锁。在这种情况下,问题是相反的 - 我们希望多个线程写入(添加)并且只需要线程来读取(制作快照)。因此,我们希望多个线程使用读锁定,即使它们实际上是在变异。即使快照只读取,只有线程使用写锁定专门创建快照。独占意味着在制作快照期间,其他线程不能同时为其他添加或快照请求提供服务。
正如@PeterLawrey指出的那样,Concurrent队列将序列化写入aql虽然锁将尽可能少地使用。我们可以自由地使用任何其他并发数据结构,例如ConcurrentDoublyLinkedList。该队列仅用作示例。主要思想是使用读写锁。