这个问题是关于有偏置锁定的启发式Java使用之一。下一段是给未来的读者。我怀疑任何可以回答这个问题的人都可以安全地跳过。
据我所知,据我所知,曾几何时,人们注意到Java有很多线程安全的课程,但是它们的实例倾向于仅由一个线程使用,因此Sun引入了有偏见的锁定以利用这一点。问题是,如果您"猜错"并尝试偏向一个需要从两个线程中使用的锁,那么即使没有争议,也需要撤消偏见("被撤销"),这是如此昂贵,以至于JVM试图避免它,即使这有时意味着在有偏见的锁定可能是净赢的情况下错过。我也知道有时JVM决定做"批量"重差,并将许多类型的所有锁迁移到其他线程。这个问题不是那个。出于这个问题的目的,假设我只有两个线程和一个锁。(真实情况更加复杂,涉及线池,但现在让我们忽略它。真的,假装我没有提到它。)假设Thread A沿着无限的环路沿着"睡几秒钟睡觉"的线条。,在锁定下增加整数,重复"。(这并不是真的没有用,但这应该足以说明要点。)与此同时,线程B也有类似的循环,但是睡眠时间是几个小时而不是几秒钟。进一步假设调度程序是神奇的,并保证永远不会有任何争论。(先发制人:如果那是真的,我们可能只是一个动荡的。
现在,假设我们关心线程醒来并成功地增加其整数之间的平均延迟。据我了解,JVM最初会将锁定偏向A,然后在线程B首次醒来时撤销偏见。
我的问题是:JVM是否会意识到其最初的猜测基本上是正确的,从而再次偏向于线程A的锁?
从理论上讲是可能的,但需要一些其他条件和特殊的JVM设置。
理论
有某些对象显然是无利可图的,例如涉及两个或多个线程的生产者 - 消费者队列。这样的对象必然具有锁定争论。另一方面有在一个线程分配许多线程的情况下,将一组对象重现的能力是有利可图的情况对象并在每个上执行初始同步操作,但是另一个线程对它们进行后续工作,例如基于春季的应用程序。
JVM试图涵盖两个用例,并同时支持重新出现和撤销。请参阅消除与同步相关的原子操作的详细说明
换句话说,您的理解:
据我了解,JVM最初会偏向锁 a,然后在第一次醒来时撤销偏见。
并不总是正确的,即JVM足够聪明,可以检测无害的同步并将锁定向另一线程。
这是一些实现注释:
- 热点仅支持大量的重新聚集,以摊销每个对象偏差的撤销成本,同时保留优化的好处。
- 散装Rebias和Bulk Revoke共享一个SafePoint/operation Name -RevokeBias。这很令人困惑,需要进行其他调查。
-
随后的散装rebias是可能的,并且仅当吊销数量大于
BiasedLockingBulkRebiasThreshold
且小于BiasedLockingBulkRevokeThreshold
,而最新的撤销不迟于BiasedLockingDecayTime
,其中所有ESCALE的变量均为JVM属性。请仔细阅读此代码。 - 您可以使用属性
-XX:+PrintSafepointStatistics
跟踪SafePoint事件。最有趣的是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 tt" + printHeader(a));
}
thread.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
synchronized (a) {
out.println("Work thread tt" + 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: printSafointStatistics -xx:printSafointStatisticsCount = 1-启用SafePoint Log
- -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
下一步是将锁定向主线程。这部分是最难的,因为我们必须按下以下启发式方法:
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调试。