多线程环境下需要 ArrayList 的防呆同步
Posted
技术标签:
【中文标题】多线程环境下需要 ArrayList 的防呆同步【英文标题】:Need Fool Proof Synchronization of ArrayList in A Multi-threaded Environment 【发布时间】:2012-10-10 11:26:05 【问题描述】:我已经在此工作了一周,正在研究如何正确同步 ArrayList。
简而言之,我的主要问题是我有一个“主”ArrayList 对象。不同的线程可能会进入并从此列表中添加/设置/删除。我需要确保当一个线程遍历 ArrayList 时,另一个线程不会改变它。
现在我已经阅读了很多关于“最佳”处理方式的文章:
使用 collections.synchronizedlist 使用 CopyOnWriteArrayList 将 synchronized() 块与 collections.synchronizedlist 结合使用 使用Vector(很多人反对)在每次迭代中使用同步块,添加/设置/删除块似乎是我想要的,但人们说有很多开销。
然后我开始玩 CopyOnWriteArrayList(我对我的主 ArrayList 进行的读取比写入要多)。这很适合阅读,但是很多论坛帖子忽略了提及的是不能从迭代器本身添加、设置或删除元素。例如(一个基本版本,但想象它在多线程环境中):
public static void main(String[] args)
class TestObject
private String s = "";
public TestObject(String s)
this.s = s;
public void setTheString(String s)
this.s = s;
public String getTheString()
return s;
CopyOnWriteArrayList<TestObject> list = new CopyOnWriteArrayList<TestObject>();
list.add(new TestObject("A"));
list.add(new TestObject("B"));
list.add(new TestObject("C"));
list.add(new TestObject("D"));
list.add(new TestObject("E"));
ListIterator<TestObject> litr = list.listIterator();
while(litr.hasNext())
TestObject test = litr.next();
if(test.getTheString().equals("B"))
litr.set(new TestObject("TEST"));
行“litr.set(new TestObject("TEST"));”会抛出一个
java.lang.UnsupportedOperationException
查看 Java 文档,有一行描述了这种行为:
“不支持对迭代器本身的元素更改操作(删除、设置和添加)。这些方法抛出 UnsupportedOperationException。”
那么你不得不使用
来修改该列表list.set(litr.previousIndex(), new TestObject("TEST"));
现在从技术上讲,这不应该带来同步问题吗?如果另一个线程同时进入,并且说,从“列表”中删除所有元素,迭代器将看不到这一点,它会将“列表”设置为给定索引并抛出异常,因为元素那个时候已经不存在了。如果您不能通过迭代器本身添加元素,我只是不明白 CopyOnWriteArrayList 的意义。
我是否错过了使用 CopyOnWriteArrayList 的要点?
我是否将每个最终不得不在同步块中添加/设置/删除元素的迭代器包装起来?
这必须是多线程的常见问题。我原以为有人会制作一个可以无忧无虑地处理所有这些的课程......
提前感谢您查看此内容!
【问题讨论】:
“万无一失”和“多线程”互不兼容。 我希望您已经阅读 Java 并发实践?如果不花时间学习,很难正确地做这些事情。 你真的需要List
吗?用ConcurrentHashMap
怎么样?
【参考方案1】:
正如您自己发现的那样,CopyOnWriteArrayList
无法在有人处理数据时进行完全安全的更改,尤其是不能在迭代列表时进行。
因为:无论您何时处理数据,都没有上下文来确保在其他人更改列表数据之前执行访问列表的完整语句块。
因此,您必须为执行整个数据访问块的所有访问操作(也用于读取!)提供任何上下文(如同步)。例如:
ArrayList<String> list = getList();
synchronized (list)
int index = list.indexOf("test");
// if the whole block would not be synchronized,
// the index could be invalid after an external change
list.remove(index);
或者对于迭代器:
synchronized (list)
for (String s : list)
System.out.println(s);
但是现在这种同步类型的大问题出现了:它很慢并且不允许多读访问。 因此,为数据访问构建自己的上下文会很有用。我将使用 ReentrantReadWriteLock 来允许多个读取访问并提高性能。 我对这个话题很感兴趣,我会为 ArrayList 做一个这样的上下文,并在我完成后附在此处。
2012 年 10 月 20 日 | 18:30 - 编辑: 我使用 ReentrantReadWriteLock 为安全的 ArrayList 创建了一个自己的访问上下文。首先,我将插入整个 SecureArrayList 类(大多数第一个操作只是覆盖和保护),然后我插入我的 Tester 类并解释用法。 我只是用一个线程测试了访问,而不是同时测试了很多,但我很确定它有效!如果没有,请告诉我。
安全数组列表:
package mydatastore.collections.concurrent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
/**
* @date 19.10.2012
* @author Thomas Jahoda
*
* uses ReentrantReadWriteLock
*/
public class SecureArrayList<E> extends ArrayList<E>
protected final ReentrantReadWriteLock rwLock;
protected final ReadLock readLock;
protected final WriteLock writeLock;
public SecureArrayList()
super();
this.rwLock = new ReentrantReadWriteLock();
readLock = rwLock.readLock();
writeLock = rwLock.writeLock();
// write operations
@Override
public boolean add(E e)
try
writeLock.lock();
return super.add(e);
finally
writeLock.unlock();
@Override
public void add(int index, E element)
try
writeLock.lock();
super.add(index, element);
finally
writeLock.unlock();
@Override
public boolean addAll(Collection<? extends E> c)
try
writeLock.lock();
return super.addAll(c);
finally
writeLock.unlock();
@Override
public boolean addAll(int index, Collection<? extends E> c)
try
writeLock.lock();
return super.addAll(index, c);
finally
writeLock.unlock();
@Override
public boolean remove(Object o)
try
writeLock.lock();
return super.remove(o);
finally
writeLock.unlock();
@Override
public E remove(int index)
try
writeLock.lock();
return super.remove(index);
finally
writeLock.unlock();
@Override
public boolean removeAll(Collection<?> c)
try
writeLock.lock();
return super.removeAll(c);
finally
writeLock.unlock();
@Override
protected void removeRange(int fromIndex, int toIndex)
try
writeLock.lock();
super.removeRange(fromIndex, toIndex);
finally
writeLock.unlock();
@Override
public E set(int index, E element)
try
writeLock.lock();
return super.set(index, element);
finally
writeLock.unlock();
@Override
public void clear()
try
writeLock.lock();
super.clear();
finally
writeLock.unlock();
@Override
public boolean retainAll(Collection<?> c)
try
writeLock.lock();
return super.retainAll(c);
finally
writeLock.unlock();
@Override
public void ensureCapacity(int minCapacity)
try
writeLock.lock();
super.ensureCapacity(minCapacity);
finally
writeLock.unlock();
@Override
public void trimToSize()
try
writeLock.lock();
super.trimToSize();
finally
writeLock.unlock();
//// now the read operations
@Override
public E get(int index)
try
readLock.lock();
return super.get(index);
finally
readLock.unlock();
@Override
public boolean contains(Object o)
try
readLock.lock();
return super.contains(o);
finally
readLock.unlock();
@Override
public boolean containsAll(Collection<?> c)
try
readLock.lock();
return super.containsAll(c);
finally
readLock.unlock();
@Override
public Object clone()
try
readLock.lock();
return super.clone();
finally
readLock.unlock();
@Override
public boolean equals(Object o)
try
readLock.lock();
return super.equals(o);
finally
readLock.unlock();
@Override
public int hashCode()
try
readLock.lock();
return super.hashCode();
finally
readLock.unlock();
@Override
public int indexOf(Object o)
try
readLock.lock();
return super.indexOf(o);
finally
readLock.unlock();
@Override
public Object[] toArray()
try
readLock.lock();
return super.toArray();
finally
readLock.unlock();
@Override
public boolean isEmpty() // not sure if have to override because the size is temporarly stored in every case...
// it could happen that the size is accessed when it just gets assigned a new value,
// and the thread is switched after assigning 16 bits or smth... i dunno
try
readLock.lock();
return super.isEmpty();
finally
readLock.unlock();
@Override
public int size()
try
readLock.lock();
return super.size();
finally
readLock.unlock();
@Override
public int lastIndexOf(Object o)
try
readLock.lock();
return super.lastIndexOf(o);
finally
readLock.unlock();
@Override
public List<E> subList(int fromIndex, int toIndex)
try
readLock.lock();
return super.subList(fromIndex, toIndex);
finally
readLock.unlock();
@Override
public <T> T[] toArray(T[] a)
try
readLock.lock();
return super.toArray(a);
finally
readLock.unlock();
@Override
public String toString()
try
readLock.lock();
return super.toString();
finally
readLock.unlock();
////// iterators
@Override
public Iterator<E> iterator()
return new SecureArrayListIterator();
@Override
public ListIterator<E> listIterator()
return new SecureArrayListListIterator(0);
@Override
public ListIterator<E> listIterator(int index)
return new SecureArrayListListIterator(index);
// deligated lock mechanisms
public void lockRead()
readLock.lock();
public void unlockRead()
readLock.unlock();
public void lockWrite()
writeLock.lock();
public void unlockWrite()
writeLock.unlock();
// getters
public ReadLock getReadLock()
return readLock;
/**
* The writeLock also has access to reading, so when holding write, the
* thread can also obtain the readLock. But while holding the readLock and
* attempting to lock write, it will result in a deadlock.
*
* @return
*/
public WriteLock getWriteLock()
return writeLock;
protected class SecureArrayListIterator implements Iterator<E>
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
@Override
public boolean hasNext()
return cursor != size();
@Override
public E next()
// checkForComodification();
int i = cursor;
if (i >= SecureArrayList.super.size())
throw new NoSuchElementException();
cursor = i + 1;
lastRet = i;
return SecureArrayList.super.get(lastRet);
@Override
public void remove()
if (!writeLock.isHeldByCurrentThread())
throw new IllegalMonitorStateException("when the iteration uses write operations,"
+ "the complete iteration loop must hold a monitor for the writeLock");
if (lastRet < 0)
throw new IllegalStateException("No element iterated over");
try
SecureArrayList.super.remove(lastRet);
cursor = lastRet;
lastRet = -1;
catch (IndexOutOfBoundsException ex)
throw new ConcurrentModificationException(); // impossibru, except for bugged child classes
// protected final void checkForComodification()
// if (modCount != expectedModCount)
// throw new IllegalMonitorStateException("The complete iteration must hold the read or write lock!");
//
//
/**
* An optimized version of AbstractList.ListItr
*/
protected class SecureArrayListListIterator extends SecureArrayListIterator implements ListIterator<E>
protected SecureArrayListListIterator(int index)
super();
cursor = index;
@Override
public boolean hasPrevious()
return cursor != 0;
@Override
public int nextIndex()
return cursor;
@Override
public int previousIndex()
return cursor - 1;
@Override
public E previous()
// checkForComodification();
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException("No element iterated over");
cursor = i;
lastRet = i;
return SecureArrayList.super.get(lastRet);
@Override
public void set(E e)
if (!writeLock.isHeldByCurrentThread())
throw new IllegalMonitorStateException("when the iteration uses write operations,"
+ "the complete iteration loop must hold a monitor for the writeLock");
if (lastRet < 0)
throw new IllegalStateException("No element iterated over");
// try
SecureArrayList.super.set(lastRet, e);
// catch (IndexOutOfBoundsException ex)
// throw new ConcurrentModificationException(); // impossibru, except for bugged child classes
// EDIT: or any failed direct editing while iterating over the list
//
@Override
public void add(E e)
if (!writeLock.isHeldByCurrentThread())
throw new IllegalMonitorStateException("when the iteration uses write operations,"
+ "the complete iteration loop must hold a monitor for the writeLock");
// try
int i = cursor;
SecureArrayList.super.add(i, e);
cursor = i + 1;
lastRet = -1;
// catch (IndexOutOfBoundsException ex)
// throw new ConcurrentModificationException(); // impossibru, except for bugged child classes
// // EDIT: or any failed direct editing while iterating over the list
//
SecureArrayList_Test:
package mydatastore.collections.concurrent;
import java.util.Iterator;
import java.util.ListIterator;
/**
* @date 19.10.2012
* @author Thomas Jahoda
*/
public class SecureArrayList_Test
private static SecureArrayList<String> statList = new SecureArrayList<>();
public static void main(String[] args)
accessExamples();
// mechanismTest_1();
// mechanismTest_2();
private static void accessExamples()
final SecureArrayList<String> list = getList();
//
try
list.lockWrite();
//
list.add("banana");
list.add("test");
finally
list.unlockWrite();
////// independent single statement reading or writing access
String val = list.get(0);
//// ---
////// reading only block (just some senseless unoptimized 'whatever' example)
int lastIndex = -1;
try
list.lockRead();
//
String search = "test";
if (list.contains(search))
lastIndex = list.lastIndexOf(search);
// !!! MIND !!!
// inserting writing operations here results in a DEADLOCK!!!
// ... which is just really, really awkward...
finally
list.unlockRead();
//// ---
////// writing block (can also contain reading operations!!)
try
list.lockWrite();
//
int index = list.indexOf("test");
if (index != -1)
String newVal = "banana";
list.add(index + 1, newVal);
finally
list.unlockWrite();
//// ---
////// iteration for reading only
System.out.println("First output: ");
try
list.lockRead();
//
for (Iterator<String> it = list.iterator(); it.hasNext();)
String string = it.next();
System.out.println(string);
// !!! MIND !!!
// inserting writing operations called directly on the list will result in a deadlock!
// inserting writing operations called on the iterator will result in an IllegalMonitorStateException!
finally
list.unlockRead();
System.out.println("------");
//// ---
////// iteration for writing and reading
try
list.lockWrite();
//
boolean firstAdd = true;
for (ListIterator<String> it = list.listIterator(); it.hasNext();)
int index = it.nextIndex();
String string = it.next();
switch (string)
case "banana":
it.remove();
break;
case "test":
if (firstAdd)
it.add("whatever");
firstAdd = false;
break;
if (index == 2)
list.set(index - 1, "pretty senseless data and operations but just to show "
+ "what's possible");
// !!! MIND !!!
// Only I implemented the iterators to enable direct list editing,
// other implementations normally throw a ConcurrentModificationException
finally
list.unlockWrite();
//// ---
System.out.println("Complete last output: ");
try
list.lockRead();
//
for (String string : list)
System.out.println(string);
finally
list.unlockRead();
System.out.println("------");
////// getting the last element
String lastElement = null;
try
list.lockRead();
int size = list.size();
lastElement = list.get(size - 1);
finally
list.unlockRead();
System.out.println("Last element: " + lastElement);
//// ---
private static void mechanismTest_1() // fus, roh
SecureArrayList<String> list = getList();
try
System.out.print("fus, ");
list.lockRead();
System.out.print("roh, ");
list.lockWrite();
System.out.println("dah!"); // never happens cos of deadlock
finally
// also never happens
System.out.println("dah?");
list.unlockRead();
list.unlockWrite();
private static void mechanismTest_2() // fus, roh, dah!
SecureArrayList<String> list = getList();
try
System.out.print("fus, ");
list.lockWrite();
System.out.print("roh, ");
list.lockRead();
System.out.println("dah!");
finally
list.unlockRead();
list.unlockWrite();
// successful execution
private static SecureArrayList<String> getList()
return statList;
编辑:我添加了几个测试用例来演示线程中的功能。上述课程完美运行,我现在在我的主项目 (Liam) 中使用它:
private static void threadedWriteLock()
final ThreadSafeArrayList<String> list = getList();
Thread threadOne;
Thread threadTwo;
final long lStartMS = System.currentTimeMillis();
list.add("String 1");
list.add("String 2");
System.out.println("******* basic write lock test *******");
threadOne = new Thread(new Runnable()
public void run()
try
list.lockWrite();
try
Thread.sleep(2000);
catch (InterruptedException e)
e.printStackTrace();
finally
list.unlockWrite();
);
threadTwo = new Thread(new Runnable()
public void run()
//give threadOne time to lock (just in case)
try
Thread.sleep(5);
catch (InterruptedException e)
e.printStackTrace();
System.out.println("Expect a wait....");
//if this "add" line is commented out, even the iterator read will be locked.
//So its not only locking on the add, but also the read which is correct.
list.add("String 3");
for (ListIterator<String> it = list.listIterator(); it.hasNext();)
System.out.println("String at index " + it.nextIndex() + ": " + it.next());
System.out.println("ThreadTwo completed in " + (System.currentTimeMillis() - lStartMS) + "ms");
);
threadOne.start();
threadTwo.start();
private static void threadedReadLock()
final ThreadSafeArrayList<String> list = getList();
Thread threadOne;
Thread threadTwo;
final long lStartMS = System.currentTimeMillis();
list.add("String 1");
list.add("String 2");
System.out.println("******* basic read lock test *******");
threadOne = new Thread(new Runnable()
public void run()
try
list.lockRead();
try
Thread.sleep(2000);
catch (InterruptedException e)
e.printStackTrace();
finally
list.unlockRead();
);
threadTwo = new Thread(new Runnable()
public void run()
//give threadOne time to lock (just in case)
try
Thread.sleep(5);
catch (InterruptedException e)
e.printStackTrace();
System.out.println("Expect a wait if adding, but not reading....");
//if this "add" line is commented out, the read will continue without holding up the thread
list.add("String 3");
for (ListIterator<String> it = list.listIterator(); it.hasNext();)
System.out.println("String at index " + it.nextIndex() + ": " + it.next());
System.out.println("ThreadTwo completed in " + (System.currentTimeMillis() - lStartMS) + "ms");
);
threadOne.start();
threadTwo.start();
【讨论】:
我的印象相同,我们必须将所有内容包装在同步块中。我期待你的意见!听起来它会比我想出的更优雅,呵呵。 @Liam 我上传了我的作品,请仔细阅读并在这里提出任何不清楚的问题。 (请不要忘记接受一个答案 [preferable mine :DD ]) 好吧,Thomas,我想你成功了。这是个非常完美的作品。我对您的示例进行了几个 Java 6 兼容性编辑,我想添加我的两个线程示例,以便我能够证明您的工作。看起来我可以编辑你的帖子,但我不想在没有先和你讨论的情况下编辑你的贡献。你想怎么做? 谢谢你,我很高兴看到你欣赏我的作品。 :D 是否可以标记编辑并添加您的 Java 6 版本(通常只有一些小的更改?)作为外部链接和您的示例作为外部链接和插入的 html 代码?如果你愿意,你可以在Skype上加我。我会在此评论下写下我的帐户名称,以便之后删除它。【参考方案2】:使用 CopyOnWriteArrayList,并且只在写操作上同步
CopyOnWriteArrayList<TestObject> list = ...
final Object writeLock = new Object();
void writeOpA()
synchronized(writeLock)
read/write list
void writeOpB()
synchronized(writeLock)
read/write list
因此没有两个写入会话会相互重叠。
读取不需要锁定。但是读取会话可能会看到一个变化的列表。如果我们希望读取会话查看列表的快照,请使用iterator()
,或通过toArray()
拍摄快照。
如果你自己做copy-on-write可能会更好
volatile Foo data = new Foo(); // ArrayList in your case
final Object writeLock = new Object();
void writeOpA()
synchronized(writeLock)
Foo clone = data.clone();
// read/write clone
data = clone;
void writeOpB()
// similar...
void readSession()
Foo snapshot = data;
// read snapshot
【讨论】:
顺便提一下:我们正在为每一种可能性寻找一个完全安全的解决方案。 只有当您只使用由不相关或单个语句组成的读取操作时,才可以将所有块与写入操作同步。但是,如果您处理相关阅读活动的块,您还必须在解决方案中同步这些块。 顺便说一句,克隆列表也会克隆包含的数据,而不仅仅是引用,这真的非常非常糟糕,所以我会很感激发表有关评论或删除此操作。 读取会话读取数据的快照;快照不会改变,因此整个读取会话的状态是一致的。 不是因为在两次读取操作之间(而第二次取决于第一次的结果),另一个线程可能会更改数据。第二次读取操作将使用新数据,而不是任何快照。尤其是在您仅使用 ArrayList 的第二个示例中。 好的,我刚刚看了一遍。你的第二个例子有点工作(除了我之前提到的你的克隆),但这是一个非常丑陋和缓慢的解决方案......只是说【参考方案3】:另一种方法是保护对列表的所有访问,但使用 ReadWriteLock 而不是同步块。
这允许以安全的方式同时读取,并且可以在多读少写的情况下大大提高性能。
【讨论】:
【参考方案4】:如果您要在迭代期间修改,是的,您必须使用选项 3。其他人都不会真正按照您的意愿行事。
更具体地说:考虑到您想要做的事情,您必须在迭代的长度内锁定整个列表,因为您可能会在中间修改它,这会破坏任何其他正在工作的迭代器同时在名单上。这意味着选项 3,因为 Java 语言不能只有“同步迭代器”——迭代器本身只能同步对 hasNext()
或 next()
的单个调用,但它不能在整个长度上同步迭代。
【讨论】:
在迭代期间进行修改的原因是我必须进行查找。在我的主数组中,有一个带有关联 ID 的对象。因此,如果我正在查找该 ID,我需要在该索引处修改该对象。我认为这是一种很常见的做法,不是吗? “因此,如果我正在查找该 ID,我需要在该索引处修改该对象。”这里有两个想法——a)为什么需要修改对象? b) 也许您应该修改对象的内容,而不是尝试替换列表中的对象。这些是我能想到的“常见做法”。 好吧,我正在使用一个 API,它可以让我返回全新的对象。我的意思是我可以复制每个变量而不是替换对象,但它仍然遇到同样的问题,即主数组列表中的项目被删除,现在我正在修改索引 (4) 或没有的东西更长的指向正确的对象。 :S 所以你是说我应该同步阻止一切?我只是想知道我是否在这里遗漏了一项关键的数据结构技术。我的意思是我这样做的方式对我来说似乎是合乎逻辑的,但也许有更好的...... 使用Collections.synchronizedList
,并围绕您进行的任何迭代执行synchronized(list)
——围绕整个迭代,而不仅仅是迭代的个别部分。以上是关于多线程环境下需要 ArrayList 的防呆同步的主要内容,如果未能解决你的问题,请参考以下文章