关于java:The-art-of-multipropcessor-programming-读书笔记3-自旋锁与争用1

26次阅读

共计 4443 个字符,预计需要花费 12 分钟才能阅读完成。

本系列是 The art of multipropcessor programming 的读书笔记,在原版图书的根底上,联合 OpenJDK 11 以上的版本的代码进行了解和实现。并依据集体的查资料以及了解的经验,给各位想更深刻了解的人分享一些集体的材料

自旋锁与争用

1. 再论 TAS 与 TTAS 的自旋锁

在后面的章节咱们实现了 TASLock 与 TTASLock 自旋锁,因为 compareAndSet 都会导致互连线上的播送,这样会导致所有线程的提早,包含没有期待锁的线程。更蹩脚的一点是,compareAndSet 调用会让其余的处理器抛弃本人高速缓存中的所正本,这样每一个正在自旋的线程简直每次都会遇到一个缓存缺失cache miss,须要通过总线获取新的值。还有更蹩脚的是,当持有锁的线程,尝试开释锁的时候,因为互连线可能被自旋的线程所独占,所以开释可能被提早。以上就是 TASLock 为何性能如此之差的起因。

上面剖析当锁被线程 A 持有时,TTASLock 锁的行为。线程 B 第一次读锁时产生 cache 缺失,从而阻塞期待值被载入它的 cache 中。只有 A 持有锁,B 就会一直读取该值,且每次都命中 cache。这样,在 A 持有锁时,不会产生总线流量,而且也不会升高其余线程的访问速度。此外,A 开释锁也不会被正在自旋的线程所提早。

然而,在 锁开释的时候,会引起一场总线风暴:A 线程将 false 值写入锁变量来开释锁,该操作将会使自旋线程的 cache 正本立即生效。

2. Exponential Backoff(指数回退,或者称为指数弥补)

咱们在微服务零碎设计中,可能会常常看到 Backoff 这个名词。他经常出现在微服务调用失败,重试的时候,常常不会是间接重试,而是有肯定距离的重试。这个重试距离也个别不是固定的,对于同一个申请,重试距离和重试次数是有肯定关系的。最罕用的就是指数函数关系。

这个设计其实源于底层适应硬件的软件设计。首先咱们来明确一个概念,争用 (contention):多线程争用同一资源,这里指的是锁。 高争用 指的是大量线程竞争同一个锁,低争用 则指的是相同的状况。

在咱们之前实现的 TTASLock 中,lock 次要分为两步:一直读取锁状态,读取到闲暇时,尝试获取锁。如果一个线程通过这个残缺过程然而获取锁失败,其余线程获取到了这个锁,那么很可能这个锁面临着 高争用 的状况。试图获取一个高争用的资源,是应该防止的操作。因为这样线程获取资源的概率十分小,然而造成的总线流量十分大。相同,如果让线程后退一段时间,不去争用锁,这样效率会更高。

线程再次重试之前应该后退多久呢?一种比拟好的形式就是让 后退的工夫与重试的次数成正比,因为重试次数越多,高争用的可能性越高。上面是一个简略的办法:

  1. 读取锁状态
  2. 读取到闲暇时,尝试获取锁
  3. 如果获取锁失败,随机后退一段时间
  4. 反复步骤 1 ~ 3,如果获取锁失败,则将步骤 3 的后退工夫加倍,直到一个固定的最大值 maxDelay 为止。

咱们来实现下这个锁:

public class Backoff {
    private final long minDelay;
    private final long maxDelay;
    private long current;

    public Backoff(long minDelay, long maxDelay) {
        this.minDelay = minDelay;
        this.maxDelay = maxDelay;
        // 初始随机最大为 minDelay
        this.current = minDelay;
    }

    public void backoff() {
        // 应用 ThreadLocalRandom 避免并发影响随机
        long delay = ThreadLocalRandom.current().nextLong(1, current);
        // 随着次数翻倍,直到 maxDelay
        current = Math.min(current * 2L, maxDelay);
        try {Thread.sleep(delay);
        } catch (InterruptedException e) {//ignore}
    }
}
public class TTASWithBackoffLock implements Lock {
    private boolean locked = false;
    private final Backoff backoff = new Backoff(10L, 100L);
    // 操作 locked 的句柄
    private static final VarHandle LOCKED;
    static {
        try {
            // 初始化句柄
            LOCKED = MethodHandles.lookup().findVarHandle(TTASWithBackoffLock.class, "locked", boolean.class);
        } catch (Exception e) {throw new Error(e);
        }
    }

    @Override
    public void lock() {while (true) {
            // 一般读取 locked,如果被占用,则始终 SPIN
            while ((boolean) LOCKED.get(this)) {
                // 让出 CPU 资源,这是目前实现 SPIN 成果最好的让出 CPU 的形式,当线程数量远大于 CPU 数量时,成果比 Thread.yield 好,从及时性角度成果远好于 Thread.sleep
                Thread.onSpinWait();}
            // 胜利代表获取了锁
            if (LOCKED.compareAndSet(this, false, true)) {return;} else {
                // 失败则回退
                backoff.backoff();}
        }
    }

    @Override
    public void unlock() {LOCKED.setVolatile(this, false);
    }
}

之后,咱们应用 JMH 测试 TTASWithBackoffLock 与之前实现的 TTASLock 锁的性能差别:

// 测试指标为单次调用工夫
@BenchmarkMode(Mode.SingleShotTime)
// 须要预热,排除 jit 即时编译以及 JVM 采集各种指标带来的影响,因为咱们单次循环很屡次,所以预热一次就行
@Warmup(iterations = 1)
// 单线程即可
@Fork(1)
// 测试次数,咱们测试 10 次
@Measurement(iterations = 10)
// 定义了一个类实例的生命周期,所有测试线程共享一个实例
@State(value = Scope.Benchmark)
public class LockTest {
    private static class ValueHolder {int count = 0;}

    // 测试不同线程数量
    @Param(value = {"1", "2", "5", "10", "20", "50", "100"})
    private int threadsCount;

    @Benchmark
    public void testTTASWithBackoffLock(Blackhole blackhole) throws InterruptedException {test(new TTASWithBackoffLock());
    }

    @Benchmark
    public void testTTASLock(Blackhole blackhole) throws InterruptedException {test(new TTASLock());
    }

    private void test(Lock lock) throws InterruptedException {ValueHolder valueHolder = new ValueHolder();
        Thread[] threads = new Thread[threadsCount];
        // 测试累加 5000000 次
        for (int i = 0; i < threads.length; i++) {threads[i] = new Thread(() -> {for (int j = 0; j < 5000000 / threads.length; j++) {lock.lock();
                    try {valueHolder.count++;} finally {lock.unlock();
                    }
                }
            });
            threads[i].start();}
        for (int i = 0; i < threads.length; i++) {threads[i].join();}
        if (valueHolder.count != 5000000) {throw new RuntimeException("something wrong in lock implementation");
        }
    }

    public static void main(String[] args) throws RunnerException {Options opt = new OptionsBuilder().include(LockTest.class.getSimpleName()).build();
        new Runner(opt).run();}
}

其后果是:

Benchmark                         (threadsCount)  Mode  Cnt  Score   Error  Units
LockTest.testTTASLock                          1    ss   10  0.064 ± 0.005   s/op
LockTest.testTTASLock                          2    ss   10  0.138 ± 0.044   s/op
LockTest.testTTASLock                          5    ss   10  0.426 ± 0.100   s/op
LockTest.testTTASLock                         10    ss   10  0.699 ± 0.128   s/op
LockTest.testTTASLock                         20    ss   10  0.932 ± 0.241   s/op
LockTest.testTTASLock                         50    ss   10  1.162 ± 0.542   s/op
LockTest.testTTASLock                        100    ss   10  1.379 ± 0.939   s/op
LockTest.testTTASWithBackoffLock               1    ss   10  0.068 ± 0.008   s/op
LockTest.testTTASWithBackoffLock               2    ss   10  0.080 ± 0.023   s/op
LockTest.testTTASWithBackoffLock               5    ss   10  0.135 ± 0.037   s/op
LockTest.testTTASWithBackoffLock              10    ss   10  0.187 ± 0.072   s/op
LockTest.testTTASWithBackoffLock              20    ss   10  0.200 ± 0.063   s/op
LockTest.testTTASWithBackoffLock              50    ss   10  0.239 ± 0.052   s/op
LockTest.testTTASWithBackoffLock             100    ss   10  0.261 ± 0.042   s/op

Process finished with exit code 0

从后果上能够看出,性能变好了很多。

尽管基于回退的锁实现很简略,也晋升了性能。然而针对不同的机器,以及不同的配置,很难找出通用的最合适的 minDelay 以及 maxDelay

正文完
 0