Java 同步块进入和退出的内存屏障
Memory barriers on entry and exit of Java synchronized block
我在 SO 上找到了关于 Java 在退出期间刷新同步块内变量的工作副本的答案。类似地,它在进入同步部分期间从主内存同步所有变量一次。
但是,我对此有一些基本问题:
如果我访问同步部分中的大部分非易失性实例变量会怎样? JVM 会在进入块时自动将这些变量缓存到 CPU 寄存器中,然后在最终刷新它们之前进行所有必要的计算吗?
我有一个同步块如下:
带下划线的变量 _
例如_callStartsInLastSecondTracker
是我在这个关键部分大量访问的所有实例变量。
public CallCompletion startCall()
{
long currentTime;
Pending pending;
synchronized (_lock)
{
currentTime = _clock.currentTimeMillis();
_tracker.getStatsWithCurrentTime(currentTime);
_callStartCountTotal++;
_tracker._callStartCount++;
if (_callStartsInLastSecondTracker != null)
_callStartsInLastSecondTracker.addCall();
_concurrency++;
if (_concurrency > _tracker._concurrentMax)
{
_tracker._concurrentMax = _concurrency;
}
_lastStartTime = currentTime;
_sumOfOutstandingStartTimes += currentTime;
pending = checkForPending();
}
if (pending != null)
{
pending.deliver();
}
return new CallCompletionImpl(currentTime);
}
这是否意味着所有这些操作,例如+=, ++, >
等需要JVM反复与主存交互?如果是这样,我可以使用局部变量来缓存它们(最好是为基元分配堆栈)并执行操作,最后将它们分配回实例变量吗?这将有助于优化此块的性能吗?
我在其他地方也有这样的街区。在 运行 一个 JProfiler 上,观察到大部分时间线程处于 WAITING 状态并且吞吐量也很低。因此优化的必要性。
在此感谢任何帮助。
您正在访问单个对象上的成员。所以当CPU读取_lock成员时,需要先加载包含_lock成员的缓存行。因此,可能有相当多的成员变量将位于您的缓存中已经存在的同一缓存行上。
如果您确定它确实是一个问题,我会更担心同步块本身;这可能根本不是问题。例如Java使用了相当多的锁优化技术,如偏向锁、自适应自旋锁来降低锁的成本。
但如果是竞争锁,您可能希望通过尽可能多地移出锁来缩短锁的持续时间,甚至可能摆脱整个锁并切换到无锁方法.
我一刻都不会相信 JPofiler。
http://psy-lob-saw.blogspot.com/2016/02/why-most-sampling-java-profilers-are.html
所以可能是 JProfiler 把你带错了方向。
(我不太了解 Java,只是 Java 公开的底层锁定和内存排序概念。其中一些是基于关于 Java 有效,欢迎指正。)
我假设如果您重复访问它们,JVM 可以并将它们优化到寄存器中 在 相同的 synchronized
块中。
即打开 {
和关闭 }
是内存屏障(acquiring and releasing 锁),但在该块内应用正常规则。
非volatile
变量的正常规则就像在 C++ 中一样:JIT 编译器可以保留私有副本/临时文件并进行全面优化。关闭 }
使任何分配在将锁标记为已释放之前可见,因此运行同一同步块的任何其他线程都将看到这些更改。
但是如果你 read/write 这些变量 在 一个 synchronized(_lock)
块之外,而这个 synchronized
块正在执行,则没有顺序保证,只有Java 具有的任何原子性保证。只有 volatile
会强制 JVM 在每次访问时重新读取变量。
most of the time threads are in WAITING state and throughput is also very low. Hence the optimisation necessity.
你担心的事情并不能真正解释这一点。关键部分内的低效代码生成会使其花费更长的时间,这可能会导致额外的争用。
但是与让大多数线程最活跃 运行 相比,大多数线程在大多数时间被阻塞等待锁(或 I/O?)的影响还不够大当时。
@Kayaman 的评论很可能是正确的:这是一个设计问题,在一个大互斥体中做了太多工作。我在你的临界区内没有看到循环,但可能你调用的一些方法包含循环或者在其他方面很昂贵,并且当一个线程在其中时,没有其他线程可以进入这个 synchronized(_lock)
块。
从内存中 store/reload 理论上最坏情况下的减速(比如在反优化调试模式下编译 C)与将变量保存在寄存器中的情况类似 while (--shared_var >= 0) {}
,可能给出当前 x86 硬件的速度降低了 6 倍。 (dec eax
的 1 个周期延迟与内存目标 dec
的 5 个周期存储转发延迟相比)。但这仅当您在共享变量上循环,或者通过重复修改它来创建依赖链时才会这样。
请注意,具有存储转发功能的存储缓冲区仍将其保留在 CPU 核心本地,甚至无需提交到 L1d 缓存。
在更可能的情况下,代码只是多次读取 var,每次真正加载的反优化代码可以非常有效地在 L1d 缓存中命中所有这些加载。在 x86 上,您可能几乎不会注意到差异,现代 CPUs 具有 2/clock 负载吞吐量,并且使用内存源操作数有效处理 ALU 指令,例如 cmp eax, [rdi]
基本上与 cmp eax, edx
.
(CPUs 具有连贯的缓存,因此无需刷新或一直到 DRAM 以确保您 "see" 来自其他内核的数据;JVM 或 C 编译器只需要使确保加载或存储实际上发生在 asm 中,而不是优化到寄存器中。寄存器是线程私有的。)
但正如我所说,没有理由期望您的 JVM 在 synchronized
块内进行这种反优化。 但即使是,它也可能减速 25%。
我在 SO 上找到了关于 Java 在退出期间刷新同步块内变量的工作副本的答案。类似地,它在进入同步部分期间从主内存同步所有变量一次。
但是,我对此有一些基本问题:
如果我访问同步部分中的大部分非易失性实例变量会怎样? JVM 会在进入块时自动将这些变量缓存到 CPU 寄存器中,然后在最终刷新它们之前进行所有必要的计算吗?
我有一个同步块如下: 带下划线的变量
_
例如_callStartsInLastSecondTracker
是我在这个关键部分大量访问的所有实例变量。
public CallCompletion startCall()
{
long currentTime;
Pending pending;
synchronized (_lock)
{
currentTime = _clock.currentTimeMillis();
_tracker.getStatsWithCurrentTime(currentTime);
_callStartCountTotal++;
_tracker._callStartCount++;
if (_callStartsInLastSecondTracker != null)
_callStartsInLastSecondTracker.addCall();
_concurrency++;
if (_concurrency > _tracker._concurrentMax)
{
_tracker._concurrentMax = _concurrency;
}
_lastStartTime = currentTime;
_sumOfOutstandingStartTimes += currentTime;
pending = checkForPending();
}
if (pending != null)
{
pending.deliver();
}
return new CallCompletionImpl(currentTime);
}
这是否意味着所有这些操作,例如+=, ++, >
等需要JVM反复与主存交互?如果是这样,我可以使用局部变量来缓存它们(最好是为基元分配堆栈)并执行操作,最后将它们分配回实例变量吗?这将有助于优化此块的性能吗?
我在其他地方也有这样的街区。在 运行 一个 JProfiler 上,观察到大部分时间线程处于 WAITING 状态并且吞吐量也很低。因此优化的必要性。
在此感谢任何帮助。
您正在访问单个对象上的成员。所以当CPU读取_lock成员时,需要先加载包含_lock成员的缓存行。因此,可能有相当多的成员变量将位于您的缓存中已经存在的同一缓存行上。
如果您确定它确实是一个问题,我会更担心同步块本身;这可能根本不是问题。例如Java使用了相当多的锁优化技术,如偏向锁、自适应自旋锁来降低锁的成本。
但如果是竞争锁,您可能希望通过尽可能多地移出锁来缩短锁的持续时间,甚至可能摆脱整个锁并切换到无锁方法.
我一刻都不会相信 JPofiler。 http://psy-lob-saw.blogspot.com/2016/02/why-most-sampling-java-profilers-are.html 所以可能是 JProfiler 把你带错了方向。
(我不太了解 Java,只是 Java 公开的底层锁定和内存排序概念。其中一些是基于关于 Java 有效,欢迎指正。)
我假设如果您重复访问它们,JVM 可以并将它们优化到寄存器中 在 相同的 synchronized
块中。
即打开 {
和关闭 }
是内存屏障(acquiring and releasing 锁),但在该块内应用正常规则。
非volatile
变量的正常规则就像在 C++ 中一样:JIT 编译器可以保留私有副本/临时文件并进行全面优化。关闭 }
使任何分配在将锁标记为已释放之前可见,因此运行同一同步块的任何其他线程都将看到这些更改。
但是如果你 read/write 这些变量 在 一个 synchronized(_lock)
块之外,而这个 synchronized
块正在执行,则没有顺序保证,只有Java 具有的任何原子性保证。只有 volatile
会强制 JVM 在每次访问时重新读取变量。
most of the time threads are in WAITING state and throughput is also very low. Hence the optimisation necessity.
你担心的事情并不能真正解释这一点。关键部分内的低效代码生成会使其花费更长的时间,这可能会导致额外的争用。
但是与让大多数线程最活跃 运行 相比,大多数线程在大多数时间被阻塞等待锁(或 I/O?)的影响还不够大当时。
@Kayaman 的评论很可能是正确的:这是一个设计问题,在一个大互斥体中做了太多工作。我在你的临界区内没有看到循环,但可能你调用的一些方法包含循环或者在其他方面很昂贵,并且当一个线程在其中时,没有其他线程可以进入这个 synchronized(_lock)
块。
从内存中 store/reload 理论上最坏情况下的减速(比如在反优化调试模式下编译 C)与将变量保存在寄存器中的情况类似 while (--shared_var >= 0) {}
,可能给出当前 x86 硬件的速度降低了 6 倍。 (dec eax
的 1 个周期延迟与内存目标 dec
的 5 个周期存储转发延迟相比)。但这仅当您在共享变量上循环,或者通过重复修改它来创建依赖链时才会这样。
请注意,具有存储转发功能的存储缓冲区仍将其保留在 CPU 核心本地,甚至无需提交到 L1d 缓存。
在更可能的情况下,代码只是多次读取 var,每次真正加载的反优化代码可以非常有效地在 L1d 缓存中命中所有这些加载。在 x86 上,您可能几乎不会注意到差异,现代 CPUs 具有 2/clock 负载吞吐量,并且使用内存源操作数有效处理 ALU 指令,例如 cmp eax, [rdi]
基本上与 cmp eax, edx
.
(CPUs 具有连贯的缓存,因此无需刷新或一直到 DRAM 以确保您 "see" 来自其他内核的数据;JVM 或 C 编译器只需要使确保加载或存储实际上发生在 asm 中,而不是优化到寄存器中。寄存器是线程私有的。)
但正如我所说,没有理由期望您的 JVM 在 synchronized
块内进行这种反优化。 但即使是,它也可能减速 25%。