我在线程方面没有太多经验,但我用 Atomics 编写了一个漂亮的非阻塞顺序 Id 生成器......它让我在测试中获得了非常显着的性能提升。现在我想知道为什么有人会使用同步,因为它慢得多......现代 64 位多核硬件是否有原因?其他人现在正在向我询问有关 Atomics 的问题。我希望能够告诉他们永远不要使用该关键字,除非他们正在部署到古老的硬件。
5 回答
因为您不能仅使用原子来执行多个操作(好吧,从技术上讲,您可以,因为您可以使用原子来实现“锁定”,但我认为这不是重点)。你也不能使用原子来做阻塞等待(你可以做一个忙碌的等待,但这几乎总是一个坏主意)。
这是 OP 的一个练习:编写一个程序,该程序使用多个线程将带时间戳的日志消息写入同一个文件,其中消息必须按时间戳顺序显示在文件中。仅使用原子来实现这一点,但无需重新发明 ReentrantLock/synchronized。
现在我想知道为什么有人会使用同步,因为它慢得多......
也许是因为速度不是一切。
事实上,如果您客观地看待在实际应用程序中使用“漂亮”生成器的整体性能优势,我怀疑您会发现它太小了,无所谓。分析应用程序会告诉你这一点。
然后是你的基准测试是否真的有效的问题;即,您是否正在执行避免误导性影响(如 JVM 预热异常、优化异常等)所需的事情。以及您是否(实际上)衡量有争议和无争议的案件。
是否存在 Java 同步关键字优于 Atomics 的可行用例?
这很容易。
需要独占访问一个或多个数据结构以执行一系列操作或本质上不是线程安全的操作的任何情况。这些AtomicXxx
类型不支持这种事情。
我希望能够告诉他们永远不要使用该关键字,除非他们正在部署到古老的硬件。
不要告诉他们。这是不正确的。事实上,如果您是 Java 线程的新手,我建议您在开始向人们提供建议之前阅读 Goetz 等人的“Java Concurrency in Practice”。
取决于你在做什么——如果你只需要原子提供给你的功能,那么是的,你不需要自己做同样的工作(使用 synchronized 关键字)。然而,许多多线程应用程序做的事情比仅仅需要原子地增加一个数字要复杂得多。
例如,您可能需要完成一个工作单元,在其中修改内存中的多个数据结构,并且所有这些都必须在不受干扰的情况下发生 - 您可以为此使用同步函数或块。
据我了解, synchronized 关键字实际上是一个中等重量级的递归(重入)锁。
例如,以下(可怕的)代码不会死锁:
public static Object lock = new Object();
int recurCount = 0;
public int fLocktorial(int n) {
synchronized(lock) {
recurCount++;
if (n <= 0)
return 1;
return n * fLocktorial(n-1);
}
}
实现这一点需要在锁中维护额外的状态和逻辑,这可能会导致其性能低于原子和其他原语。但是,它确实允许您在函数内部任意获取锁,而不必担心调用者是否已经获得了锁。在这种情况下,简单地使用 Atomics 实现的锁会死锁。
此外,如果在锁内完成大量处理,同步可能会产生性能优势。获得锁只会影响一次性能,而原子强制每个操作进行核心同步。这会刷新处理器管道,影响性能。
从概念上讲,受锁保护的临界区将状态从一种有效状态转换为另一种有效状态。
int x, y; // invariant: x==y
void inc()
synchronized(lock)
x++;
y++;
void dec()
...
我们可以将状态封装在一个对象中,并原子地更改该对象。
class State
final int x, y;
State(int x, y) { ... }
volatile State state;
void inc()
do
State s = state;
State s2 = new State(s.x+1, s.y+1);
while( ! compareAndSet( "state", s, s2) ) // use Unsafe or something
那个更好吗?不必要。它很吸引人,当状态变得更复杂时它会更简单;但在大多数情况下它可能会更慢。