我对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;
}
}
修改过的servlet:
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);
}
}
我有一种强烈的感觉,这也不是线程安全的,因为我从增量器读取,并且可能得到陈旧的值(例如,如果引用已经被另一个线程替换)。如果它确实不是线程安全的,那么我想知道是否存在任何“功能”方式来处理这种计数器场景而没有锁定/同步。
所以我的问题是
虽然上面的示例代码是用Java编写的,但Scala中的回复当然也是受欢迎的
答案 0 :(得分: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);
}
}
答案 1 :(得分:4)
来自immutables的线程安全看起来更像是这样。
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的索引失败。或者您必须复制数据,如果您没有尽可能多地重复使用的集合(如果向量更长),则可能会非常昂贵。或者你必须在两个线程中同步大块,而不是只是原子地获取和/或获取你的数据。
您仍然需要以某种方式进行同步,并且您可能必须处理过期的数据副本。但是你不必须跟踪谁拥有哪个数据结构的副本,并且像疯了一样同步所有人,因为数据可能会从你的下方改变并在整个地方抛出异常。