【问题标题】:Java Concurrency In Practice. Listing 5.6Java 并发实践。清单 5.6
【发布时间】:2019-03-07 13:20:33
【问题描述】:

在 Java 并发实践中作者给出了以下非线程安全类的示例,该类在后台调用 set 对象上的迭代器,如果涉及多个线程,这可能会导致 ConcurrentModificationException。这是可以理解的,一个线程正在修改集合,另一个线程正在对其进行迭代,然后 - 砰!

我不明白,-作者说可以通过用Collections.synchronizedSet() 包装HashSet 来修复此代码。这将如何解决问题?即使对所有方法的访问将被同一个内在锁同步和保护,但一旦获得迭代器对象,不能保证一旦进行迭代,其他线程就不会修改集合。

书中引用:

如果 HiddenIterator 用 synchronizedSet 包装了 HashSet,封装了同步,就不会出现这种错误。

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);
        }
    }
}

或者,作为替代方案,可以为add()remove() 方法保留synchronized 关键字。在这种情况下,我们将在 this 上进行同步。此外,我们必须将一个同步块(再次在 this 上同步)添加到 addTenThings(),其中将包含一个操作 - 带有隐式迭代的日志记录:

public class HiddenIterator {

    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());
        synchronized(this) {
            System.out.println("DEBUG: added ten elements to " + set);
        }
    }
}

【问题讨论】:

  • 如果重复没有回答您的问题,请告诉我,我们会重新打开。
  • 它没有回答这个问题。它不是重复的。请仔细阅读我的询问内容。

标签: java concurrency


【解决方案1】:

Collections.synchronizedSet() 将集合包装在一个名为SynchronizedSet 的内部类的实例中,扩展SynchronizedCollection。现在让我们看看SynchronizedCollection.toString()是如何实现的:

public String toString() {
    synchronized (mutex) {return c.toString();}
}

基本上迭代仍然存在,隐藏在c.toString() 调用中,但它已经与此包装器集合的所有其他方法同步。所以你不需要在你的代码中重复同步。

【讨论】:

  • 啊哈!因此,在同步集合中,toString() 足够聪明,可以像其他方法一样在同一个锁上同步。好的,我们不需要额外的同步是有道理的。感谢您的解释。不幸的是,作者没有给出这个解释。
【解决方案2】:

已编辑

synchronizedSet()::toString()

正如 Sergei Petunin 正确指出的那样,Collections.synchronizedSet()toString() 方法在内部负责同步,因此在这种情况下不需要手动同步。

synchronizedSet() 的外部迭代

一旦获得了迭代器对象,就不能保证一旦进行迭代,其他线程就不会修改集合。

在外部迭代的情况下,例如使用 for-eachIterator,将迭代封装在 synchronize(set) 块中的方法是必需/足够的。

这就是Collections.synchronizedSet() 的 JavaDoc 声明的原因,即

用户必须手动同步返回的 遍历它或它的任何subSetheadSettailSet 浏览量。

  SortedSet s = Collections.synchronizedSortedSet(new TreeSet());
      ...   
  synchronized (s) {
      Iterator i = s.iterator(); // Must be in the synchronized block
      while (i.hasNext())
          foo(i.next());   
  }

手动同步

您的第二个版本使用 synchronizedHiddenIteratorsynchronize(this) 的添加/删除方法也可以,但它会引入不必要的开销,因为添加/删除将同步两次(通过 HiddenIterator 和 @987654335 @。

但是,在这种情况下,您可以省略 Collections.synchronizedSet(..),因为 HiddenIterator 负责访问私有 Set 字段时所需的所有同步。

【讨论】:

  • 感谢您的评论。我试图理解作者的意思是“如果 HiddenIterator 用 synchronizedSet 包装了 HashSet,封装了同步,就不会发生这种错误。”
  • 我已经修改了解决方案#2。
  • 我认为 Sergei Petunin 的回答最有意义。不需要额外的同步,因为同步集在 toString() 方法中进行内部同步。同步被封装。
  • 更新了我对这一事实的回答。
猜你喜欢
  • 2018-11-06
  • 2011-03-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-04-12
  • 1970-01-01
相关资源
最近更新 更多