【问题标题】:Is there any justification not to ALWAYS use AtomicInteger as data members?是否有任何理由不总是使用 AtomicInteger 作为数据成员?
【发布时间】:2012-06-20 18:01:51
【问题描述】:

在像 Android 这样的多线程环境中,一个简单的 int 变量可能由多个线程操作,在某些情况下仍然可以使用 int 作为数据成员吗?

int 作为局部变量,仅限于对其具有独占访问权限的方法的范围(因此修改它的开始和结束总是在同一个线程中),在性能方面非常有意义。

但是作为数据成员,即使被访问器包裹,也会遇到众所周知的并发交错修改问题。

所以看起来“安全起见”,人们可以全盘使用AtomicInteger。但这似乎非常低效。

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

【问题讨论】:

  • int 上的哪些操作不是原子的? (诚​​实的问题,这对我来说是一个新话题。)
  • @djechlin 即使++ 也不是原子的。
  • ++ 不是原子的,但读取是写入(分配)。换句话说,你永远不会得到交错的字节。 ++ 的问题在于它是 i = i + 1 的简写,并且在读取和分配之间可能存在突变。

标签: java multithreading performance atomic


【解决方案1】:

是否有任何理由不总是使用 AtomicInteger 作为数据成员?

是的,有充分的理由总是使用AtomicIntegerAtomicInteger 可以至少慢一个数量级(可能更多),因为 volatile 构造比本地 int 和其他 Unsafe 构造用于设置/获取底层 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 数据成员的类的线程安全?
  • 你必须在课堂上正确地synchronize。见docs.oracle.com/javase/tutorial/essential/concurrency/sync.html
  • 是的,但是锁定整个类不是效率更低吗?
  • 不,因为即使您访问多个字段,您也只会跨越一个内存屏障。此外,它还允许您在需要时获得同步性能。当您知道自己正在更新对象或需要确保具有一致的同步值时。
  • Volatile 并不是与 AtomicInteger 相关的唯一性能损失。最终读取和写入解析为不同且速度较慢的处理器指令。
【解决方案2】:

如果您有有效不可变的ints,您可以避免以计算为代价不确保同步。一个例子是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。
  • 除了重复计算之外,您还会产生什么副作用?
  • hashCode 可以在线程环境中的不同调用上返回不同的值,这在非线程环境中是不正确的。
  • 我在回答中明确表示calculateHashCode 需要为每个对象返回相同的值
  • 哦,我明白了,我把你的 hashCode 和 calculateHashCode 搞混了。
【解决方案3】:

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

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

【讨论】:

    【解决方案4】:

    线程安全不仅与原子 int 分配有关,您还需要仔细设计锁定模式以使代码保持一致。

    如果您有两个具有公共数据成员 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
    }
    

    说实话。在现实生活中,原子分配很少足以解决我现实生活中的并发问题。

    【讨论】:

      【解决方案5】:

      我猜 Google 看到了 OP 并更新了他们关于该主题的文档以更清晰:

      “AtomicInteger 用于原子递增计数器等应用程序中,不能用作 Integer 的替代品。”

      https://developer.android.com/reference/java/util/concurrent/atomic/AtomicInteger

      【讨论】:

        猜你喜欢
        • 2020-10-05
        • 1970-01-01
        • 2010-12-31
        • 2015-01-22
        • 2018-03-02
        • 1970-01-01
        • 2018-10-20
        • 1970-01-01
        • 2018-08-10
        相关资源
        最近更新 更多