本系列是《Theartofmultiprocessorprogramming》的阅读笔记,在原书的基础上,结合OpenJDK11以上的代码理解和实现。并且根据个人资料搜索和了解的经验,我会分享一些个人资料给想了解的朋友。TTASLock自旋锁,因为compareAndSet会在互连线上造成广播,这会导致所有线程延迟,包括没有等待锁的线程。更糟糕的是,compareAndSet调用会导致其他处理器丢弃自己缓存中的所有副本,这样每一个正在自旋的线程几乎每次都会遇到缓存未命中,需要通过总线获取新值.更糟糕的是,当持有锁的线程试图释放锁时,释放可能会延迟,因为互连可能被自旋线程所独占。以上就是TASLock性能这么差的原因。下面分析当锁被线程A持有时TTASLock锁的行为。线程B第一次读取锁时发生缓存未命中,阻塞等待值加载到其缓存中。只要A持有锁,B就会继续读取值,每次都命中缓存。这样,当A持有锁时,没有总线流量,也不会减慢其他线程的访问速度。此外,A释放锁不会被自旋线程延迟。但是释放锁时会引起总线风暴:线程A向锁变量写入false值释放锁,此操作会立即使自旋线程的缓存副本失效。2.ExponentialBackoff(指数退避,或称指数补偿)我们在微服务系统的设计中可能经常会看到Backoff这个词。他经常出现在微服务调用失败的时候,重试的时候往往不是直接重试,而是间隔一定时间重试。重试间隔一般不固定。对于同一个请求,重试间隔和重试次数有一定的关系。最常用的是指数函数关系。这种设计其实源于与硬件适配的底层软件设计。首先明确一个概念,争用:多个线程竞争同一个资源,这里指的是锁。高争用是指大量线程竞争同一个锁,低争用是指相反的情况。在我们之前实现的TTASLock中,获取锁主要分为两步:不断读取锁状态,空闲时尝试获取锁。如果一个线程走完全程却没有获取到锁,其他线程获取到了锁,那么很可能是这个锁面临高争用。试图获取竞争激烈的资源是应该避免的操作。因为线程获取资源的概率很小,但由此产生的总线流量却非常大。相反,如果让线程在不竞争锁的情况下退却一段时间,效率会更高。在再次重试之前线程应该退出多长时间?更好的办法是让退避时间与重试次数成正比,因为重试次数越多,高争用的可能性就越大。下面是一个简单的方法:当读取锁状态,读到空闲时,尝试获取锁。如果获取锁失败,则随机后退一段时间,重复步骤1~3。如果获取锁失败,则将步骤3的退避时间加倍,直到达到固定的最大值maxDelay。让我们实现这个锁:publicclassBackoff{privatefinallongminDelay;私人最终长最大延迟;私人长电流;publicBackoff(longminDelay,longmaxDelay){this.minDelay=minDelay;this.maxDelay=maxDelay;//初始随机最大值为minDelaythis.current=minDelay;}publicvoidbackoff(){//使用ThreadLocalRandom防止并发影响随机longdelay=ThreadLocalRandom.current().nextLong(1,current);//加倍次数直到maxDelaycurrent=Math.min(current*2L,maxDelay);尝试{Thread.sleep(延迟);}catch(InterruptedExceptione){//ignore}}}publicclassTTASWithBackoffLockimplementsLock{privatebooleanlocked=false;privatefinalBackoffbackoff=newBackoff(10L,100L);//操作锁定句柄privatestaticfinalVarHandleLOCKED;static{try{//初始化句柄LOCKED=MethodHandles.lookup().findVarHandle(TTASWithBackoffLock.class,"锁定",boolean.class);}catch(Exceptione){thrownewError(e);}}@Overridepublicvoidlock(){while(true){//普通读锁,如果被占用,则一直为SPINwhile((boolean)LOCKED.get(this)){//放弃CPU资源,这是目前实现SPIN放弃CPU的最好方式。当线程数远大于CPU数时,效果优于Thread.yield,效果优于Thread.sleepThread.onSpinWait();}//成功表示已经获取到锁if(LOCKED.compareAndSet(this,false,true)){return;}else{//如果失败,回退到backoff.backoff();}}}@Overridepublicvoidunlock(){LOCKED.setVolatile(this,false);}}之后,我们使用JMH来测试TTASWithBackoffLock和之前实现的TTASLock锁的性能区别://测试指标是单次调用时间@BenchmarkMode(Mode.SingleShotTime)//需要预热,排除JIT的影响即时编译和JVM收集各种指标,因为我们单循环很多次,所以warmup就一次@Warmup(iterations=1)//单线程就够了@Fork(1)//测试次数,我们测试10次@Measurement(迭代次数=10)//定义类实例的生命周期,所有测试线程共享一个实例@State(value=Scope.Benchmark)publicclassLockTest{privatestaticclassValueHolder{intcount=0;}//测试不同的线程数@Param(value={"1","2","5","10","20","50","100"})privateintthreadsCount;@BenchmarkpublicvoidtestTTASWithBackoffLock(Blackholeblackhole)throwsInterruptedException{test(newTTASWithBackoffLock());}@BenchmarkpublicvoidtestTTASLock(Blackholeblackhole)throwsInterruptedException{test(newTTASLock());}privatevoidtest(Locklock)throwsInterruptedException{ValueHoldervalueHolder=newValueHolder();线程[]线程=新线程[threadsCount];//测试累计5000000次for(inti=0;i
