Java 中的忙等待
啥叫“忙等待”(或者忙等)呢?就是在忙着干“等待”这件事,英文是busy waiting。针对Java环境的含义是:在多线程下,其中一个线程通过Thread.sleep()方法等待其他线程满足其进一步执行的条件。下面就是忙等的典型场景:
共享变量X;
线程A:
while(X 不满足我的条件) {
Thread.sleep(1000);
}
线程B:
使X满足线程A的条件
忙等是正确的业务逻辑,但是它带来的问题是线程A可能会频繁的休眠,也就会导致CPU在线程休眠和就绪后需要切换(比忙等还差的写法是不休眠,一直不停地重试)。所以一般给出的解决方案是使用wait来代替sleep():
共享变量X;
线程A:
synchronized(X) {
if (X 不满足我的条件) {
X.wait();
}
}
线程B:
synchronized(X) {
使X满足线程A的条件;
X.notifyAll();
}
你可能已经发现改成这样会有bug(上面的sleep写法虽然性能略差但是毕竟逻辑没问题),比如线程A在判断条件成立进入if块后,还没执行wait(),线程B先执行了notify():这样就再也没有线程能唤醒线程A类,A就死掉了。
对这个问题的修复是使用Future。将线程B封装为FutureTask,通过线程A启动线程池执行线程B的任务并获取结果。这样在获取的地方线程A就hold住了。因为Future的get()实现特意考虑了两次判断。
故事本该到此结束。但是让我注意到这个问题的是一段分布式环境下的Thread.sleep()代码,由于在分布式环境下,线程A和线程B通常会是不同的JVM,这样的是不是忙等呢?该怎么解决?比如下面的逻辑:
// 两个JVM都向数据库中写入相同资源的数据,如果数据库已经存在记录则更新
共享资源X;
JVM1:
1.查询X的记录,如果存在则更新;不存在则获取分布式锁;
2.如果获取锁成功并且记录依然不存在则插入,记录存在则更新;
3.如果获取锁失败,说明其他JVM拿到了锁,只需要再次查询记录,查到记录后更新即可。
JVM2:
同上
从概念上将分布式环境也是忙等,这个忙等出现在第三步的“再次查询记录”:
while(记录没查到 && 没有超时) {
Thread.sleep();
查询记录;
}
由于是分布式环境,无法通过Java自身的语义或API来解决。那怎么办呢?
目前在这样的逻辑下,我没有找到合适的解决方案。但是如果转换思路,倒是可以搞定。比如在获取分布式锁失败后写入待执行任务队列表,而在插入成功后发送异步消息。当然具体的实现还有不少细节要考虑。
如果逻辑简单,只是插入或者更新数据库,也可以尝试使用upsert类似的功能
大家也可以考虑一下有没有其他方案!
关于Thread.sleep(),主要有两点要注意:
- 如果当前线程获取到的有锁,sleep不会让出锁
- 线程睡眠到期自动苏醒,并返回到可运行状态(就绪),不是运行状态。所以sleep()中指定的时间是线程不运行的最短时间,sleep方法不能作为精确的时间控制。
