Java 是否曾重新偏向单个锁
Does Java ever rebias an individual lock
这个问题是关于偏向锁定的启发式 Java 用途之一。下一段是给未来的读者的;我怀疑任何能回答这个问题的人都可以安全地跳过它。
据我了解,曾几何时,人们注意到 Java 有很多 类 是线程安全的,但它们的实例往往只被一个线程使用,所以Sun 引入了偏向锁定来利用这一点。问题是,如果你 "guess wrong" 并尝试从两个线程中偏向需要使用的锁,即使没有争用,也需要撤消偏向 ("revoked"),就是这样JVM 努力避免它的代价很高,即使这意味着有时会错过偏向锁定本可以成为净赢的情况。
我也知道有时 JVM 会决定执行 "bulk" 重新偏置,并将特定类型的许多所有锁迁移到不同的线程。这个问题与此无关。为了这个问题的目的,假设我只有两个线程和一个锁。 (实际情况更复杂,涉及线程池,但我们暂时忽略它。真的,假装我没有提到它。)进一步假设线程 A 运行一个类似于 "sleep for a few seconds, increment integer under lock, repeat" 的无限循环。 (它并不是真的那么无用,但这应该足以说明要点。)同时,线程 B 运行一个类似的循环,但睡眠时间是几个小时而不是几秒钟。进一步假设调度程序是神奇的并且保证永远不会有任何争用。 (先发制人的挑剔:如果那是真的,我们可以只是一个 volatile。这只是一个例子。在这里和我一起工作。)这个假设是不现实的,但我试图一次只担心一件事。
现在,假设我们关心线程 A 唤醒和成功递增其整数之间的平均延迟。据我了解,JVM 最初会将锁偏向 A,然后在线程 B 第一次唤醒时撤销偏向。
我的问题是:JVM 是否会意识到它最初的猜测基本上是正确的,从而再次将锁重新偏向线程 A?
理论上是可行的,但需要一些额外的条件和特殊的 JVM 设置。
理论
对于某些对象,偏向锁显然是无利可图的,例如涉及两个或多个线程的生产者-消费者队列。这样的对象必然有锁争用。另一方面有
将一组对象重新偏向另一个线程的能力是有利可图的,特别是当一个线程分配多个对象时
对象并对每个对象执行初始同步操作,
但是另一个线程对它们执行后续工作,例如基于 spring 的应用程序。
JVM 尝试涵盖这两种用例并同时支持重新设置和撤销。详见Eliminating Synchronization-Related Atomic Operations with Biased Locking and Bulk Rebiasing
也就是说你的理解:
As far as I understand, the JVM would initially bias the lock towards
A, and then revoke the bias the first time that thread B woke up.
并不总是正确的,即 JVM 足够聪明,可以检测到无竞争的同步并将锁重新偏向另一个线程。
以下是一些实施说明:
- HotSpot 仅支持批量重新偏置以分摊每个对象偏置撤销的成本,同时保留优化的好处。
- bulk rebias 和 bulk revoke 共享一个 safepoint/operation 名称 - RevokeBias。这非常令人困惑,需要进一步调查。
- 当且仅当撤销数量大于
BiasedLockingBulkRebiasThreshold
且小于 BiasedLockingBulkRevokeThreshold
并且最近一次撤销不迟于BiasedLockingDecayTime
,其中所有转义变量都是 JVM 属性。请仔细阅读this code.
- 您可以使用 属性
-XX:+PrintSafepointStatistics
追踪安全点事件。最有趣的是 EnableBiasedLocking、RevokeBias 和 BulkRevokeBias
-XX:+TraceBiasedLocking
生成一个有趣的日志,其中包含有关 JVM 决策的详细描述。
练习
这是我的复制器,其中一个线程(实际上是主线程)分配监视器对象并对其执行初始同步操作,然后另一个线程执行后续工作:
package samples;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static java.lang.System.out;
public class BiasLocking {
private static final Unsafe U;
private static final long OFFSET = 0L;
static {
try {
Field unsafe = Unsafe.class.getDeclaredField("theUnsafe");
unsafe.setAccessible(true);
U = (Unsafe) unsafe.get(null);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
public static void main(String[] args) throws Exception {
ExecutorService thread = Executors.newSingleThreadExecutor();
for (int i = 0; i < 15; i++) {
final Monitor a = new Monitor();
synchronized (a) {
out.println("Main thread \t\t" + printHeader(a));
}
thread.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
synchronized (a) {
out.println("Work thread \t\t" + printHeader(a));
}
return null;
}
}).get();
}
thread.shutdown();
}
private static String printHeader(Object a) {
int word = U.getInt(a, OFFSET);
return Integer.toHexString(word);
}
private static class Monitor {
// mutex object
}
}
为了重现我的结果,请使用以下 JVM 参数:
- -XX:+UseBiasedLocking - 不需要,默认使用
- -XX:BiasedLockingStartupDelay=0 - 默认延迟 4s
- -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 - 启用安全点日志
- -XX:+TraceBiasedLocking - 非常有用的日志
- -XX:BiasedLockingBulkRebiasThreshold=1 - 在我的示例中减少迭代次数
在测试过程中,JVM 决定重新设置监视器而不是撤销
Main thread 0x7f5af4008805 <-- this is object's header word contains thread id
* Beginning bulk revocation (kind == rebias) because of object 0x00000000d75631d0 , mark 0x00007f5af4008805 , type samples.BiasLocking$Monitor
* Ending bulk revocation
Rebiased object toward thread 0x00007f5af415d800
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
0.316: BulkRevokeBias [ 10 0 0 ] [ 0 0 0 0 0 ] 0
Work thread 0x7f5af415d905 <-- this is object's header word contains thread id => biased
下一步是将锁重新偏向主线程。这部分是最难的,因为我们要打下面的heuristics:
Klass* k = o->klass();
jlong cur_time = os::javaTimeMillis();
jlong last_bulk_revocation_time = k->last_biased_lock_bulk_revocation_time();
int revocation_count = k->biased_lock_revocation_count();
if ((revocation_count >= BiasedLockingBulkRebiasThreshold) &&
(revocation_count < BiasedLockingBulkRevokeThreshold) &&
(last_bulk_revocation_time != 0) &&
(cur_time - last_bulk_revocation_time >= BiasedLockingDecayTime)) {
// This is the first revocation we've seen in a while of an
// object of this type since the last time we performed a bulk
// rebiasing operation. The application is allocating objects in
// bulk which are biased toward a thread and then handing them
// off to another thread. We can cope with this allocation
// pattern via the bulk rebiasing mechanism so we reset the
// klass's revocation count rather than allow it to increase
// monotonically. If we see the need to perform another bulk
// rebias operation later, we will, and if subsequently we see
// many more revocation operations in a short period of time we
// will completely disable biasing for this type.
k->set_biased_lock_revocation_count(0);
revocation_count = 0;
}
您可以使用 JVM 参数和我的示例来试探一下,但请记住,这非常困难,有时需要 JVM 调试。
这个问题是关于偏向锁定的启发式 Java 用途之一。下一段是给未来的读者的;我怀疑任何能回答这个问题的人都可以安全地跳过它。
据我了解,曾几何时,人们注意到 Java 有很多 类 是线程安全的,但它们的实例往往只被一个线程使用,所以Sun 引入了偏向锁定来利用这一点。问题是,如果你 "guess wrong" 并尝试从两个线程中偏向需要使用的锁,即使没有争用,也需要撤消偏向 ("revoked"),就是这样JVM 努力避免它的代价很高,即使这意味着有时会错过偏向锁定本可以成为净赢的情况。
我也知道有时 JVM 会决定执行 "bulk" 重新偏置,并将特定类型的许多所有锁迁移到不同的线程。这个问题与此无关。为了这个问题的目的,假设我只有两个线程和一个锁。 (实际情况更复杂,涉及线程池,但我们暂时忽略它。真的,假装我没有提到它。)进一步假设线程 A 运行一个类似于 "sleep for a few seconds, increment integer under lock, repeat" 的无限循环。 (它并不是真的那么无用,但这应该足以说明要点。)同时,线程 B 运行一个类似的循环,但睡眠时间是几个小时而不是几秒钟。进一步假设调度程序是神奇的并且保证永远不会有任何争用。 (先发制人的挑剔:如果那是真的,我们可以只是一个 volatile。这只是一个例子。在这里和我一起工作。)这个假设是不现实的,但我试图一次只担心一件事。
现在,假设我们关心线程 A 唤醒和成功递增其整数之间的平均延迟。据我了解,JVM 最初会将锁偏向 A,然后在线程 B 第一次唤醒时撤销偏向。
我的问题是:JVM 是否会意识到它最初的猜测基本上是正确的,从而再次将锁重新偏向线程 A?
理论上是可行的,但需要一些额外的条件和特殊的 JVM 设置。
理论
对于某些对象,偏向锁显然是无利可图的,例如涉及两个或多个线程的生产者-消费者队列。这样的对象必然有锁争用。另一方面有 将一组对象重新偏向另一个线程的能力是有利可图的,特别是当一个线程分配多个对象时 对象并对每个对象执行初始同步操作, 但是另一个线程对它们执行后续工作,例如基于 spring 的应用程序。
JVM 尝试涵盖这两种用例并同时支持重新设置和撤销。详见Eliminating Synchronization-Related Atomic Operations with Biased Locking and Bulk Rebiasing
也就是说你的理解:
As far as I understand, the JVM would initially bias the lock towards A, and then revoke the bias the first time that thread B woke up.
并不总是正确的,即 JVM 足够聪明,可以检测到无竞争的同步并将锁重新偏向另一个线程。
以下是一些实施说明:
- HotSpot 仅支持批量重新偏置以分摊每个对象偏置撤销的成本,同时保留优化的好处。
- bulk rebias 和 bulk revoke 共享一个 safepoint/operation 名称 - RevokeBias。这非常令人困惑,需要进一步调查。
- 当且仅当撤销数量大于
BiasedLockingBulkRebiasThreshold
且小于BiasedLockingBulkRevokeThreshold
并且最近一次撤销不迟于BiasedLockingDecayTime
,其中所有转义变量都是 JVM 属性。请仔细阅读this code. - 您可以使用 属性
-XX:+PrintSafepointStatistics
追踪安全点事件。最有趣的是 EnableBiasedLocking、RevokeBias 和 BulkRevokeBias -XX:+TraceBiasedLocking
生成一个有趣的日志,其中包含有关 JVM 决策的详细描述。
练习
这是我的复制器,其中一个线程(实际上是主线程)分配监视器对象并对其执行初始同步操作,然后另一个线程执行后续工作:
package samples;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static java.lang.System.out;
public class BiasLocking {
private static final Unsafe U;
private static final long OFFSET = 0L;
static {
try {
Field unsafe = Unsafe.class.getDeclaredField("theUnsafe");
unsafe.setAccessible(true);
U = (Unsafe) unsafe.get(null);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
public static void main(String[] args) throws Exception {
ExecutorService thread = Executors.newSingleThreadExecutor();
for (int i = 0; i < 15; i++) {
final Monitor a = new Monitor();
synchronized (a) {
out.println("Main thread \t\t" + printHeader(a));
}
thread.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
synchronized (a) {
out.println("Work thread \t\t" + printHeader(a));
}
return null;
}
}).get();
}
thread.shutdown();
}
private static String printHeader(Object a) {
int word = U.getInt(a, OFFSET);
return Integer.toHexString(word);
}
private static class Monitor {
// mutex object
}
}
为了重现我的结果,请使用以下 JVM 参数:
- -XX:+UseBiasedLocking - 不需要,默认使用
- -XX:BiasedLockingStartupDelay=0 - 默认延迟 4s
- -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 - 启用安全点日志
- -XX:+TraceBiasedLocking - 非常有用的日志
- -XX:BiasedLockingBulkRebiasThreshold=1 - 在我的示例中减少迭代次数
在测试过程中,JVM 决定重新设置监视器而不是撤销
Main thread 0x7f5af4008805 <-- this is object's header word contains thread id
* Beginning bulk revocation (kind == rebias) because of object 0x00000000d75631d0 , mark 0x00007f5af4008805 , type samples.BiasLocking$Monitor
* Ending bulk revocation
Rebiased object toward thread 0x00007f5af415d800
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
0.316: BulkRevokeBias [ 10 0 0 ] [ 0 0 0 0 0 ] 0
Work thread 0x7f5af415d905 <-- this is object's header word contains thread id => biased
下一步是将锁重新偏向主线程。这部分是最难的,因为我们要打下面的heuristics:
Klass* k = o->klass();
jlong cur_time = os::javaTimeMillis();
jlong last_bulk_revocation_time = k->last_biased_lock_bulk_revocation_time();
int revocation_count = k->biased_lock_revocation_count();
if ((revocation_count >= BiasedLockingBulkRebiasThreshold) &&
(revocation_count < BiasedLockingBulkRevokeThreshold) &&
(last_bulk_revocation_time != 0) &&
(cur_time - last_bulk_revocation_time >= BiasedLockingDecayTime)) {
// This is the first revocation we've seen in a while of an
// object of this type since the last time we performed a bulk
// rebiasing operation. The application is allocating objects in
// bulk which are biased toward a thread and then handing them
// off to another thread. We can cope with this allocation
// pattern via the bulk rebiasing mechanism so we reset the
// klass's revocation count rather than allow it to increase
// monotonically. If we see the need to perform another bulk
// rebias operation later, we will, and if subsequently we see
// many more revocation operations in a short period of time we
// will completely disable biasing for this type.
k->set_biased_lock_revocation_count(0);
revocation_count = 0;
}
您可以使用 JVM 参数和我的示例来试探一下,但请记住,这非常困难,有时需要 JVM 调试。