在 Java 中编写线程安全的模块化计数器
完整的免责声明:这不是真正的家庭作业,但我将其标记为这样,因为它主要是一种自学练习,而不是实际的“工作”。
假设我想用Java编写一个简单的线程安全模块化计数器。也就是说,如果模数为3,则计数器应无限期循环。M
0, 1, 2, 0, 1, 2, …
这里有一个尝试:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicModularCounter {
private final AtomicInteger tick = new AtomicInteger();
private final int M;
public AtomicModularCounter(int M) {
this.M = M;
}
public int next() {
return modulo(tick.getAndIncrement(), M);
}
private final static int modulo(int v, int M) {
return ((v % M) + M) % M;
}
}
我对这段代码的分析(可能是错误的)是,由于它使用AtomicInteger
,因此即使没有任何显式方法/块,它也是相当线程安全的。synchronized
不幸的是,“算法”本身并不完全“有效”,因为当环绕时,可能会根据模返回错误的值。那是:tick
Integer.MAX_VALUE
next()
M
System.out.println(Integer.MAX_VALUE + 1 == Integer.MIN_VALUE); // true
System.out.println(modulo(Integer.MAX_VALUE, 3)); // 1
System.out.println(modulo(Integer.MIN_VALUE, 3)); // 1
也就是说,当模数为 3 并环绕时,将返回两个调用 to。next()
1, 1
tick
获取无序值也可能存在问题,例如:next()
-
线程 1 调用
next()
-
线程 2 调用
next()
-
线程 2 完成,返回 x
tick.getAndIncrement()
-
线程 1 完成,返回 y = x+1 (mod M)
tick.getAndIncrement()
在这里,除了前面提到的包装问题之外,x 和 y 确实是为这两个调用返回的两个正确值,但根据计数器行为的指定方式,可以认为它们是无序的。也就是说,我们现在有(Thread1,y)和(Thread2,x),但也许应该指定(Thread1,x)和(Thread2,y)是“正确”的行为。next()
因此,根据单词的某些定义,是线程安全的,但实际上不是原子的。AtomicModularCounter
所以问题是:
- 我的分析是否正确?如果没有,请指出任何错误。
- 我上面的最后一句话是否使用了正确的术语?如果不是,正确的说法是什么?
- 如果上面提到的问题是真实的,那么您将如何解决它?
- 你能在不使用的情况下修复它吗?的原子性?
synchronized
AtomicInteger
- 你会怎么写它,以至于它本身是由模控制的,甚至从来没有机会包络?
tick
Integer.MAX_VALUE
- 我们可以假设至少比必要时小一个订单
M
Integer.MAX_VALUE
- 我们可以假设至少比必要时小一个订单
附录
这是无序“问题”的类比。List
-
线程 1 调用
add(first)
-
线程 2 调用
add(second)
现在,如果我们成功地更新了列表,添加了两个元素,但排在前面,即最后,那么“线程安全”吗?second
first
如果这是“线程安全”,那么它不是什么?也就是说,如果我们指定在上面的场景中,应该总是在 前面,那么并发属性叫什么?(我称之为“原子性”,但我不确定这是否是正确的术语)。first
second
就其价值而言,这种无序方面的行为是什么?Collections.synchronizedList