完全免责声明:这不是一个真正的功课,但我这样标记它是因为它主要是一种自学练习,而不是实际的“工作”。
假设我想在Java中编写一个简单的线程安全模块计数器。也就是说,如果模M
为3,则计数器应无限循环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()
可能会返回错误的值,具体取决于模{{1} }。那就是:
M
也就是说,当模数为3且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
换行时,对next()
的两次调用将返回1, 1
。
tick
可能存在无序值的问题,例如:
next()
next()
next()
,返回 x tick.getAndIncrement()
,返回 y = x + 1(mod M) 这里,除了前面提到的包装问题, x 和 y 确实是这两个tick.getAndIncrement()
调用返回的两个正确值,但取决于如何指定了反制行为,可以说它们是乱序的。也就是说,我们现在有(Thread1,y)和(Thread2,x),但也许真的应该指定(Thread1,x)和(Thread2,y)是“正确的”行为。
因此,通过对单词的某些定义,next()
线程安全,但实际上不是 atomic 。
所以问题是:
AtomicModularCounter
的原子性来解决问题而不使用synchronized
?AtomicInteger
本身由模数控制范围,甚至没有机会包裹tick
?
Integer.MAX_VALUE
至少是一个小于M
的订单这是对无序“问题”的Integer.MAX_VALUE
类比。
List
add(first)
现在,如果我们已经成功更新了列表并添加了两个元素,但add(second)
位于second
之前,最后是“线程安全”吗?
如果那是“线程安全”,那么它不是什么?也就是说,如果我们在上面的场景中指定first
应该总是在first
之前,那么并发属性被称为什么? (我把它称为“原子性”,但我不确定这是否是正确的术语。)
对于它的价值,关于这个无序方面的second
行为是什么?
答案 0 :(得分:7)
据我所知,你只需要一个getAndIncrement()方法的变体
public final int getAndIncrement(int modulo) {
for (;;) {
int current = atomicInteger.get();
int next = (current + 1) % modulo;
if (atomicInteger.compareAndSet(current, next))
return current;
}
}
答案 1 :(得分:5)
我会说,除了包装,它没关系。当两个方法调用有效同时进行时,您无法保证首先会发生哪种方法。
代码仍然是原子的,因为无论哪个实际上先发生,它们都不会相互干扰。
基本上,如果您的代码试图依赖于同时调用的顺序,那么您已经有了竞争条件。即使在调用代码中,一个线程在另一个线程之前到达next()
调用的开始,您可以想象它在进入之前到达其时间片的末尾{ {1}}调用 - 允许第二个线程进入。
如果next()
电话有任何其他副作用 - 例如它打印出“从线程(线程ID)开始”和然后返回下一个值,然后它不是原子的;你的行为会有明显的差异。事实上,我认为你很好。
关于包装的一件事要考虑:如果你使用next()
,你可以在包装之前使计数器持续更长时间:)
编辑:我刚刚想到了在所有现实场景中避免包装问题的简洁方法:
当您使用AtomicLong
获取值时,请检查它是否大于此数字。如果是,进入“减少循环”,看起来像这样:
getAndIncrement()
基本上这说,“我们需要通过减少模数的一些倍数将值恢复到安全范围内”(这样它不会改变模值M的值)。它在一个紧密的循环中执行此操作,基本上确定了新值应该是什么,但只有在没有其他任何改变它们之间的值时才进行更改。
这可能会导致病态条件出现问题,因为您有无数个线程尝试增加该值,但我认为这实际上是可以的。
答案 2 :(得分:1)
关于原子性问题:我不相信Counter本身可能提供行为来保证你所暗示的语义。
我认为我们有一个线程正在做一些工作
A - get some stuff (for example receive a message)
B - prepare to call Counter
C - Enter Counter <=== counter code is now in control
D - Increment
E - return from Counter <==== just about to leave counter's control
F - application continues
您正在寻找的中介涉及在A。
处建立的“有效载荷”身份排序例如,两个线程各自读取一条消息 - 一个读取X,一个读取Y.您希望确保X获得第一个计数器增量,Y获得第二个,即使两个线程同时运行,并且可能已安排在1个或多个CPU之间进行仲裁。
因此,必须在所有步骤A-F中强加任何顺序,并由计数器外的某些并发控制强制执行。例如:
pre-A - Get a lock on Counter (or other lock)
A - get some stuff (for example receive a message)
B - prepare to call Counter
C - Enter Counter <=== counter code is now in control
D - Increment
E - return from Counter <==== just about to leave counter's control
F - application continues
post- F - release lock
现在我们以牺牲一些并行性为代价来保证;线程正在等待彼此。当严格排序是一项要求时,这往往会限制并发性;这是邮件系统中的常见问题。
关于列表问题。应该从接口保证的角度来看待线程安全性。绝对最低要求:面对来自多个线程的同时访问,List必须具有弹性。例如,我们可以想象一个不安全的列表可能会死锁或使列表错误链接,以便任何迭代都会循环。下一个要求是我们应该在两个线程同时访问时指定行为。有很多案例,这里有几个
a). Two threads attempt to add
b). One thread adds item with key "X", another attempts to delete the item with key "X"
C). One thread is iterating while a second thread is adding
假设实现在每种情况下都有明确定义的行为,它是线程安全的。有趣的问题是行为方便。
我们可以简单地在列表上进行同步,因此很容易为a和b提供易于理解的行为。然而,这在并行性方面是有代价的。而且我认为没有任何价值可以做到这一点,因为你仍然需要在更高级别进行同步以获得有用的语义。所以我会有一个接口规范说“按任何顺序添加”。
至于迭代 - 这是一个难题,看看Java集合承诺的内容:不是很多!
讨论Java集合的This article可能很有趣。
答案 3 :(得分:1)
Atomic (据我所知)指的是中间状态不能从外部观察到的事实。 atomicInteger.incrementAndGet()
是原子的,而return this.intField++;
不是,在前者中,你不能观察到整数已经递增但尚未返回的状态。
对于线程安全,Java Concurrency in Practice的作者在他们的书中提供了一个定义:
如果一个类行为,则它是线程安全的 从多个访问时正确 线程,无论调度如何 或交错执行 运行时的那些线程 环境,没有额外的 同步或其他协调 在调用代码方面。
(我个人意见如下)
现在,如果我们有列表 成功更新了两个元素 补充说,但第二次来到第一次, 最后,是“线程 安全“?
如果thread1在thread2之前输入了互斥对象的条目集(如果是Collections.synchronizedList()列表本身),则保证first
位于列表中的second
之前。更新后。这是因为synchronized
关键字使用公平锁定。坐在队列前面的人首先要做的事情。公平锁可能非常昂贵,你也可以在java中使用不公平的锁(通过使用java.util.concurrent实用程序)。如果你这样做,那就没有这样的保证。
但是,java平台不是实时计算平台,因此您无法预测一段代码需要运行多长时间。这意味着,如果您希望在first
之前second
,则需要在java中明确确保这一点。通过“控制呼叫的时间”来确保这一点是不可能的。
现在,这里的线程安全或不安全是什么?我认为这完全取决于需要做什么。如果你只是需要避免列表被破坏而且first
是第一个或second
在列表中的第一位并不重要,为了使应用程序正确运行,那么只是避免损坏就足够了建立线程安全。如果没有,则不然。
因此,我认为在没有我们试图实现的特定功能的情况下,无法定义线程安全性。
着名的String.hashCode()
不使用java中提供的任何特定“同步机制”,但它仍然是线程安全的,因为可以在他们自己的应用程序中安全地使用它。而不用担心同步等。
着名的String.hashCode()技巧:
int hash = 0;
int hashCode(){
int hash = this.hash;
if(hash==0){
hash = this.hash = calcHash();
}
return hash;
}