6

我对 Scala 和函数式编程比较陌生,我喜欢使用不可变对象可以避免许多线程安全陷阱的想法。一件事仍然困扰着我,它是用于教授线程安全的经典示例——共享计数器。

我想知道是否可以使用不可变对象和功能概念来实现线程安全计数器(本例中的请求计数器),并完全避免同步。

因此,这里首先是计数器的经典可变版本供参考(请原谅我的公共成员变量,只是为了示例的简洁)

可变的,非线程安全版本:

public class Servlet extends HttpServlet {

  public int requestCount = 0; 

  @Override
  public void service(ServletRequest req, ServletResponse res) throws ... {
    requestCount++; //thread unsafe
    super.service(req, res);  
  }
}

可变的,经典线程安全版本:(或者我希望......)

public class Servlet extends HttpServlet {

  public volatile int requestCount = 0;

  @Override
  public void service(ServletRequest req, ServletResponse res) throws ... {
    synchronized (this) {
      requestCount++;
    }
    super.service(req, res);  
  }
}

我想知道是否有一种方法可以使用不可变对象和 volatile 变量来实现线程安全而无需同步。

所以这是我天真的尝试。这个想法是为计数器提供一个不可变对象,并使用 volatile 变量替换对它的引用。感觉很腥,但值得一试。

持有者:

public class Incrementer {
  private final int value;
  public Incrementer(final int oldValue) {
    this.value = oldValue + 1;
  }

  public Incrementer() {
    this.value = 0;
  }

  public int getValue() {
    return value;
  }
}

修改后的小服务程序:

public class Servlet extends HttpServlet {

  public volatile Incrementer incrementer = new Incrementer();

  @Override
  public void service(ServletRequest req, ServletResponse res) throws ... {
    incrementer = new Incrementer(incrementer.getValue());
    super.service(req, res);
  }
}

我有一种强烈的感觉,这也不是线程安全的,因为我正在从增量器中读取数据,并且可能会得到一个陈旧的值(例如,如果引用已经被另一个线程替换)。如果它确实不是线程安全的,那么我想知道是否有任何“功能”方式来处理这种计数器场景而无需锁定/同步。

所以我的问题是

  1. 这个线程是否安全?
  2. 如果是,为什么?
  3. 如果没有,是否有任何方法可以在不同步的情况下实现这样的计数器?

尽管上面的示例代码是用 Java 编写的,当然也欢迎使用 Scala 进行回复

4

2 回答 2

13

这个线程是否安全?

不,除非您已经在同步块中创建了不可变对象,否则这不是线程安全的。在线程竞争条件下有可能创建损坏的不可变对象。

为了实现相同的功能,您可以使用AtomicInteger来避免显式同步。

public class Servlet extends HttpServlet {

  public AtomicInteger incrementer = new AtomicInteger (0);

  @Override
  public void service(ServletRequest req, ServletResponse res) throws ... {
    int newValue = incrementer.incrementAndGet();
    super.service(req, res);
  }
}
于 2013-06-07T05:20:11.790 回答
4

不可变的线程安全看起来更像这样。

val x = AtomicReference(Vector("salmon", "cod"))

// Thread 1
val y = x.get
println(y(y.length-1))

// Thread 2
x.getAndSet(x.get.tail)

如果你在可变地工作,你会很想让线程 2 改变一个可变列表,这可能会导致线程 1 的索引失败。或者您必须复制数据,如果您没有旨在尽可能多地重用的集合(并且如果向量更长),这可能会非常昂贵。或者您必须在两个线程中同步大块,而不仅仅是原子地获取和/或设置您的数据。

您仍然必须以某种方式进行同步,并且您可能必须处理过时的数据副本。但是您不必跟踪谁拥有哪个数据结构的副本并疯狂地同步每个人,因为这些数据可能会从您的手下发生变化并到处抛出异常。

于 2013-06-07T05:31:09.663 回答