自旋锁学习系列(3):指数后退技术

可紊 提交于 2020-03-23 21:48:51

3 月,跳不动了?>>>

上一篇中分析了测试锁的两种实现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不是非常的完美,所以引出了我们下一个主角的出现:队列锁。



易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!