是否有理由不始终使用AtomicInteger作为数据成员?

5
在像Android这样的多线程环境中,一个简单的int变量可能会被多个线程操作。在某些情况下,仍然有正当理由将int用作数据成员吗?
对于限定在方法范围内、只能由一个线程独占访问(因此修改始终在同一线程中进行)的局部变量来说,int在性能上是完全合理的。
但是作为数据成员,即使被封装在访问器中,它也可能遇到众所周知的并发交错修改问题。
因此,看起来“玩得安全”就可以在整个程序中使用AtomicInteger。但是这似乎非常低效。
你能举一个线程安全的int数据成员使用的例子吗?

什么整数操作不是原子的?(诚实的问题,这对我来说是一个新课题。) - djechlin
即使是 ++ 也不是原子性的。 - ef2011
1
“++” 不是原子操作,但读取和写入(分配)却是。换句话说,您永远不会得到交错的字节。问题在于“++”是“i = i + 1”的简写形式,并且在读取和分配之间可能会出现变异。 - pamphlet
5个回答

9

是否总是使用AtomicInteger作为数据成员有正当理由吗?

是的,有很好的理由总是使用AtomicInteger。由于volatile构造和其他用于设置/获取基础int值的Unsafe构造,AtomicInteger可能比本地int慢至少一个数量级(可能更多)。volatile意味着每次访问AtomicInteger时都会跨越内存屏障,这会导致在相关处理器上进行高速缓存刷新。

此外,仅仅因为您已将所有字段设置为AtomicInteger并不意味着在多个字段被访问时就可以保护您免受竞态条件的影响。没有任何替代方案可以取代对何时使用volatilesynchronizedAtomic*类做出明智决策。

例如,如果您想要以可靠的方式访问类中的两个字段,则可以执行以下操作:

synchronized (someObject) {
   someObject.count++;
   someObject.total += someObject.count;
}

如果这两个成员都使用AtomicInteger,那么您将访问volatile两次,因此跨越了2个内存屏障而不是1个。此外,与AtomicInteger内部的Unsafe操作相比,分配速度更快。另外,由于存在数据竞争条件(与上面的synchronized块相反),您可能无法获得正确的total值。

您能举个线程安全的int数据成员的使用例子吗?

除了将其标记为final之外,没有机制可以使int数据成员线程安全,除非将其标记为volatile或使用AtomicInteger。并没有一种神奇的方法可以使所有字段都具有线程安全性。如果有的话,线程编程将变得容易。挑战在于找到适当的位置来放置您的synchronized块,找到应该用volatile标记的正确字段,以及找到使用AtomicInteger和其他类的正确位置。

好的。那么,您如何保证具有int数据成员的类的线程安全性? - ef2011
你必须在类周围正确“同步”。请参阅http://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html。 - Gray
1
不是因为你只穿越了一个内存屏障,即使你访问多个字段。此外,它允许您在需要时承受同步性能损失。当您知道正在更新对象或需要确保具有一致的同步值时。 - Gray
1
"Volatile并不是AtomicInteger相关的唯一性能损失。最终读写操作将解析为不同且更慢的处理器指令。" - pamphlet
@Gray +1。我没有想到会有比这更好的答案。既然你提到了关键字synchronized,我再问一个相关的问题:我敢说我在某个地方看到使用注释@Synchronizedsynchronized更优秀。这是真的吗?你能在回答中提到吗? - ef2011
显示剩余4条评论

1

如果您有不可变的int,则可以在不确保同步的情况下进行计算,这样可以节省成本。一个例子是hashCode

int hash = 0;

public int hashCode(){
   if(hash == 0){
     hash = calculateHashCode(); //needs to always be the same for each Object
   }
   return hash;
}

这里明显的权衡是对于相同哈希值可能存在多次计算的可能性,但如果选择一个synchronized的hashCode,其影响可能更为严重。

尽管多余,但这在技术上是线程安全的。


这个不是线程安全的吗? 线程1执行hash == 0,线程2执行hash == 0,线程1计算并返回值,线程2计算并返回值。 相同的hashCode产生了不同的值。 - djechlin
除了重复计算之外,您会遭受哪些副作用? - John Vint
在多线程环境下,hashCode 可以在不同的调用中返回不同的值,但在非线程化环境中则不是这样。 - djechlin
我在我的回答中明确指出,calculateHashCode需要为每个对象返回相同的值。 - John Vint
@djechlin 你认为Java的String不是线程安全的吗?那个类执行非常相似的操作。 - John Vint
1
哦,我明白了,我把你的hashCode和calculateHashCode搞混了。 - djechlin

0

0

这取决于它与其他数据的使用方式。类封装了一种行为,因此一个变量通常没有其他变量就几乎没有意义。在这种情况下,保护属于一起的数据成员(或整个对象)可能比仅保护一个整数更好。如果您这样做,那么AtomicInteger是不必要的性能损失。

(*) 使用常见的线程安全机制:互斥锁、信号量、监视器等。


0

线程安全不仅仅是关于原子整数赋值,您需要仔细设计锁定模式以在代码中获得一致性。

如果您有两个具有公共数据成员BalanceAccount类,请考虑以下简单代码。

Account a;
...
int withdrawal = 100;
if(a.Balance >= withdrawal)
{
    // No atomic operations in the world can save you from another thread
    // withdrawing some balance here
    a.Balance -= withdrawal
}
else
{
   // Handle error
}

说实话,在现实生活中,仅有原子赋值是很少能解决我在并发问题上遇到的实际问题的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接