上一篇中分析了测试锁的两种实现TASLock和TTASLock,主要对这两种锁的性能进行了分析。对于TTASLock,我们知道比TASLock性能上要好很多,具体分析已经讲过了。我们最后也说了,TTASLock虽然比TASLock大有改进,但是在性能上还是不够理想。这一篇的目的就是针对TTASLock做一下改进。
我们再来看一下TTASLock的实现源码和加锁的流程图:
/**
*
* Test test and set lock
*
*/
public class TTASLock {
private AtomicBoolean state = new AtomicBoolean(false);
// 加锁
public void lock() {
while (true) {
while (state.get()) {
// 自旋
}
if (!state.getAndSet(true)) {
break;
}
}
}
// 解锁
public void unlock() {
state.set(false);
}
}
加锁流程图如下:
从上文我们知道,对于TTASLock锁,性能问题主要出现在解锁上。一旦一个已经获得锁的线程执行解锁操作。其他线程都会产生缓存缺失,将会由”本地自旋”转变为从共享服务器中去获取状态值。这会消耗大量的总线资源。所以,如果我们要对TTASLock改进的话,需要从这里去想办法。
这里我们就要一直成为指数后退的技术。“指数后退”名字听起来挺吓人的,但是原理上很简单,实现上也不是多复杂的事情。我举个例子大家都明白了。我们用迅雷账户登录的时候,如果网络断掉了。迅雷会重新尝试登录。第一次尝试可能是5秒钟以后。如果第一次尝试失败,第二次尝试就会在10秒钟以后登录。依次类推,失败的次数越多,尝试登录的延时时间就越长,成指数性“后退”。很简单吧。
迅雷掉线“指数后退”方式的重登陆。
这个和加锁有什么关系呢?
我们再来深入看一下TTASLock的加锁过程,一旦一个线程获取不到锁,它会一直的在本地自旋等待。如果有一百多个线程争锁,就有99个(有一个获得了锁)在本地自旋等待。而那个获得锁的线程一旦释放锁,这99个线程都会产生缓存缺失。一直总线风暴之后,还是只有一个线程能获得锁。其他的依然在本地不断的自旋等待。过程分析完了,发现什么问题了吗?问题在于既然每次只有一个线程获得锁,需要所有的线程同时自旋等待吗?我们难道不能让等待线程“指数后退”吗?
OK,理论到此为止。让我们按照这个思路来实现吧。既然指数后退,我们先实现这部分功能。我们用一个BackOff类来代表这个抽象。
public class Backoff {
// 需要对后退时间设置一个最大值和最小值
final int minDelay, maxDelay;
int limit;
final Random random;
public Backoff(int min, int max) {
maxDelay = max;
minDelay = min;
limit = minDelay;
random = new Random();
}
public void backoff() throws InterruptedException {
// 计算需要睡眠的时间
int delay = random.nextInt(limit);
// 重新计算limit,每次两倍增加。但最大等于maxDelay
limit = Math.min(maxDelay, 2 * limit);
// 当前线程睡眠
Thread.sleep(delay);
}
}
然后我们把它用来改造我们的TTASLock:
class BackoffLock {
private AtomicBoolean state = new AtomicBoolean(false);
private static final int MIN_DELAY = ...;
private static final int MAX_DELAY = ...;
// 加锁
public void lock() throws InterruptedException {
Backoff back = new Backoff(MIN_DELAY, MAX_DELAY);
while (true) {
while (state.get()) {
// 自旋
}
if (!state.getAndSet(true)) {
return;
}
//关键点,一旦获取锁失败,就指数后退
else {
back.backoff();
}
}
}
// 解锁
public void unlock() {
state.set(false);
}
}
上面实现和TTASLock相比就多了指数后退这一步操作。其他的都没有什么变化。BackoffLock性能上比TAS系列要好的多。因为它不会在同时(随机的妙用哦)存在大量的线程去争锁,不会造成总线风暴。BackoffLock最关键的地方是确定睡眠的最小值和最大值,这个需要大量的测试才能确定合适的值,而且这两个值对性能影响非常大。所以BackoffLock锁的可移植性不好,因为不同的机器对这两个值都有不同的要求。
正因为BackoffLock不是非常的完美,所以引出了我们下一个主角的出现:队列锁。
来源:oschina
链接:https://my.oschina.net/u/578710/blog/142320