在《 Java Concurrency in Practice》中,作者给出了以下非线程安全类的示例,该类在后台调用set
对象上的迭代器,如果涉及多个线程,则可能导致{{1} }。可以理解,一个线程正在修改集合,另一个线程对其进行迭代,并且-繁荣!
我不明白的是-作者是说可以通过用ConcurrentModificationException
包装HashSet
来解决此代码。这将如何解决问题?即使对所有方法的访问都将由同一个内在锁进行同步和保护,但是一旦获得迭代器对象,就无法保证一旦进行迭代,另一个线程就不会修改集合。
这本书的语录:
如果HiddenIterator将HashSet封装为SynchronizedSet,并封装了同步,则不会发生这种错误。
Collections.synchronizedSet()
如果有人可以帮助我理解这一点,我将不胜感激。
这是我认为可以解决的方式:
public class HiddenIterator {
//Solution :
//If HiddenIterator wrapped the HashSet with a synchronizedSet, encapsulating the synchronization,
//this sort of error would not occur.
//@GuardedBy("this")
private final Set<Integer> set = new HashSet<Integer>();
public synchronized void add(Integer i) {
set.add(i);
}
public synchronized void remove(Integer i) {
set.remove(i);
}
public void addTenThings() {
Random r = new Random();
for (int i = 0; i < 10; i++)
add(r.nextInt());
/*The string concatenation gets turned by the compiler into a call to StringBuilder.append(Object),
* which in turn invokes the collection's toString method - and the implementation of toString in
* the standard collections iterates the collection and calls toString on each element to
* produce a nicely formatted representation of the collection's contents. */
System.out.println("DEBUG: added ten elements to " + set);
}
}
或者,作为替代方案,可以为public class HiddenIterator {
private final Set<Integer> set = Collections.synchronizedSet(new HashSet<Integer>());
public void add(Integer i) {
set.add(i);
}
public void remove(Integer i) {
set.remove(i);
}
public void addTenThings() {
Random r = new Random();
for (int i = 0; i < 10; i++)
add(r.nextInt());
// synchronizing in set's intrinsic lock
synchronized(set) {
System.out.println("DEBUG: added ten elements to " + set);
}
}
}
和synchronized
方法保留add()
关键字。在这种情况下,我们将在remove()
上进行同步。另外,我们必须在this
中添加一个同步块(再次在this
上同步),该块将包含一个操作-使用隐式迭代进行记录:
addTenThings()
答案 0 :(得分:3)
Collections.synchronizedSet()
将集合包装在名为SynchronizedSet
的内部类的实例中,扩展了SynchronizedCollection
。现在让我们看一下SynchronizedCollection.toString()
的实现方式:
public String toString() {
synchronized (mutex) {return c.toString();}
}
基本上,迭代仍然存在,隐藏在c.toString()
调用中,但是它已经与此包装器集合的所有其他方法同步。因此,您无需在代码中重复同步。
答案 1 :(得分:0)
已编辑
正如Sergei Petunin正确指出的那样,toString()
的{{1}}方法在内部负责同步,因此在这种情况下无需手动同步。
一旦获得了迭代器对象,就无法保证一旦进行迭代,另一个线程就不会修改集合。
在外部迭代的情况下,例如使用 for-each 或Collections.synchronizedSet()
,将迭代封装在Iterator
块中的方法是必需的/足够的。
这就是Collections.synchronizedSet()
的JavaDoc指出
用户必须手动对返回的内容进行同步 遍历它或其任何
synchronize(set)
,subSet
或headSet
次浏览。tailSet
您的第二个版本具有类 SortedSet s = Collections.synchronizedSortedSet(new TreeSet());
...
synchronized (s) {
Iterator i = s.iterator(); // Must be in the synchronized block
while (i.hasNext())
foo(i.next());
}
和synchronized
的{{1}}添加/删除方法也可以工作,但是由于添加/删除将被同步两次,因此引入了不必要的开销。 HiddenIterator
和synchronize(this)
。
但是,在这种情况下,您可以省略HiddenIterator
,因为Collections.synchronizedSet(..)
会处理访问专用Collections.synchronizedSet(..)
字段时所需的所有同步。